If you already have your HTML written and want to convert it into a Mithril template, paste the HTML below and press the "Convert" button.
+
+HTML to Mithril Template Converter
+
+
HTML to Mithril Template Converter
+
+ If you have HTML you want to convert into a Mithril template, paste it below
+ and press the "Convert" button. In case you're wondering, this itself is a
+ Mithril app.
+
\ No newline at end of file
+m.mount(document.getElementById("converter"), templateConverter)
+
diff --git a/docs/layout/tools/template-converter.js b/docs/layout/tools/template-converter.js
index b8cd01cf..55217ba3 100644
--- a/docs/layout/tools/template-converter.js
+++ b/docs/layout/tools/template-converter.js
@@ -1,90 +1,145 @@
-var templateConverter = {};
+/* global m: false */
+// TODO: ensure this targets the current API.
+window.templateConverter = (function () {
+ "use strict"
-templateConverter.DOMFragment = function(markup) {
- if (markup.indexOf(" -1) return [new DOMParser().parseFromString(markup, "text/html").childNodes[1]]
- var container = document.createElement("div");
- container.insertAdjacentHTML("beforeend", markup);
- return container.childNodes;
-}
-templateConverter.VirtualFragment = function recurse(domFragment) {
- var virtualFragment = [];
- for (var i = 0, el; el = domFragment[i]; i++) {
- if (el.nodeType == 3) {
- virtualFragment.push(el.nodeValue);
- }
- else if (el.nodeType == 1) {
- var attrs = {};
- for (var j = 0, attr; attr = el.attributes[j]; j++) {
- attrs[attr.name] = attr.value;
- }
-
- virtualFragment.push({tag: el.nodeName.toLowerCase(), attrs: attrs, children: recurse(el.childNodes)});
+ function each(list, f) {
+ for (var i = 0; i < list.length; i++) {
+ f(list[i], i)
}
}
- return virtualFragment;
-}
-templateConverter.Template = function recurse() {
- if (Object.prototype.toString.call(arguments[0]) == "[object String]") {
- return new recurse(new templateConverter.VirtualFragment(new templateConverter.DOMFragment(arguments[0])));
- }
-
- var virtualFragment = arguments[0], level = arguments[1]
- if (!level) level = 1;
-
- var tab = "\n" + new Array(level + 1).join("\t");
- var virtuals = [];
- for (var i = 0, el; el = virtualFragment[i]; i++) {
- if (typeof el == "string") {
- if (el.match(/\t| {2,}/g) && el.trim().length == 0) virtuals.indented = true;
- else virtuals.push('"' + el.replace(/"/g, '\\"').replace(/\r/g, "\\r").replace(/\n/g, "\\n") + '"');
+
+ function createFragment(markup) {
+ if (markup.indexOf("= 0) {
+ return [
+ new DOMParser()
+ .parseFromString(markup, "text/html")
+ .childNodes[1]
+ ]
}
- else {
- var virtual = "";
- if (el.tag != "div") virtual += el.tag;
- if (el.attrs["class"]) {
- virtual += "." + el.attrs["class"].replace(/\t+/g, " ").split(" ").join(".");
- delete el.attrs["class"];
+
+ var container = document.createElement("div")
+ container.insertAdjacentHTML("beforeend", markup)
+ return container.childNodes
+ }
+
+ function createVirtual(fragment) {
+ var list = []
+
+ each(fragment, function (el) {
+ if (el.nodeType === 3) {
+ list.push(el.nodeValue)
+ } else if (el.nodeType === 1) {
+ var attrs = {}
+
+ each(el.attributes, function (attr) {
+ attrs[attr.name] = attr.value
+ })
+
+ list.push({
+ tag: el.nodeName.toLowerCase(),
+ attrs: attrs,
+ children: createVirtual(el.childNodes)
+ })
}
- var attrNames = Object.keys(el.attrs).sort()
- for (var j = 0, attrName; attrName = attrNames[j]; j++) {
- if (attrName != "style") virtual += "[" + attrName + "='" + el.attrs[attrName].replace(/'/g, "\\'") + "']";
+ })
+
+ return list
+ }
+
+ function TemplateBuilder(virtual, level) {
+ this.virtual = virtual
+ this.level = level
+ this.virtuals = []
+ this.indented = false
+ }
+
+ TemplateBuilder.prototype = {
+ addVirtualString: function (el) {
+ if (/\t| {2,}/.test(el) && /^\s*/.test(el)) {
+ this.indented = true
+ } else {
+ this.virtuals.push('"' + el.replace(/(["\r\n])/g, "\\$1") + '"')
}
- if (virtual == "") virtual = "div"
- virtual = '"' + virtual + '"';
-
- var style = ""
+ },
+
+ addVirtualAttrs: function (el) {
+ var virtual = el.tag === "div" ? "" : el.tag
+
+ if (el.attrs.class) {
+ virtual += "." + el.attrs.class.replace(/\s+/g, ".")
+ el.attrs.class = undefined
+ }
+
+ each(Object.keys(el.attrs).sort(), function (attrName) {
+ if (attrName === "style") return
+ virtual += "[" + attrName + "='"
+ virtual += el.attrs[attrName].replace(/'/g, "\\'") + "']"
+ })
+
+ if (virtual === "") virtual = "div"
+ virtual = '"' + virtual + '"'
+
if (el.attrs.style) {
- virtual += ", {style: " + ("{\"" + el.attrs.style.replace(/:/g, "\": \"").replace(/;/g, "\", \"") + "}").replace(/, "}|"}/, "}") + "}"
+ var style = "{\"" + el.attrs.style
+ .replace(/:/g, "\": \"")
+ .replace(/;/g, "\", \"") + "}"
+ virtual += ", {style: " + style.replace(/(, )"}/, "}") + "}"
}
-
- if (el.children.length > 0) {
- virtual += ", " + recurse(el.children, level + 1);
+
+ if (el.children.length !== 0) {
+ var builder = new TemplateBuilder(el.children, this.level + 1)
+ virtual += ", " + builder.complete()
+ }
+
+ this.virtuals.push("m(" + virtual + ")")
+ },
+
+ complete: function () {
+ var tab = "\n"
+ for (var i = 0; i <= this.level; i++) tab += "\t"
+
+ each(this.virtual, function (el) {
+ if (typeof el === "string") {
+ this.addVirtualString(el)
+ } else {
+ this.addVirtualAttrs(el)
+ }
+ }.bind(this))
+
+ if (!this.indented) tab = ""
+
+ if (this.virtuals.length === 1 && this.virtuals[0][0] === "\"") {
+ return this.virtuals.join(", ")
+ } else {
+ var body = this.virtuals.join("," + tab)
+ return "[" + tab + body + tab.slice(0, -1) + "]"
}
- virtual = "m(" + virtual + ")";
- virtuals.push(virtual);
}
}
- if (!virtuals.indented) tab = "";
-
- var isInline = virtuals.length == 1 && virtuals[0].charAt(0) == '"';
- var template = isInline ? virtuals.join(", ") : "[" + tab + virtuals.join("," + tab) + tab.slice(0, -1) + "]";
- return new String(template);
-}
-templateConverter.controller = function() {
- this.source = m.prop("");
- this.output = m.prop("");
-
- this.convert = function() {
- return this.output(new templateConverter.Template(this.source()));
- };
-
-};
+ return {
+ controller: function () {
+ this.source = m.prop("")
+ this.output = m.prop("")
-templateConverter.view = function(ctrl) {
- return m("div", [
- m("textarea", {autofocus: true, style: {width:"100%", height: "40%"}, onchange: m.withAttr("value", ctrl.source)}, ctrl.source()),
- m("button", {onclick: ctrl.convert.bind(ctrl)}, "Convert"),
- m("textarea", {style: {width:"100%", height: "40%"}}, ctrl.output())
- ]);
-};
\ No newline at end of file
+ this.convert = function () {
+ var source = createVirtual(createFragment(this.source()))
+ return this.output(new TemplateBuilder(source, 1).complete())
+ }.bind(this)
+ },
+
+ view: function (ctrl) {
+ return m("div", [
+ m("textarea", {
+ autofocus: true,
+ style: {width: "100%", height: "40%"},
+ onchange: m.withAttr("value", ctrl.source)
+ }, ctrl.source()),
+ m("button", {onclick: ctrl.convert}, "Convert"),
+ m("textarea", {style: {width: "100%", height: "40%"}},
+ ctrl.output())
+ ])
+ }
+ }
+})()
diff --git a/package.json b/package.json
index ffd7ac2e..c84013ae 100644
--- a/package.json
+++ b/package.json
@@ -1,46 +1,48 @@
{
- "name": "mithril",
- "description": "Mithril.js beta build - use this to help us test the releases before they are released",
- "version": "0.2.1",
- "repository": {
- "type": "git",
- "url": "git@github.com:lhorie/mithril.js.git"
- },
- "scripts": {
- "test": "grunt test"
- },
- "main": "mithril.js",
- "devDependencies": {
- "grunt": "*",
- "grunt-cli": "*",
- "grunt-contrib-copy": "*",
- "grunt-contrib-uglify": "*",
- "grunt-contrib-clean": "*",
- "grunt-contrib-concat": "*",
- "grunt-execute": "*",
- "grunt-md2html": "*",
- "grunt-replace": "*",
- "grunt-contrib-qunit": "*",
- "grunt-zip": "*",
- "grunt-jsfmt": "git://github.com/ysimonson/grunt-jsfmt",
-
- "grunt-contrib-connect": "~0.7.1",
- "grunt-contrib-jshint": "~0.10.0",
- "grunt-contrib-watch": "~0.6.1",
- "grunt-jscs": "^1.1.0",
- "grunt-sauce-tunnel": "^0.2.1",
- "load-grunt-config": "^0.9.2",
- "merge": "^1.1.3",
- "publish": "~0.3.2",
- "grunt-saucelabs": "*",
- "request": "~2.35.0",
- "q": "~1.0.0",
- "saucelabs": "~0.1.1",
- "sauce-tunnel": "~2.0.6",
- "colors": "~0.6.2",
- "lodash": "~2.4.1"
- },
- "main": "mithril.js",
- "licenses": [{"type": "MIT", "url": "http://opensource.org/licenses/MIT"}],
- "files": ["mithril.min.js", "mithril.min.js.map", "mithril.js", "README.*"]
+ "name": "mithril",
+ "description": "Mithril.js beta build - use this to help us test the releases before they are released",
+ "version": "0.2.1",
+ "repository": {
+ "type": "git",
+ "url": "git@github.com:lhorie/mithril.js.git"
+ },
+ "scripts": {
+ "test": "grunt test"
+ },
+ "main": "mithril.js",
+ "devDependencies": {
+ "chai": "^3.4.0",
+ "eslint": "^1.7.3",
+ "grunt": "*",
+ "grunt-cli": "*",
+ "grunt-contrib-clean": "*",
+ "grunt-contrib-connect": "~0.7.1",
+ "grunt-contrib-copy": "*",
+ "grunt-contrib-uglify": "*",
+ "grunt-eslint": "^17.3.1",
+ "grunt-md2html": "*",
+ "grunt-mocha-phantomjs": "^2.0.0",
+ "grunt-replace": "*",
+ "grunt-saucelabs": "*",
+ "grunt-saucelabs-browsers": "^0.2.0",
+ "grunt-zip": "*",
+ "load-grunt-config": "^0.9.2",
+ "mocha": "^2.3.3",
+ "request": "~2.35.0",
+ "saucelabs": "~0.1.1",
+ "sinon": "^1.17.2",
+ "sinon-chai": "^2.8.0"
+ },
+ "licenses": [
+ {
+ "type": "MIT",
+ "url": "http://opensource.org/licenses/MIT"
+ }
+ ],
+ "files": [
+ "mithril.min.js",
+ "mithril.min.js.map",
+ "mithril.js",
+ "README.*"
+ ]
}
diff --git a/test-deps/dom.js b/test-deps/dom.js
new file mode 100644
index 00000000..e90499dc
--- /dev/null
+++ b/test-deps/dom.js
@@ -0,0 +1,16 @@
+/* eslint-env browser, mocha */
+/* global m, mock */
+this.dom = function (cb) {
+ "use strict"
+ context("(requires real DOM)", function () {
+ before(function () {
+ m.deps(window)
+ })
+
+ after(function () {
+ m.deps(mock)
+ })
+
+ cb()
+ })
+}
diff --git a/test-deps/mock.js b/test-deps/mock.js
new file mode 100644
index 00000000..5bf78e80
--- /dev/null
+++ b/test-deps/mock.js
@@ -0,0 +1,258 @@
+;(function () { // eslint-disable-line no-extra-semi
+ "use strict"
+
+ /* eslint-disable no-extend-native */
+ Array.prototype.forEach = Array.prototype.forEach || function (callback) {
+ for (var i = 0; i < this.length; i++) {
+ callback(this[i], i, this)
+ }
+ }
+
+ Array.prototype.indexOf = Array.prototype.indexOf || function (item) {
+ for (var i = 0; i < this.length; i++) {
+ if (this[i] === item) return i
+ }
+ return -1
+ }
+
+ Array.prototype.map = Array.prototype.map || function (callback) {
+ var results = []
+ this.forEach(function (value, i, array) {
+ results.push(callback(value, i, array))
+ })
+ return results
+ }
+
+ Array.prototype.filter = Array.prototype.filter || function (callback) {
+ var results = []
+ this.forEach(function (value, i, array) {
+ if (callback(value, i, array)) results.push(value)
+ })
+ return results
+ }
+
+ Object.keys = Object.keys || function (obj) {
+ var keys = []
+ for (var i in obj) if ({}.hasOwnProperty.call(obj, i)) {
+ keys.push(i)
+ }
+ return keys
+ }
+ /* eslint-enable no-extend-native */
+})()
+
+window.mock = (function () {
+ "use strict"
+
+ var window = {}
+ var document = window.document = {
+ // FIXME: add document.createRange().createContextualFragment()
+
+ childNodes: [],
+
+ createElement: function (tag) {
+ return {
+ style: {},
+ childNodes: [],
+ nodeType: 1,
+ nodeName: tag.toUpperCase(),
+ appendChild: document.appendChild,
+ removeChild: document.removeChild,
+ replaceChild: document.replaceChild,
+ insertBefore: function (node, reference) {
+ node.parentNode = this
+ var referenceIndex = this.childNodes.indexOf(reference)
+
+ var index = this.childNodes.indexOf(node)
+ if (index > -1) this.childNodes.splice(index, 1)
+
+ if (referenceIndex < 0) this.childNodes.push(node)
+ else this.childNodes.splice(referenceIndex, 0, node)
+ },
+
+ insertAdjacentHTML: function (position, html) {
+ // TODO: accept markup
+ if (position === "beforebegin") {
+ this.parentNode.insertBefore(
+ document.createTextNode(html),
+ this)
+ } else if (position === "beforeend") {
+ this.appendChild(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) {
+ return this[name]
+ },
+
+ addEventListener: function () {},
+ removeEventListener: function () {}
+ }
+ },
+
+ createElementNS: function (namespace, tag) {
+ var element = document.createElement(tag)
+ element.namespaceURI = namespace
+ return element
+ },
+
+ createTextNode: function (text) {
+ return {nodeValue: text.toString()}
+ },
+
+ 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
+ },
+
+ appendChild: function (child) {
+ var index = this.childNodes.indexOf(child)
+ if (index > -1) this.childNodes.splice(index, 1)
+ this.childNodes.push(child)
+ child.parentNode = this
+ },
+
+ removeChild: function (child) {
+ var index = this.childNodes.indexOf(child)
+ this.childNodes.splice(index, 1)
+ child.parentNode = null
+ },
+
+ // getElementsByTagName is only used by JSONP tests, it's not required
+ // by Mithril
+ getElementsByTagName: function (name) {
+ name = name.toLowerCase()
+ var out = []
+
+ function traverse(node){
+ if (node.childNodes && node.childNodes.length > 0) {
+ node.childNodes.forEach(function (curr) {
+ if (curr.nodeName.toLowerCase() === name) {
+ out.push(curr)
+ }
+ traverse(curr)
+ })
+ }
+ }
+
+ traverse(document)
+ return out
+ }
+ }
+
+ document.documentElement = document.createElement("html")
+
+ window.scrollTo = function () {}
+
+ ;(function (window) {
+ // This is an actual conforming implementation of the
+ // requestAnimationFrame spec, with the nonstandard extension of
+ // rAF.$resolve for running the callbacks. It works in Node and the
+ // browser.
+ // https://html.spec.whatwg.org/multipage/#animation-frames
+ //
+ // Adding and removing callbacks run in constant time. Please don't
+ // modify this unless it actually has an edge case bug. It will break
+ // other tests
+ var callbacks = []
+ var id = 0
+ var indices = {}
+
+ function requestAnimationFrame(callback) {
+ id++
+ indices[id] = callbacks.length
+ callbacks.push({
+ callback: callback,
+ id: id
+ })
+ return id
+ }
+
+ window.cancelAnimationFrame = function (id) {
+ var index = indices[id]
+ if (index !== 0) {
+ indices[id] = 0
+ callbacks[index] = undefined
+ }
+ }
+
+ var nanotime = typeof process === "object" ? function () {
+ var time = process.hrtime() // eslint-disable-line no-undef
+ return time[0] * 1e9 + time[1]
+ } : typeof performance === "object" ? function () {
+ return performance.now()
+ } : function () {
+ // PhantomJS 1 doesn't have the Performance API implemented.
+ return +new Date()
+ }
+
+ requestAnimationFrame.$resolve = function () {
+ var list = callbacks
+ callbacks = []
+ indices = {}
+
+ for (var i = 0; i < list.length; i++) {
+ var data = list[i]
+ if (data !== undefined) {
+ data.callback.call(data.id, nanotime())
+ }
+ }
+ }
+
+ window.requestAnimationFrame = requestAnimationFrame
+ })(window)
+
+ window.XMLHttpRequest = (function () {
+ function Request() {
+ 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
+ })()
+
+ var location = window.location = {search: "", pathname: "", hash: ""}
+
+ window.history = {
+ $$length: 0,
+
+ pushState: function (data, title, url) {
+ window.history.$$length++
+ location.pathname = location.search = location.hash = url
+ },
+
+ replaceState: function (data, title, url) {
+ location.pathname = location.search = location.hash = url
+ }
+ }
+
+ return window
+})()
diff --git a/test/.eslintrc b/test/.eslintrc
new file mode 100644
index 00000000..205b7860
--- /dev/null
+++ b/test/.eslintrc
@@ -0,0 +1,18 @@
+{
+ "extends": "../.eslintrc",
+ "env": {
+ "mocha": true
+ },
+ "globals": {
+ "sinon": false,
+ "expect": false,
+ "mock": false,
+ "dom": false,
+ "m": false
+ },
+ "rules": {
+ "max-statements": 0,
+ "no-unused-expressions": 0,
+ "no-warning-comments": 0
+ }
+}
diff --git a/test/index.html b/test/index.html
new file mode 100644
index 00000000..6880ccfe
--- /dev/null
+++ b/test/index.html
@@ -0,0 +1,73 @@
+
+
+Mithril test suite
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/input-cursor.html b/test/input-cursor.html
new file mode 100644
index 00000000..8ee57964
--- /dev/null
+++ b/test/input-cursor.html
@@ -0,0 +1,73 @@
+
+Input cursor test
+
Things to check:
+
+
+ Typing in the fields below should not move the cursor to the end of the
+ input. Especially in Chrome.
+
+
All inputs should update with the same value.
+
+ Typing in an input should not prevent it from being updated by other
+ inputs.
+
+
+
+
+
+
+
diff --git a/test/mithril.deferred.js b/test/mithril.deferred.js
new file mode 100644
index 00000000..1f31eb1c
--- /dev/null
+++ b/test/mithril.deferred.js
@@ -0,0 +1,289 @@
+describe("m.deferred()", function () {
+ "use strict"
+
+ // Let unchecked exceptions bubble up in order to allow meaningful error
+ // messages in common cases like null reference exceptions due to typos.
+ // An unchecked exception is defined as an object that is a subclass of
+ // Error (but not a direct instance of Error itself) - basically anything
+ // that can be thrown without an explicit `throw` keyword and that we'd
+ // never want to programmatically manipulate. In other words, an unchecked
+ // error is one where we only care about its line number and where the only
+ // reasonable way to deal with it is to change the buggy source code that
+ // caused the error to be thrown in the first place.
+ //
+ // By contrast, a checked exception is defined as anything that is
+ // explicitly thrown via the `throw` keyword and that can be
+ // programmatically handled, for example to display a validation error
+ // message on the UI. If an exception is a subclass of Error for whatever
+ // reason, but it is meant to be handled as a checked exception (i.e.
+ // follow the rejection rules for A+), it can be rethrown as an instance
+ // of Error.
+ //
+ // This implementation deviates from the Promises/A+ spec in two ways:
+ //
+ // 1) A+ requires the `then` callback to be called asynchronously (this
+ // requires a setImmediate polyfill, which cannot be implemented in a
+ // reasonable way for Mithril's purpose - the possible polyfills are
+ // either too big or too slow). This implementation calls the `then`
+ // callback synchronously.
+ // 2) A+ swallows exceptions in a unrethrowable way, i.e. it's not possible
+ // to see default error messages on the console for runtime errors thrown
+ // from within a promise chain. This throws such checked exceptions.
+
+ it("exists", function () {
+ expect(m.deferred).to.be.a("function")
+ })
+
+ it("resolves values", function () {
+ var value = m.prop()
+ var deferred = m.deferred()
+
+ deferred.promise.then(value)
+ deferred.resolve("test")
+
+ expect(value()).to.equal("test")
+ })
+
+ it("resolves values returned in `then` method", function () {
+ var value = m.prop()
+ var deferred = m.deferred()
+
+ deferred.promise
+ .then(function () { return "foo" })
+ .then(value)
+ deferred.resolve("test")
+
+ expect(value()).to.equal("foo")
+ })
+
+ it("passes rejections through second `then` handler", function () {
+ var obj = {}
+ var value1 = m.prop(obj)
+ var value2 = m.prop(obj)
+ var deferred = m.deferred()
+
+ deferred.promise.then(value1, value2)
+ deferred.reject("test")
+
+ expect(value1()).to.equal(obj)
+ expect(value2()).to.equal("test")
+ })
+
+ it("passes rejections through `catch`", function () {
+ var value = m.prop()
+ var deferred = m.deferred()
+
+ deferred.promise.catch(value)
+ deferred.reject("test")
+
+ expect(value()).to.equal("test")
+ })
+
+ it("can resolve from a `then` rejection handler", function () {
+ var value = m.prop()
+ var deferred = m.deferred()
+
+ deferred.promise
+ .then(null, function () { return "foo" })
+ .then(value)
+ deferred.reject("test")
+
+ expect(value()).to.equal("foo")
+ })
+
+ it("can resolve from a `catch`", function () {
+ var value = m.prop()
+ var deferred = m.deferred()
+
+ deferred.promise
+ .catch(function () { return "foo" })
+ .then(value)
+ deferred.reject("test")
+
+ expect(value()).to.equal("foo")
+ })
+
+ it("can reject by throwing an `Error`", function () {
+ var value1 = m.prop()
+ var value2 = m.prop()
+ var deferred = m.deferred()
+
+ deferred.promise
+ .then(function () { throw new Error() })
+ .then(value1, value2)
+ deferred.resolve("test")
+
+ expect(value1()).to.not.exist
+ expect(value2()).to.be.an("error")
+ })
+
+ // FIXME: this is a bug.
+ xit("synchronously throws subclasses of Errors on creation", function () {
+ expect(function () {
+ m.deferred().reject(new TypeError())
+ }).to.throw()
+ })
+
+ it("synchronously throws subclasses of Errors thrown from its `then` fufill handler", function () { // eslint-disable-line
+ expect(function () {
+ var deferred = m.deferred()
+ deferred.promise.then(function () { throw new TypeError() })
+ deferred.resolve()
+ }).to.throw()
+ })
+
+ it("synchronously throws subclasses of Errors thrown from its `then` rejection handler", function () { // eslint-disable-line
+ expect(function () {
+ var deferred = m.deferred()
+ deferred.promise.then(null, function () { throw new TypeError() })
+ deferred.reject("test")
+ }).to.throw()
+ })
+
+ it("synchronously throws subclasses of Errors thrown from its `catch` method", function () { // eslint-disable-line
+ expect(function () {
+ var deferred = m.deferred()
+ deferred.promise.catch(function () { throw new TypeError() })
+ deferred.reject("test")
+ }).to.throw()
+ })
+
+ it("unwraps other thenables, and returns the correct values in the chain", function () { // eslint-disable-line
+ 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)
+ expect(value1).to.equal(1)
+ expect(value2).to.equal(2)
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/80
+ it("propogates returns with `then` after being resolved", function () {
+ var deferred = m.deferred()
+ var value = m.prop()
+ deferred.resolve(1)
+ deferred.promise.then(value)
+ expect(value()).to.equal(1)
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/80
+ it("propogates errors with `then` after being rejected", function () {
+ var deferred = m.deferred()
+ var value = m.prop()
+ deferred.reject(1)
+ deferred.promise.then(null, value)
+ expect(value()).to.equal(1)
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/80
+ it("can only be resolved once before being chained", function () {
+ var deferred = m.deferred()
+ var value = m.prop()
+ deferred.resolve(1)
+ deferred.resolve(2)
+ deferred.promise.then(value)
+ expect(value()).to.equal(1)
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/80
+ it("can only be resolved once after being chained", function () {
+ var deferred = m.deferred()
+ var value = m.prop()
+ deferred.promise.then(value)
+ deferred.resolve(1)
+ deferred.resolve(2)
+ expect(value()).to.equal(1)
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/80
+ it("can't be rejected after being resolved", function () {
+ var deferred = m.deferred()
+ var value1 = m.prop()
+ var value2 = m.prop()
+ deferred.promise.then(value1, value2)
+ deferred.resolve(1)
+ deferred.reject(2)
+ expect(value1()).to.equal(1)
+ expect(value2()).to.not.exist
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/80
+ it("can't be resolved after being rejected", function () {
+ var deferred = m.deferred()
+ var value1 = m.prop()
+ var value2 = m.prop()
+ deferred.promise.then(value1, value2)
+ deferred.reject(1)
+ deferred.resolve(2)
+ expect(value1()).to.not.exist
+ expect(value2()).to.equal(1)
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/80
+ it("can only be rejected once before being chained", function () {
+ var deferred = m.deferred()
+ var value = m.prop()
+ deferred.reject(1)
+ deferred.reject(2)
+ deferred.promise.then(null, value)
+ expect(value()).to.equal(1)
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/80
+ it("can only be rejected once after being chained", function () {
+ var deferred = m.deferred()
+ var value = m.prop()
+ deferred.promise.then(null, value)
+ deferred.reject(1)
+ deferred.reject(2)
+ expect(value()).to.equal(1)
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/85
+ it("calls resolution handler when resolved with `undefined`", function () {
+ var deferred = m.deferred()
+ var value
+ deferred.resolve()
+ deferred.promise.then(function () {
+ value = 1
+ })
+ expect(value).to.equal(1)
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/85
+ it("calls rejection handler when rejected with `undefined`", function () {
+ var deferred = m.deferred()
+ var value
+ deferred.reject()
+ deferred.promise.then(null, function () {
+ value = 1
+ })
+ expect(value).to.equal(1)
+ })
+
+ it("immediately resolves promise with `resolve` method", function () {
+ var deferred = m.deferred()
+ deferred.resolve(1)
+ expect(deferred.promise()).to.equal(1)
+ })
+
+ it("gets chained promise value when called", function () {
+ var deferred = m.deferred()
+ var promise = deferred.promise.then(function (data) { return data + 1 })
+ deferred.resolve(1)
+ expect(promise()).to.equal(2)
+ })
+
+ it("returns `undefined` from call if it's rejected", function () {
+ var deferred = m.deferred()
+ deferred.reject(1)
+ expect(deferred.promise()).to.be.undefined
+ })
+})
diff --git a/test/mithril.js b/test/mithril.js
new file mode 100644
index 00000000..bf86f972
--- /dev/null
+++ b/test/mithril.js
@@ -0,0 +1,216 @@
+describe("m.version()", function () {
+ "use strict"
+
+ it("exists", function () {
+ expect(m.version).to.be.a("function")
+ })
+
+ it("is a string", function () {
+ expect(m.version()).to.be.a("string")
+ })
+})
+
+describe("m()", function () {
+ "use strict"
+
+ it("exists", function () {
+ expect(m).to.be.a("function")
+ })
+
+ it("sets correct tag name", function () {
+ expect(m("div")).to.have.property("tag", "div")
+ })
+
+ it("sets correct tag name with only a class", function () {
+ expect(m(".foo")).to.have.property("tag", "div")
+ })
+
+ it("sets correct class name", function () {
+ expect(m(".foo")).to.have.deep.property("attrs.className", "foo")
+ })
+
+ it("sets correct tag name with only an attr", function () {
+ expect(m("[title=bar]")).to.have.property("tag", "div")
+ })
+
+ it("sets correct unquoted attr", function () {
+ expect(m("[title=bar]")).to.have.deep.property("attrs.title", "bar")
+ })
+
+ it("sets correct single quoted attr", function () {
+ expect(m("[title=\'bar\']")).to.have.deep.property("attrs.title", "bar")
+ })
+
+ it("sets correct double quoted attr", function () {
+ expect(m("[title=\"bar\"]")).to.have.deep.property("attrs.title", "bar")
+ })
+
+ it("sets correct children with 1 string arg", function () {
+ expect(m("div", "test"))
+ .to.have.property("children")
+ .that.eqls(["test"])
+ })
+
+ it("sets correct children with multiple string args", function () {
+ expect(m("div", "test", "test2"))
+ .to.have.property("children")
+ .that.eqls(["test", "test2"])
+ })
+
+ it("sets correct children with string array", function () {
+ expect(m("div", ["test"]))
+ .to.have.property("children")
+ .that.eqls(["test"])
+ })
+
+ it("sets correct attrs with object", function () {
+ expect(m("div", {title: "bar"}, "test"))
+ .to.have.deep.property("attrs.title", "bar")
+ })
+
+ it("sets correct children with attrs object", function () {
+ expect(m("div", {title: "bar"}, "test"))
+ .to.have.property("children")
+ .that.eqls(["test"])
+ })
+
+ it("sets correct children with nested node", function () {
+ expect(m("div", {title: "bar"}, m("div")))
+ .to.have.property("children")
+ .that.eqls([m("div")])
+ })
+
+ it("sets correct children with string rest arg", function () {
+ expect(m("div", {title: "bar"}, "test0", "test1", "test2", "test3"))
+ .to.have.property("children")
+ .that.eqls(["test0", "test1", "test2", "test3"])
+ })
+
+ it("sets correct children with node rest arg", function () {
+ expect(m("div", {title: "bar"}, m("div"), m("i"), m("span")))
+ .to.have.property("children")
+ .that.eqls([m("div"), m("i"), m("span")])
+ })
+
+ it("sets correct children with string array & no attrs", function () {
+ expect(m("div", ["a", "b"]))
+ .to.have.property("children")
+ .that.eqls(["a", "b"])
+ })
+
+ it("sets correct children with node array & no attrs", function () {
+ expect(m("div", [m("div"), m("i")]))
+ .to.have.property("children")
+ .that.eqls([m("div"), m("i")])
+ })
+
+ it("sets correct children with 2nd arg as node", function () {
+ expect(m("div", m("div"))).to.have.property("children")
+ .that.eqls([m("div")])
+ })
+
+ it("sets correct tag with undefined array entry", function () {
+ expect(m("div", [undefined])).to.have.property("tag", "div")
+ })
+
+ it("loosely accepts invalid objects", function () {
+ expect(function () { m("div", [{foo: "bar"}]) }).to.not.throw()
+ })
+
+ it("accepts svg nodes", function () {
+ expect(m("svg", [m("g")]))
+ .to.have.property("children")
+ .that.eqls([m("g")])
+ })
+
+ it("accepts HTML children in svg element", function () {
+ expect(m("svg", [m("a[href='http://google.com']")]))
+ .to.have.property("children")
+ .that.eqls([m("a[href='http://google.com']")])
+ })
+
+ it("uses className if given", function () {
+ expect(m(".foo", {className: ""}))
+ .to.have.deep.property("attrs.className", "foo")
+ })
+
+ it("accepts a class and class attr", function () {
+ var node = m(".foo", {class: "bar"})
+ expect(node).to.have.deep.property("attrs.class")
+ expect(node.attrs.class).to.include("foo").and.include("bar")
+ })
+
+ it("accepts a class and className attr", function () {
+ var node = m(".foo", {className: "bar"})
+ expect(node).to.have.deep.property("attrs.className")
+ expect(node.attrs.className).to.include("foo").and.include("bar")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/382 and 512
+ it("sets an empty className attr if it's an empty string", function () {
+ expect(m("div", {className: ""}))
+ .to.have.deep.property("attrs.className", "")
+ })
+
+ it("does not set className attr if class is given", function () {
+ expect(m("div", {class: ""})).to.not.have.property("attrs.className")
+ })
+
+ it("does not set class attr if className is given", function () {
+ expect(m("div", {className: ""})).to.not.have.property("attrs.class")
+ })
+
+ it("sets an empty class attr if it's an empty string", function () {
+ expect(m("div", {class: ""})).to.have.deep.property("attrs.class", "")
+ })
+
+ it("does not flatten 1 nested array", function () {
+ expect(m("div", [1, 2, 3], 4))
+ .to.have.property("children")
+ .that.eqls([[1, 2, 3], 4])
+ })
+
+ it("does not flatten 2 nested arrays", function () {
+ expect(m("div", [1, 2, 3], [4, 5, 6, 7]))
+ .to.have.property("children")
+ .that.eqls([[1, 2, 3], [4, 5, 6, 7]])
+ })
+
+ it("does not flatten 3 nested arrays", function () {
+ expect(m("div", [1], [2], [3]))
+ .to.have.property("children")
+ .that.eqls([[1], [2], [3]])
+ })
+
+ it("doesn't recreate the DOM when classes are different", function () {
+ var v1 = m(".foo", {class: "", onclick: function () {}})
+ var v2 = m(".foo", {class: "bar", onclick: function () {}})
+
+ expect(v1)
+ .to.have.property("attrs")
+ .that.contains.all.keys("class", "onclick")
+
+ expect(v2)
+ .to.have.property("attrs")
+ .that.contains.all.keys("class", "onclick")
+ })
+
+ it("proxies an object first arg to m.component()", function () {
+ var spy = sinon.spy()
+
+ var component = {
+ controller: spy,
+ view: function () {
+ return m("div", "testing")
+ }
+ }
+
+ var args = {age: 12}
+
+ m(component, args).controller()
+ expect(spy.firstCall).to.have.been.calledWith(args)
+
+ m.component(component, args).controller()
+ expect(spy.secondCall).to.have.been.calledWith(args)
+ })
+})
diff --git a/test/mithril.mount.js b/test/mithril.mount.js
new file mode 100644
index 00000000..9b2f268d
--- /dev/null
+++ b/test/mithril.mount.js
@@ -0,0 +1,802 @@
+describe("m.mount()", function () {
+ "use strict"
+
+ // This is a frequent idiom
+ function refresh(force) {
+ m.redraw(!!force)
+ mock.requestAnimationFrame.$resolve()
+ }
+
+ function clear(root) {
+ m.mount(root, null)
+ mock.requestAnimationFrame.$resolve()
+ }
+
+ function mount(root, mod) {
+ var res = m.mount(root, mod)
+ mock.requestAnimationFrame.$resolve()
+ return res
+ }
+
+ // This is extremely frequent in the tests
+ function pure(view) {
+ return {
+ controller: function () {},
+ view: view
+ }
+ }
+
+ it("exists", function () {
+ expect(m.mount).to.be.a("function")
+ })
+
+ it("mounts onto the root", function () {
+ var root = mock.document.createElement("div")
+ var whatever = 1
+
+ var app = pure(function () {
+ return [
+ whatever % 2 ? m("span", "% 2") : undefined,
+ m("div", "bugs"),
+ m("a")
+ ]
+ })
+
+ mount(root, app)
+
+ whatever++
+ refresh()
+
+ whatever++
+ refresh()
+
+ expect(root.childNodes).to.have.length.above(0)
+ })
+
+ it("reloads components correctly", function () {
+ mock.requestAnimationFrame.$resolve()
+
+ var root1 = mock.document.createElement("div")
+ var controller1 = sinon.spy(function () { this.value = "test1" }) // eslint-disable-line
+ var view1 = sinon.stub().returns("test1")
+
+ var mod1 = m.mount(root1, {
+ controller: controller1,
+ view: view1
+ })
+
+ var controller2 = sinon.spy(function () { this.value = "test2" }) // eslint-disable-line
+ var view2 = sinon.stub().returns("test2")
+ var root2 = mock.document.createElement("div")
+
+ var mod2 = mount(root2, {
+ controller: controller2,
+ view: view2
+ })
+
+ expect(controller1).to.have.been.called
+ expect(view1).to.have.been.called
+ expect(controller2).to.have.been.called
+ expect(view2).to.have.been.called
+
+ expect(root1.childNodes[0].nodeValue).to.equal("test1")
+ expect(root2.childNodes[0].nodeValue).to.equal("test2")
+ expect(mod1).to.have.property("value", "test1")
+ expect(mod2).to.have.property("value", "test2")
+ })
+
+ it("triggers an unload when the element is removed", function () {
+ mock.requestAnimationFrame.$resolve()
+
+ var root = mock.document.createElement("div")
+ var spy = sinon.spy()
+
+ mount(root, {
+ controller: function () {
+ this.onunload = spy
+ },
+ view: function () {}
+ })
+
+ clear(root)
+
+ expect(spy).to.have.been.called
+ })
+
+ it("passes the args to both component & view", function () {
+ mock.requestAnimationFrame.$resolve()
+
+ var root = mock.document.createElement("div")
+ var ctrlSpy = sinon.spy()
+ var viewSpy = sinon.stub().returns(m("div"))
+
+ var component = {
+ controller: ctrlSpy,
+ view: viewSpy
+ }
+
+ var arg = {}
+
+ mount(root, m.component(component, arg))
+
+ expect(ctrlSpy).to.have.been.calledWith(arg)
+ expect(viewSpy.firstCall.args[1]).to.equal(arg)
+ })
+
+ it("mounts a component without a controller", function () {
+ mock.requestAnimationFrame.$resolve()
+
+ var root = mock.document.createElement("div")
+ var arg = {}
+ var spy = sinon.spy()
+
+ var component = pure(spy)
+
+ mount(root, m.component(component, arg))
+
+ expect(spy.firstCall.args[1]).to.equal(arg)
+ })
+
+ it("only runs a component controller once", function () {
+ mock.requestAnimationFrame.$resolve()
+
+ var root = mock.document.createElement("div")
+ var ctrlSpy = sinon.spy()
+ var viewSpy = sinon.stub().returns(m("div"))
+
+ var sub = {
+ controller: ctrlSpy,
+ view: viewSpy
+ }
+
+ mount(root, pure(function () { return sub }))
+
+ refresh(true)
+
+ expect(ctrlSpy).to.have.been.calledOnce
+ expect(viewSpy).to.have.been.calledTwice
+ })
+
+ it("only runs a subcomponent controller once", function () {
+ mock.requestAnimationFrame.$resolve()
+
+ var root = mock.document.createElement("div")
+ var ctrl1 = sinon.spy()
+ var view1 = sinon.stub().returns(m("div"))
+
+ var subsub = {
+ controller: ctrl1,
+ view: view1
+ }
+
+ var ctrl2 = sinon.spy()
+ var view2 = sinon.stub().returns(subsub)
+
+ var sub = {
+ controller: ctrl2,
+ view: view2
+ }
+
+ mount(root, pure(function () { return sub }))
+
+ refresh(true)
+
+ expect(ctrl1).to.have.been.calledOnce
+ expect(ctrl2).to.have.been.calledOnce
+ expect(view1).to.have.been.calledTwice
+ expect(view2).to.have.been.calledTwice
+ })
+
+ it("addresses keys in components", function () {
+ mock.requestAnimationFrame.$resolve()
+
+ var root = mock.document.createElement("div")
+ var list = [1, 2, 3]
+
+ var sub = pure(function () { return m("div") })
+
+ m.mount(root, pure(function () {
+ return list.map(function (i) {
+ return m.component(sub, {key: i})
+ })
+ }))
+
+ var firstBefore = root.childNodes[0]
+
+ mock.requestAnimationFrame.$resolve()
+
+ list.reverse()
+ refresh(true)
+
+ expect(root.childNodes[2]).to.equal(firstBefore)
+ })
+
+ it("addresses keys in subcomponents correctly", function () {
+ mock.requestAnimationFrame.$resolve()
+
+ var root = mock.document.createElement("div")
+ var list = [1, 2, 3]
+
+ var subsub = pure(function () { return m("div") })
+
+ var sub = pure(function () { return subsub })
+
+ m.mount(root, pure(function () {
+ return list.map(function (i) {
+ return m.component(sub, {key: i})
+ })
+ }))
+
+ var firstBefore = root.childNodes[0]
+
+ mock.requestAnimationFrame.$resolve()
+
+ list.reverse()
+ refresh(true)
+
+ expect(root.childNodes[2]).to.equal(firstBefore)
+ })
+
+ it("is error resistant with keys in components", function () {
+ mock.requestAnimationFrame.$resolve()
+
+ var root = mock.document.createElement("div")
+ var list = [1, 2, 3]
+
+ var sub = pure(function () { return m("div", {key: 1}) })
+
+ m.mount(root, pure(function () {
+ return list.map(function (i) {
+ return m.component(sub, {key: i})
+ })
+ }))
+
+ var firstBefore = root.childNodes[0]
+
+ mock.requestAnimationFrame.$resolve()
+
+ list.reverse()
+ refresh(true)
+
+ expect(root.childNodes[2]).to.equal(firstBefore)
+ })
+
+ it("is error resistant with keys in subcomponents", function () {
+ mock.requestAnimationFrame.$resolve()
+
+ var root = mock.document.createElement("div")
+ var list = [1, 2, 3]
+
+ var subsub = pure(function () { return m("div", {key: 1}) })
+
+ var sub = pure(function () { return subsub })
+
+ m.mount(root, pure(function () {
+ return list.map(function (i) {
+ return m.component(sub, {key: i})
+ })
+ }))
+
+ var firstBefore = root.childNodes[0]
+
+ mock.requestAnimationFrame.$resolve()
+
+ list.reverse()
+ refresh(true)
+
+ expect(root.childNodes[2]).to.equal(firstBefore)
+ })
+
+ it("retains subcomponent identity if child of keyed element", function () {
+ mock.requestAnimationFrame.$resolve()
+
+ var root = mock.document.createElement("div")
+ var list = [1, 2, 3]
+
+ var sub = pure(function () { return m("div") })
+
+ m.mount(root, pure(function () {
+ return list.map(function (i) {
+ return m("div", {key: i}, sub)
+ })
+ }))
+
+ var firstBefore = root.childNodes[0].childNodes[0]
+
+ mock.requestAnimationFrame.$resolve()
+
+ list.reverse()
+ refresh(true)
+
+ expect(root.childNodes[2].childNodes[0]).to.equal(firstBefore)
+ })
+
+ it("calls component onunload when removed from template", function () {
+ mock.requestAnimationFrame.$resolve()
+
+ var root = mock.document.createElement("div")
+ var list = [1, 2, 3]
+ var spies = []
+
+ var sub = {
+ controller: function (opts) {
+ this.onunload = spies[opts.key] = sinon.spy()
+ },
+ view: function () {
+ return m("div")
+ }
+ }
+
+ mount(root, pure(function () {
+ return list.map(function (i) {
+ return m.component(sub, {key: i})
+ })
+ }))
+
+ list.pop()
+ refresh(true)
+
+ // TODO: These fail.
+ // expect(spies[1]).to.have.been.called
+ // expect(spies[2]).to.have.been.called
+ expect(spies[3]).to.have.been.called
+ })
+
+ it("calls subcomponent onunload when removed from template", function () {
+ mock.requestAnimationFrame.$resolve()
+
+ var root = mock.document.createElement("div")
+ var list = [1, 2, 3]
+ var spies1 = []
+ var spies2 = []
+
+ var subsub = {
+ controller: function (opts) {
+ this.onunload = spies1[opts.key] = sinon.spy()
+ },
+ view: function () {
+ return m("div")
+ }
+ }
+
+ var sub = {
+ controller: function (opts) {
+ this.onunload = spies2[opts.key] = sinon.spy()
+ },
+ view: function (ctrl, opts) {
+ return m.component(subsub, {key: opts.key})
+ }
+ }
+
+ mount(root, pure(function () {
+ return list.map(function (i) {
+ return m.component(sub, {key: i})
+ })
+ }))
+
+ list.pop()
+ refresh(true)
+
+ // TODO: These fail.
+ // expect(spies1[1]).to.have.been.called
+ // expect(spies1[2]).to.have.been.called
+ expect(spies1[3]).to.have.been.called
+
+ // TODO: These fail.
+ // expect(spies2[1]).to.have.been.called
+ // expect(spies2[2]).to.have.been.called
+ expect(spies2[3]).to.have.been.called
+ })
+
+ it("doesn't redraw if m.render() is called by controller constructor", function () { // eslint-disable-line
+ mock.requestAnimationFrame.$resolve()
+
+ var root = mock.document.createElement("div")
+ var spy = sinon.stub().returns(m("div"))
+
+ var sub = {
+ controller: function () {
+ m.redraw()
+ },
+ view: spy
+ }
+
+ mount(root, pure(function () { return sub }))
+
+ expect(spy).to.have.been.called
+ })
+
+ it("doesn't redraw if m.render() is called by subcomponent controller constructor", function () { // eslint-disable-line
+ mock.requestAnimationFrame.$resolve()
+
+ var root = mock.document.createElement("div")
+ var spy = sinon.stub().returns(m("div"))
+
+ var subsub = {
+ controller: function () {
+ m.redraw()
+ },
+ view: spy
+ }
+
+ var sub = pure(function () { return subsub })
+
+ mount(root, pure(function () { return sub }))
+
+ expect(spy).to.have.been.called
+ })
+
+ it("renders nested components under keyed components", function () {
+ mock.requestAnimationFrame.$resolve()
+
+ var root = mock.document.createElement("div")
+ var spy = sinon.stub().returns(m(".reply"))
+
+ var Reply = pure(spy)
+
+ var CommentList = pure(function (ctrl, props) {
+ return m(".list", props.list.map(function (i) {
+ return m(".comment", [
+ m.component(Reply, {key: i})
+ ])
+ }))
+ })
+
+ mount(root, pure(function () {
+ return m(".outer", [
+ m(".inner", m.component(CommentList, {list: [1, 2, 3]}))
+ ])
+ }))
+
+ expect(spy).to.have.been.calledThrice
+ })
+
+ it("calls unload when the component is replaced with another component", function () { // eslint-disable-line
+ var root = mock.document.createElement("div")
+ var spy = sinon.spy()
+
+ m.mount(root, {
+ controller: function () {
+ this.onunload = spy
+ },
+ view: function () {}
+ })
+
+ m.mount(root, pure(function () {}))
+
+ expect(spy).to.have.been.called
+ })
+
+
+ it("calls config with truthy init only once", function () {
+ mock.requestAnimationFrame.$resolve()
+
+ var root = mock.document.createElement("div")
+ var count = 0
+
+ mount(root, pure(function () {
+ return m("div", {
+ config: function (el, init) {
+ if (init) count += 1
+ }
+ })
+ }))
+
+ refresh()
+
+ expect(count).to.equal(1)
+ })
+
+ it("doesn't recreate node that modifies DOM in config, but stays same between redraws", function () { // eslint-disable-line
+ var root = mock.document.createElement("div")
+ var child = mock.document.createElement("div")
+
+ var show = true
+
+ function test(el, init) {
+ if (!init) {
+ root.appendChild(child)
+ }
+ }
+
+ mount(root, pure(function () {
+ return [
+ m(".foo", {
+ key: 1,
+ config: test,
+ onclick: function () { show = !show }
+ }),
+ show ? m(".bar", {key: 2}) : null
+ ]
+ }))
+
+ show = false
+ refresh()
+
+ show = true
+ refresh()
+
+ expect(root.childNodes).to.have.length(3)
+ })
+
+ it("correctly replaces nodes", function () {
+ var root = mock.document.createElement("div")
+ var show = true
+
+ var sub = pure(function () { return m("div", "component") })
+
+ mount(root, pure(function () {
+ return show ? [
+ m("h1", "1"),
+ sub
+ ] : [
+ m("h1", "2")
+ ]
+ }))
+
+ show = false
+ refresh()
+
+ show = true
+ refresh()
+
+ expect(root.childNodes).to.have.length(2)
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/551
+ it("only redraws a component when clicked", function () {
+ var root = mock.document.createElement("div")
+ var a = false
+ var found = {}
+
+ var onunload = sinon.spy()
+ var view = sinon.spy(function () {
+ return m("div", {config: Comp.config}, [ // eslint-disable-line
+ m("div", {
+ onclick: function () {
+ a = !a
+ m.redraw(true)
+ found = root.childNodes[0].childNodes[1]
+ }
+ }, "asd"),
+ a ? m("#a", "aaa") : null,
+ "test"
+ ])
+ })
+
+ var Comp = {
+ view: view,
+
+ config: function (el, init, ctx) {
+ if (!init) ctx.onunload = onunload
+ }
+ }
+
+ m.mount(root, pure(function () { return Comp }))
+
+ var target = root.childNodes[0].childNodes[0]
+
+ target.onclick({currentTarget: target})
+ mock.requestAnimationFrame.$resolve()
+
+ expect(onunload).to.not.be.called
+ expect(found).to.have.property("id", "a")
+ expect(view).to.have.been.calledThrice
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/551
+ it("only redraws a component when clicked if the strategy is `none`", function () { // eslint-disable-line
+ var root = mock.document.createElement("div")
+ var a = false
+ var found = {}
+
+ var onunload = sinon.spy()
+ var view = sinon.spy(function () {
+ return m("div", {config: Comp.config}, [ // eslint-disable-line
+ m("div", {
+ onclick: function () {
+ a = !a
+ m.redraw(true)
+ found = root.childNodes[0].childNodes[1]
+ m.redraw.strategy("none")
+ }
+ }, "asd"),
+ a ? m("#a", "aaa") : null,
+ "test"
+ ])
+ })
+
+ var Comp = {
+ view: view,
+
+ config: function (el, init, ctx) {
+ if (!init) ctx.onunload = onunload
+ }
+ }
+
+ m.mount(root, pure(function () { return Comp }))
+
+ var target = root.childNodes[0].childNodes[0]
+
+ target.onclick({currentTarget: target})
+ mock.requestAnimationFrame.$resolve()
+
+ expect(onunload).to.not.be.called
+ expect(found).to.have.property("id", "a")
+ expect(view).to.have.been.calledTwice
+ })
+
+ it("redraws when clicked and click handler forces redraw", function () {
+ var root = mock.document.createElement("div")
+ var view = sinon.stub().returns(m("div", {
+ onclick: function () { m.redraw(true) }
+ }))
+
+ m.mount(root, pure(view))
+
+ var target = root.childNodes[0]
+
+ target.onclick({currentTarget: target})
+ mock.requestAnimationFrame.$resolve()
+
+ expect(view).to.be.calledThrice
+ })
+
+ function resolveXhr() {
+ mock.XMLHttpRequest.$instances.pop().onreadystatechange()
+ mock.requestAnimationFrame.$resolve()
+ }
+
+ it("doesn't redraw on a single synchronous request", function () {
+ var root = mock.document.createElement("div")
+
+ var data
+ var view = sinon.spy(function (ctrl) {
+ data = ctrl.foo()
+ return m("div")
+ })
+
+ var Comp = {
+ controller: function () {
+ this.foo = m.request({method: "GET", url: "/foo"})
+ },
+
+ view: view
+ }
+
+ mount(root, pure(function () { return Comp }))
+
+ resolveXhr()
+
+ clear(root)
+
+ expect(view).to.be.calledOnce
+ expect(data).to.have.property("url", "/foo")
+ })
+
+ it("doesn't redraw on multiple synchronous requests", function () {
+ mock.requestAnimationFrame.$resolve()
+ mock.location.search = "?"
+
+ var root = mock.document.createElement("div")
+ var view1 = sinon.stub().returns(m("div"))
+ var view2 = sinon.stub().returns(m("div"))
+
+ var Comp1 = {
+ controller: function () {
+ this.foo = m.request({method: "GET", url: "/foo"})
+ },
+ view: view1
+ }
+
+ var Comp2 = {
+ controller: function () {
+ this.bar = m.request({method: "GET", url: "/bar"})
+ },
+ view: view2
+ }
+
+ mount(root, pure(function () {
+ return m("div", [
+ Comp1,
+ Comp2
+ ])
+ }))
+
+ resolveXhr()
+ resolveXhr()
+
+ clear(root)
+
+ expect(view1).to.be.calledOnce
+ expect(view2).to.be.calledOnce
+ })
+
+ it("instantiates different controllers for components without controller constructors", function () { // eslint-disable-line
+ var root = mock.document.createElement("div")
+
+ var cond = true
+ var controller1, controller2
+
+ var Comp1 = pure(function (ctrl) {
+ controller1 = ctrl
+ return m("div")
+ })
+
+ var Comp2 = pure(function (ctrl) {
+ controller2 = ctrl
+ return m("div")
+ })
+
+ mount(root, pure(function () { return cond ? Comp1 : Comp2 }))
+
+ cond = false
+ refresh(true)
+
+ expect(controller1).to.not.equal(controller2)
+ })
+
+ it("unloads removed components", function () {
+ var root = mock.document.createElement("div")
+
+ var onunload = sinon.spy()
+ var cond = true
+
+ var Comp1 = pure(function () {
+ return m("div", {
+ config: function (el, init, ctx) {
+ ctx.onunload = onunload
+ }
+ })
+ })
+
+ var Comp2 = pure(function () { return m("div") })
+
+ mount(root, pure(function () { return cond ? Comp1 : Comp2 }))
+
+ cond = false
+ refresh(true)
+
+ expect(onunload).to.be.called
+ })
+
+ it("calls config with its second argument false first", function () {
+ var root = mock.document.createElement("div")
+
+ var cond = true
+ var config = sinon.spy()
+
+ var Comp1 = pure(function () { return m("div") })
+ var Comp2 = pure(function () { return m("div", {config: config}) })
+
+ mount(root, pure(function () { return cond ? Comp1 : Comp2 }))
+
+ cond = false
+ refresh(true)
+
+ expect(config.firstCall.args[1]).to.be.false
+ })
+
+ it("refreshes the component when it's redrawn in a handler", function () {
+ var root = mock.document.createElement("div")
+ var sub = pure(function () { return m("#bar", "test") })
+ var el
+
+ m.mount(root, pure(function (ctrl) {
+ return m("div", [
+ m("button", {
+ onclick: function () {
+ ctrl.bar = true
+ m.redraw(true)
+ el = root.childNodes[0].childNodes[1]
+ }
+ }, "click me"),
+ ctrl.bar ? m.component(sub) : ""
+ ])
+ }))
+
+ root.childNodes[0].childNodes[0].onclick({})
+
+ expect(el).to.have.property("id", "bar")
+ })
+})
diff --git a/test/mithril.prop.js b/test/mithril.prop.js
new file mode 100644
index 00000000..4be4b1b9
--- /dev/null
+++ b/test/mithril.prop.js
@@ -0,0 +1,64 @@
+describe("m.prop()", function () {
+ "use strict"
+
+ it("reads correct value", function () {
+ var prop = m.prop("test")
+ expect(prop()).to.equal("test")
+ })
+
+ it("defaults to `undefined`", function () {
+ var prop = m.prop()
+ expect(prop()).to.be.undefined
+ })
+
+ it("sets the correct value", function () {
+ var prop = m.prop("test")
+ prop("foo")
+ expect(prop()).to.equal("foo")
+ })
+
+ it("sets `null`", function () {
+ var prop = m.prop(null)
+ expect(prop()).to.be.null
+ })
+
+ it("sets `undefined`", function () {
+ var prop = m.prop(undefined)
+ expect(prop()).to.be.undefined
+ })
+
+ it("returns the new value when set", function () {
+ var prop = m.prop()
+ expect(prop("foo")).to.equal("foo")
+ })
+
+ it("correctly stringifies to the correct value", function () {
+ var prop = m.prop("test")
+ expect(JSON.stringify(prop)).to.equal('"test"')
+ })
+
+ it("correctly stringifies to the correct value as a child", function () {
+ var obj = {prop: m.prop("test")}
+ expect(JSON.stringify(obj)).to.equal('{"prop":"test"}')
+ })
+
+ it("correctly wraps Mithril promises", function () {
+ var defer = m.deferred()
+ var prop = m.prop(defer.promise)
+ defer.resolve("test")
+
+ expect(prop()).to.equal("test")
+ })
+
+ it("returns a thenable when wrapping a Mithril promise", function () {
+ var defer = m.deferred()
+
+ var prop = m.prop(defer.promise).then(function () {
+ return "test2"
+ })
+
+ defer.resolve("test")
+
+ expect(prop()).to.equal("test2")
+ })
+})
diff --git a/test/mithril.redraw.js b/test/mithril.redraw.js
new file mode 100644
index 00000000..22e62cc0
--- /dev/null
+++ b/test/mithril.redraw.js
@@ -0,0 +1,220 @@
+describe("m.redraw()", function () {
+ "use strict"
+
+ beforeEach(function () {
+ mock.requestAnimationFrame.$resolve()
+ })
+
+ it("exists", function () {
+ expect(m.redraw).to.be.a("function")
+ })
+
+ it("correctly renders a property if the controller value changes", function () { // eslint-disable-line
+ var ctx
+ var root = mock.document.createElement("div")
+
+ m.mount(root, {
+ controller: function () { ctx = this }, // eslint-disable-line
+ view: function (ctrl) { return ctrl.value }
+ })
+
+ mock.requestAnimationFrame.$resolve()
+
+ var valueBefore = root.childNodes[0].nodeValue
+ ctx.value = "foo"
+
+ m.redraw()
+ mock.requestAnimationFrame.$resolve()
+
+ expect(valueBefore).to.equal("")
+ expect(root.childNodes[0].nodeValue).to.equal("foo")
+ })
+
+ it("runs unnecessary redraws asynchronously", function () {
+ var root = mock.document.createElement("div")
+ var view = sinon.spy()
+
+ m.mount(root, {
+ controller: function () {},
+ view: view
+ })
+ mock.requestAnimationFrame.$resolve() // teardown
+ m.redraw()
+
+ // These should run asynchronously
+ m.redraw()
+ m.redraw()
+ m.redraw()
+ mock.requestAnimationFrame.$resolve() // teardown
+
+ expect(view).to.be.calledThrice
+ })
+
+ it("runs unnecessary forced redraws asynchronously", function () {
+ var root = mock.document.createElement("div")
+ var view = sinon.spy()
+ m.mount(root, {
+ controller: function () {},
+ view: view
+ })
+ mock.requestAnimationFrame.$resolve() // teardown
+ m.redraw(true)
+
+ // These should run asynchronously
+ m.redraw(true)
+ m.redraw(true)
+ m.redraw(true)
+ mock.requestAnimationFrame.$resolve() // teardown
+
+ expect(view).to.have.callCount(5)
+ })
+
+ context("m.redraw.strategy()", function () {
+ // Use this instead of m.route() unless you have to call m.route and do
+ // something else in the same frame.
+ function route() {
+ var res = m.route.apply(null, arguments)
+ mock.requestAnimationFrame.$resolve()
+ return res
+ }
+
+ // Little helper utility
+ function noop() {}
+
+ // Use this if all you need to do is render a view (i.e. a pure
+ // component).
+ function pure(view) {
+ return {
+ controller: noop,
+ view: view
+ }
+ }
+
+ // Use these instead of `it` and `xit` in this set of tests if you need
+ // a root element.
+ var dit = makeIt(it)
+
+ // Wraps the `it` function for dependency injection that doesn't require
+ // `this`
+ /* eslint-disable no-invalid-this */
+ function makeIt(it) {
+ return function (name, callback) {
+ return it(name, function () {
+ var args = [this.root]
+ for (var i = 0; i < arguments.length; i++) {
+ args.push(arguments[i])
+ }
+ callback.apply(null, args)
+ })
+ }
+ }
+
+ beforeEach(function () {
+ mock.requestAnimationFrame.$resolve()
+ mock.location.search = "?"
+ m.route.mode = "search"
+ this.root = mock.document.createElement("div")
+ })
+
+ afterEach(function () {
+ m.mount(this.root, null)
+ })
+ /* eslint-enable no-invalid-this */
+
+ it("exists", function () {
+ expect(m.redraw.strategy).to.be.a("function")
+ })
+
+ dit("works with \"all\"", function (root) {
+ var strategy
+
+ route(root, "/foo1", {
+ "/foo1": {
+ controller: function () {
+ strategy = m.redraw.strategy()
+ m.redraw.strategy("none")
+ },
+ view: function () {
+ return m("div")
+ }
+ }
+ })
+
+ expect(strategy).to.equal("all")
+ expect(root.childNodes).to.be.empty
+ })
+
+ dit("works with \"redraw\"", function (root) {
+ var count = 0
+ var strategy
+ function config(el, init) {
+ if (!init) count++
+ }
+
+ route(root, "/foo1", {
+ "/foo1": pure(function () {
+ return m("div", {config: config})
+ }),
+ "/bar1": {
+ controller: function () {
+ strategy = m.redraw.strategy()
+ m.redraw.strategy("redraw")
+ },
+ view: function () {
+ return m("div", {config: config})
+ }
+ }
+ })
+
+ route("/bar1")
+
+ expect(strategy).to.equal("all")
+ expect(count).to.equal(1)
+ })
+
+ dit("works with \"diff\"", function (root) {
+ var strategy
+ 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({})
+ mock.requestAnimationFrame.$resolve()
+
+ expect(strategy).to.equal("diff")
+ expect(root.childNodes[0].childNodes[0].nodeValue).to.equal("1")
+ })
+
+ dit("recreates the component when \"all\"", function (root) {
+ var count = 0
+ function config(el, init) {
+ if (!init) count++
+ }
+
+ m.route(root, "/foo1", {
+ "/foo1": pure(function () {
+ return m("div", {
+ config: config,
+ onclick: function () {
+ m.redraw.strategy("all")
+ }
+ })
+ })
+ })
+ root.childNodes[0].onclick({})
+ mock.requestAnimationFrame.$resolve()
+
+ expect(count).to.equal(2)
+ })
+ })
+})
diff --git a/test/mithril.render.js b/test/mithril.render.js
new file mode 100644
index 00000000..34faa178
--- /dev/null
+++ b/test/mithril.render.js
@@ -0,0 +1,1554 @@
+describe("m.render()", function () {
+ "use strict"
+
+ it("exists", function () {
+ expect(m.render).to.be.a("function")
+ })
+
+ it("renders a string", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, "test")
+ expect(root.childNodes[0].nodeValue).to.equal("test")
+ })
+
+ it("does not replace nodes differing in only class attr", 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"}))
+ expect(root.childNodes[0]).to.equal(elementBefore)
+ })
+
+ it("does not replace nodes differing in only class syntax", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m(".a"))
+ var elementBefore = root.childNodes[0]
+ m.render(root, m(".b"))
+ expect(root.childNodes[0]).to.equal(elementBefore)
+ })
+
+ it("replaces nodes differing in id attr", 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"}))
+ expect(root.childNodes[0]).to.not.equal(elementBefore)
+ })
+
+ it("replaces nodes differing in id syntax", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("#a"))
+ var elementBefore = root.childNodes[0]
+ m.render(root, m("[title=b]"))
+ expect(root.childNodes[0]).to.not.equal(elementBefore)
+ })
+
+ it("replaces id node with string node", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("#a"))
+ var elementBefore = root.childNodes[0]
+ m.render(root, "test")
+ expect(root.childNodes[0]).to.not.equal(elementBefore)
+ })
+
+ it("renders `undefined` body to empty string", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("div", [undefined]))
+ expect(root.childNodes[0].childNodes[0].nodeValue).to.equal("")
+ })
+
+ it("uses the W3C URI as default namespace for SVG children", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("svg", [m("g")]))
+ expect(root.childNodes[0].childNodes[0]).to.contain.all.keys({
+ nodeName: "G",
+ namespaceURI: "http://www.w3.org/2000/svg"
+ })
+ })
+
+ it("renders HTML elements contained in SVG elements", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("svg", [m("a[href='http://google.com']")]))
+ expect(root.childNodes[0].childNodes[0].nodeName).to.equal("A")
+ })
+
+ it("does not append rerendered items", 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"})]))
+ expect(root.childNodes[0].childNodes).to.have.length(1)
+ })
+
+ it("renders an added `undefined` to an empty string", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("ul", [m("li")]))
+ m.render(root, m("ul", [m("li"), undefined]))
+ expect(root.childNodes[0].childNodes[1].nodeValue).to.equal("")
+ })
+
+ it("renders a node replaced with `undefined` to an empty string", function () { // eslint-disable-line
+ var root = mock.document.createElement("div")
+ m.render(root, m("ul", [m("li"), m("li")]))
+ m.render(root, m("ul", [m("li"), undefined]))
+ expect(root.childNodes[0].childNodes[1].nodeValue).to.equal("")
+ })
+
+ it("renders a replaced first `undefined` to an empty string", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("ul", [m("li")]))
+ m.render(root, m("ul", [undefined]))
+ expect(root.childNodes[0].childNodes[0].nodeValue).to.equal("")
+ })
+
+ it("does not render something replaced with empty object", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("ul", [m("li")]))
+ m.render(root, m("ul", [{}]))
+ expect(root.childNodes[0].childNodes).to.be.empty
+ })
+
+ it("renders an incomplete tag with primitive tag type", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("ul", [m("li")]))
+ m.render(root, m("ul", [{tag: "b", attrs: {}}]))
+ expect(root.childNodes[0].childNodes[0].nodeName).to.equal("B")
+ })
+
+ it("renders an incomplete tag with String object tag type", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("ul", [m("li")]))
+ /* eslint-disable no-new-wrappers */
+ m.render(root, m("ul", [{tag: new String("b"), attrs: {}}]))
+ /* eslint-enable no-new-wrappers */
+ expect(root.childNodes[0].childNodes[0].nodeName).to.equal("B")
+ })
+
+ it("renders the last tag when `subtree: \"retain\"`", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("ul", [m("li", [m("a")])]))
+ m.render(root, m("ul", [{subtree: "retain"}]))
+ expect(root.childNodes[0].childNodes[0].childNodes[0].nodeName)
+ .to.equal("A")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/43
+ it("rerenders anchors correctly with mode abstraction (`config: m.route`)", function () { // eslint-disable-line
+ var root = mock.document.createElement("div")
+ m.render(root, m("a", {config: m.route}, "test"))
+ m.render(root, m("a", {config: m.route}, "test"))
+ expect(root.childNodes[0].childNodes[0].nodeValue).to.equal("test")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/45
+ it("replaces initial null with string", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("#foo", [null, m("#bar")]))
+ m.render(root, m("#foo", ["test", m("#bar")]))
+ expect(root.childNodes[0].childNodes[0].nodeValue).to.equal("test")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/45
+ it("replaces initial null with node", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("#foo", [null, m("#bar")]))
+ m.render(root, m("#foo", [m("div"), m("#bar")]))
+ expect(root.childNodes[0].childNodes[0].nodeName).to.equal("DIV")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/45
+ it("replaces initial string with node", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("#foo", ["test", m("#bar")]))
+ m.render(root, m("#foo", [m("div"), m("#bar")]))
+ expect(root.childNodes[0].childNodes[0].nodeName).to.equal("DIV")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/45
+ it("replaces initial node with string", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("#foo", [m("div"), m("#bar")]))
+ m.render(root, m("#foo", ["test", m("#bar")]))
+ expect(root.childNodes[0].childNodes[0].nodeValue).to.equal("test")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/45
+ it("adds new duplicate node", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("#foo", [m("#bar")]))
+ m.render(root, m("#foo", [m("#bar"), [m("#baz")]]))
+ expect(root.childNodes[0].childNodes[1].id).to.equal("baz")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/48
+ it("renders from html when base is document", function () {
+ var root = mock.document
+ m.render(root, m("html", [m("#foo")]))
+ var result = root.childNodes[0].childNodes[0].id
+ // Have to clean up before assertion, or this will break other tests
+ root.childNodes = [mock.document.createElement("html")]
+ expect(result).to.equal("foo")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/49
+ it("reattaches cached text nodes to original parent (1)", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("a", "test"))
+ m.render(root, m("a.foo", "test"))
+ expect(root.childNodes[0].childNodes[0].nodeValue).to.equal("test")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/49
+ it("reattaches cached text nodes to original parent (2)", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("a.foo", "test"))
+ m.render(root, m("a", "test"))
+ expect(root.childNodes[0].childNodes[0].nodeValue).to.equal("test")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/49
+ it("reattaches cached text nodes to original parent (3)", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("a.foo", "test"))
+ m.render(root, m("a", "test1"))
+ expect(root.childNodes[0].childNodes[0].nodeValue).to.equal("test1")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/49
+ it("reattaches cached text nodes to original parent (4)", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("a", "test"))
+ m.render(root, m("a", "test1"))
+ expect(root.childNodes[0].childNodes[0].nodeValue).to.equal("test1")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/50
+ it("renders nested arrays correctly (1)", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("#foo", [[m("div", "a"), m("div", "b")], m("#bar")]))
+
+ expect(root.childNodes[0].childNodes[1].childNodes[0].nodeValue)
+ .to.equal("b")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/50
+ it("renders nested arrays correctly (2)", function () {
+ 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")
+ ]))
+
+ expect(root.childNodes[0].childNodes[3].childNodes[0].nodeValue)
+ .to.equal("d")
+ expect(root.childNodes[0].childNodes[4].id).to.equal("bar")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/50
+ it("renders nested arrays correctly (3)", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("#foo", [[m("div", "a"), m("div", "b")], "test"]))
+ expect(root.childNodes[0].childNodes[1].childNodes[0].nodeValue)
+ .to.equal("b")
+ expect(root.childNodes[0].childNodes[2].nodeValue).to.equal("test")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/50
+ it("renders nested arrays correctly (4)", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("#foo", [["a", "b"], "test"]))
+ expect(root.childNodes[0].childNodes[1].nodeValue).to.equal("b")
+ expect(root.childNodes[0].childNodes[2].nodeValue).to.equal("test")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/156
+ it("renders nested arrays correctly (5)", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("div", [
+ ["a", "b", "c", "d"].map(function () {
+ return [m("div"), " "]
+ }),
+ m("span")
+ ]))
+ expect(root.childNodes[0].childNodes[8].nodeName).to.equal("SPAN")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/50
+ it("reconciles nested list differences correctly", function () {
+ 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")
+ ]))
+
+ expect(root.childNodes[0].childNodes[2].childNodes[0].nodeValue)
+ .to.equal("c")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/51
+ it("reconciles nested node differences correctly (1)", function () {
+ 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")])
+ ]))
+
+ expect(root.childNodes[0].childNodes[1].childNodes[0].nodeName)
+ .to.equal("SPAN")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/51
+ it("reconciles nested node differences correctly (2)", function () {
+ 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")])
+ ]))
+
+ expect(root.childNodes[0].childNodes[1].childNodes[0].nodeValue)
+ .to.equal("test")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/51
+ it("reconciles nested node differences correctly (3)", function () {
+ 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")])
+ ]))
+
+ expect(root.childNodes[0].childNodes[1].childNodes[0].nodeValue)
+ .to.equal("test")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/55
+ it("redraws when id attrs are different", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("#a"))
+ var elementBefore = root.childNodes[0]
+ m.render(root, m("#b"))
+ expect(root.childNodes[0]).to.not.equal(elementBefore)
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/56
+ it("doesn't duplicate with a preceding null element", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, [null, "foo"])
+ m.render(root, ["bar"])
+ expect(root.childNodes).to.have.length(1)
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/56
+ it("doesn't duplicate with a preceding element with same tag name", function () { // eslint-disable-line
+ var root = mock.document.createElement("div")
+ m.render(root, m("div", "foo"))
+ expect(root.childNodes).to.have.length(1)
+ })
+
+ it("removes single `undefined` child node in place", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("div", [m("button"), m("ul")]))
+ expect(root.childNodes[0].childNodes[0])
+ .to.have.property("nodeName", "BUTTON")
+ m.render(root, m("div", [undefined, m("ul")]))
+ expect(root.childNodes[0].childNodes[0])
+ .to.have.property("nodeValue", "")
+ })
+
+ it("removes multiple `undefined` nodes in place", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("div", [m("ul"), undefined]))
+
+ expect(root.childNodes[0].childNodes[0])
+ .to.have.property("nodeName", "UL")
+
+ expect(root.childNodes[0].childNodes[1])
+ .to.have.property("nodeValue", "")
+
+ m.render(root, m("div", [undefined, m("ul")]))
+
+ expect(root.childNodes[0].childNodes[0])
+ .to.have.property("nodeValue", "")
+
+ expect(root.childNodes[0].childNodes[1])
+ .to.have.property("nodeName", "UL")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/79
+ it("changes the style when specified in the node", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("div", {style: {background: "red"}}))
+ expect(root.childNodes[0].style).to.have.property("background", "red")
+ m.render(root, m("div", {style: {}}))
+ expect(root.childNodes[0].style).to.have.property("background", "")
+ })
+
+ it("reads styles from syntax", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("div[style='background:red']"))
+ expect(root.childNodes[0].style).to.equal("background:red")
+ })
+
+ it("removes styles when not passed", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("div", {style: {background: "red"}}))
+ expect(root.childNodes[0].style.background).to.equal("red")
+ m.render(root, m("div", {}))
+ expect(root.childNodes[0].style.background).to.not.exist
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/87
+ it("removes correct number of elements from nested lists (1)", function () {
+ 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")]))
+
+ expect(root.childNodes[0].childNodes).to.have.length(2)
+
+ expect(root.childNodes[0].childNodes[1])
+ .to.have.property("nodeName", "BUTTON")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/87
+ it("removes correct number of elements from nested lists (2)", function () {
+ 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")]))
+
+ expect(root.childNodes[0].childNodes).to.have.length(2)
+
+ expect(root.childNodes[0].childNodes[1])
+ .to.have.property("nodeName", "BUTTON")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/99
+ it("removes correct number of elements from nested lists (3)", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("div", [m("img"), m("h1")]))
+ m.render(root, m("div", [m("a")]))
+
+ expect(root.childNodes[0].childNodes).to.have.length(1)
+
+ expect(root.childNodes[0].childNodes[0])
+ .to.have.property("nodeName", "A")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/120
+ it("avoids duplication in nested arrays (1)", function () {
+ 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
+
+ expect(children).to.have.length(2)
+ expect(children[0]).to.have.property("nodeValue", "d")
+ expect(children[1]).to.have.property("nodeValue", "e")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/120
+ it("avoids duplication in nested arrays (2)", function () {
+ 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
+
+ expect(children).to.have.length(2)
+ expect(children[0]).to.have.property("nodeValue", "d")
+ expect(children[1]).to.have.property("nodeValue", "e")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/120
+ it("avoids duplication in nested arrays (3)", function () {
+ 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
+
+ expect(children).to.have.length(2)
+ expect(children[0]).to.have.property("nodeValue", "d")
+ expect(children[1]).to.have.property("nodeValue", "e")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/120
+ it("avoids duplication in nested arrays (4)", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("div", ["b"]))
+ m.render(root, m("div", [["e"]]))
+
+ var children = root.childNodes[0].childNodes
+
+ expect(children).to.have.length(1)
+ expect(children[0]).to.have.property("nodeValue", "e")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/120
+ it("avoids duplication in nested arrays (5)", function () {
+ 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
+
+ expect(children).to.have.length(2)
+ expect(children[0]).to.have.property("nodeValue", "d")
+ expect(children[1]).to.have.property("nodeValue", "e")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/120
+ it("avoids duplication in nested arrays (6)", function () {
+ 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
+
+ expect(children).to.have.length(2)
+ expect(children[0]).to.have.property("nodeValue", "d")
+ expect(children[1]).to.have.property("nodeValue", "e")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/120
+ it("avoids duplication in nested arrays (7)", function () {
+ 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
+
+ expect(children).to.have.length(3)
+ expect(children[0]).to.have.property("nodeValue", "d")
+ expect(children[1]).to.have.property("nodeValue", "e")
+ })
+
+ it("honors setting context properties in stateful config", function () {
+ var root = mock.document.createElement("div")
+ var config = sinon.spy()
+
+ m.render(root, m("div", {
+ config: function (el, init, ctx) { ctx.data = 1 }
+ }))
+
+ m.render(root, m("div", {config: config}))
+
+ expect(config.firstCall.args[2]).to.have.property("data", 1)
+ })
+
+ it("calls configs in order, first to last", function () {
+ var root = mock.document.createElement("div")
+ var config = sinon.spy()
+ var index = 0
+
+ var node = m("div", {
+ config: function (el, init, ctx) { ctx.data = index++ }
+ })
+
+ m.render(root, [node, node])
+
+ node = m("div", {config: config})
+ m.render(root, [node, node])
+
+ expect(config).to.have.been.called
+ config.args.forEach(function (args, i) {
+ expect(args[2]).to.have.property("data", i)
+ })
+ })
+
+ it("passes the correct node as the element", function () {
+ var root = mock.document.createElement("div")
+ var spy = sinon.spy()
+ m.render(root, m("div", m("a", {config: spy})))
+ expect(spy).to.have.been.calledWith(root.childNodes[0].childNodes[0])
+ })
+
+ it("does not recursively call the config if a separate node is rendered to", function () { // eslint-disable-line
+ var root = mock.document.createElement("div")
+ var spy = sinon.spy(function () {
+ var island = mock.document.createElement("div")
+ m.render(island, m("div"))
+ })
+
+ m.render(root, m("div", m("a", {config: spy})))
+
+ expect(spy).to.be.calledOnce
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/129
+ it("does not throw replacing arrays with single entries", function () {
+ var root = mock.document.createElement("div")
+ expect(function () {
+ m.render(root, m("div", [
+ ["foo", "bar"],
+ ["foo", "bar"],
+ ["foo", "bar"]
+ ]))
+
+ m.render(root, m("div", ["asdf", "asdf2", "asdf3"]))
+ }).to.not.throw()
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/98
+ it("correctly keeps key association to nodes (1)", function () {
+ // insert at beginning
+ var root = mock.document.createElement("div")
+
+ m.render(root, [
+ m("a", {key: 1}, 1),
+ m("a", {key: 2}, 2),
+ m("a", {key: 3}, 3)
+ ])
+
+ var firstBefore = root.childNodes[0]
+
+ m.render(root, [
+ m("a", {key: 4}, 4),
+ m("a", {key: 1}, 1),
+ m("a", {key: 2}, 2),
+ m("a", {key: 3}, 3)
+ ])
+
+ var firstAfter = root.childNodes[1]
+
+ expect(firstBefore).to.equal(firstAfter)
+
+ expect(root.childNodes[0].childNodes[0])
+ .to.have.property("nodeValue", "4")
+
+ expect(root.childNodes).to.have.length(4)
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/98
+ it("correctly keeps key association to nodes (2)", function () {
+ var root = mock.document.createElement("div")
+
+ m.render(root, [
+ m("a", {key: 1}, 1),
+ m("a", {key: 2}, 2),
+ m("a", {key: 3}, 3)
+ ])
+
+ var firstBefore = root.childNodes[0]
+
+ m.render(root, [
+ m("a", {key: 4}, 4),
+ m("a", {key: 1}, 1),
+ m("a", {key: 2}, 2)
+ ])
+
+ var firstAfter = root.childNodes[1]
+
+ expect(firstBefore).to.equal(firstAfter)
+
+ expect(root.childNodes[0].childNodes[0])
+ .to.have.property("nodeValue", "4")
+
+ expect(root.childNodes).to.have.length(3)
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/98
+ it("correctly keeps key association to nodes (3)", function () {
+ var root = mock.document.createElement("div")
+
+ m.render(root, [
+ m("a", {key: 1}, 1),
+ m("a", {key: 2}, 2),
+ m("a", {key: 3}, 3)
+ ])
+
+ var firstBefore = root.childNodes[1]
+
+ m.render(root, [
+ m("a", {key: 2}, 2),
+ m("a", {key: 3}, 3),
+ m("a", {key: 4}, 4)
+ ])
+
+ var firstAfter = root.childNodes[0]
+
+ expect(firstBefore).to.equal(firstAfter)
+
+ expect(root.childNodes[0].childNodes[0])
+ .to.have.property("nodeValue", "2")
+
+ expect(root.childNodes).to.have.length(3)
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/98
+ it("correctly keeps key association to nodes (4)", function () {
+ var root = mock.document.createElement("div")
+
+ m.render(root, [
+ m("a", {key: 1}, 1),
+ m("a", {key: 2}, 2),
+ m("a", {key: 3}, 3),
+ m("a", {key: 4}, 4),
+ m("a", {key: 5}, 5)
+ ])
+
+ var firstBefore = root.childNodes[0]
+ var secondBefore = root.childNodes[1]
+ var fourthBefore = root.childNodes[3]
+
+ m.render(root, [
+ m("a", {key: 4}, 4),
+ m("a", {key: 10}, 10),
+ m("a", {key: 1}, 1),
+ m("a", {key: 2}, 2)
+ ])
+
+ var firstAfter = root.childNodes[2]
+ var secondAfter = root.childNodes[3]
+ var fourthAfter = root.childNodes[0]
+
+ expect(firstBefore).to.equal(firstAfter)
+ expect(secondBefore).to.equal(secondAfter)
+ expect(fourthBefore).to.equal(fourthAfter)
+ expect(root.childNodes[1].childNodes[0].nodeValue).to.equal("10")
+ expect(root.childNodes).to.have.length(4)
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/98
+ it("correctly keeps key association to nodes (5)", function () {
+ var root = mock.document.createElement("div")
+
+ m.render(root, [
+ m("a", {key: 1}, 1),
+ m("a", {key: 2}, 2),
+ m("a", {key: 3}, 3),
+ m("a", {key: 4}, 4),
+ m("a", {key: 5}, 5)
+ ])
+
+ var firstBefore = root.childNodes[0]
+ var secondBefore = root.childNodes[1]
+ var fourthBefore = root.childNodes[3]
+
+ m.render(root, [
+ m("a", {key: 4}, 4),
+ m("a", {key: 10}, 10),
+ m("a", {key: 2}, 2),
+ m("a", {key: 1}, 1),
+ m("a", {key: 6}, 6),
+ m("a", {key: 7}, 7)
+ ])
+
+ var firstAfter = root.childNodes[3]
+ var secondAfter = root.childNodes[2]
+ var fourthAfter = root.childNodes[0]
+
+ expect(firstBefore).to.equal(firstAfter)
+ expect(secondBefore).to.equal(secondAfter)
+ expect(fourthBefore).to.equal(fourthAfter)
+
+ expect(root.childNodes[1].childNodes[0].nodeValue).to.equal("10")
+ expect(root.childNodes[4].childNodes[0].nodeValue).to.equal("6")
+ expect(root.childNodes[5].childNodes[0].nodeValue).to.equal("7")
+
+ expect(root.childNodes).to.have.length(6)
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/149
+ it("correctly keeps key association to nodes (6)", function () {
+ 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]
+
+ expect(firstBefore).to.equal(firstAfter)
+ expect(secondBefore).to.equal(secondAfter)
+ expect(thirdBefore).to.equal(thirdAfter)
+ expect(fourthBefore).to.equal(fourthAfter)
+ expect(fifthBefore).to.equal(fifthAfter)
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/246
+ it("correctly renders non-keyed objects in the middle", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, [m("a", {key: 1}, 1)])
+ var firstBefore = root.childNodes[0]
+ m.render(root, [m("a", {key: 2}, 2), m("br"), m("a", {key: 1}, 1)])
+ var firstAfter = root.childNodes[2]
+ expect(firstBefore).to.equal(firstAfter)
+ expect(root.childNodes[0].childNodes[0].nodeValue).to.equal("2")
+ expect(root.childNodes.length).to.equal(3)
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/134
+ it("doesn't redraw when updating contenteditable", function () {
+ 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"))
+ expect(root.childNodes[0].childNodes[0].nodeValue).to.equal("test2")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/136
+ it("redraws when a textarea updates its values", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("textarea", ["test"]))
+ m.render(root, m("textarea", ["test1"]))
+ expect(root.childNodes[0].value).to.equal("test1")
+ })
+
+ it("doesn't call onunload when matching keys are given", function () {
+ var root = mock.document.createElement("div")
+ var spy = sinon.spy()
+ m.render(root, [
+ m("div", {
+ key: 1,
+ config: function (el, init, ctx) {
+ ctx.onunload = spy
+ }
+ })
+ ])
+ m.render(root, [
+ m("div", {key: 2}),
+ m("div", {
+ key: 1,
+ config: function (el, init, ctx) {
+ ctx.onunload = spy
+ }
+ })
+ ])
+ expect(spy).to.not.have.been.called
+ })
+
+ it("unloads the parent but not child, when parent changes and not child", function () { // eslint-disable-line
+ var root = mock.document.createElement("div")
+ var parentSpy = sinon.spy()
+ var childSpy = sinon.spy()
+
+ function parent(el, init, ctx) {
+ ctx.onunload = parentSpy
+ }
+
+ function child(el, init, ctx) {
+ ctx.onunload = childSpy
+ }
+
+ m.render(root, m("div", {config: parent}, m("a", {config: child})))
+ m.render(root, m("main", {config: parent}, m("a", {config: child})))
+
+ expect(parentSpy).to.be.calledOnce
+ expect(childSpy).to.not.have.been.called
+ })
+
+ it("unloads parent and child when both change", function () {
+ var root = mock.document.createElement("div")
+ var parentSpy = sinon.spy()
+ var childSpy = sinon.spy()
+
+ function parent(el, init, ctx) {
+ ctx.onunload = parentSpy
+ }
+
+ function child(el, init, ctx) {
+ ctx.onunload = childSpy
+ }
+
+ m.render(root, m("div", {config: parent}, m("a", {config: child})))
+ m.render(root, m("main", {config: parent}, m("b", {config: child})))
+ expect(parentSpy).to.have.been.calledOnce
+ expect(childSpy).to.have.been.calledOnce
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/150
+ it("treats empty arrays similarly to `null` and `undefined`", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, [m("a"), m("div")])
+ m.render(root, [[], m("div")])
+ expect(root.childNodes.length).to.equal(1)
+ expect(root.childNodes[0].nodeName).to.equal("DIV")
+ })
+
+
+ // https://github.com/lhorie/mithril.js/issues/157
+ it("renders nodes with new keys correctly", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("ul", [
+ m("li", {key: 0}, 0),
+ m("li", {key: 2}, 2),
+ m("li", {key: 4}, 4)
+ ]))
+
+ m.render(root, m("ul", [
+ m("li", {key: 0}, 0),
+ m("li", {key: 1}, 1),
+ m("li", {key: 2}, 2),
+ m("li", {key: 3}, 3),
+ m("li", {key: 4}, 4),
+ m("li", {key: 5}, 5)
+ ]))
+
+ expect(
+ root.childNodes[0].childNodes.map(function (n) {
+ return n.childNodes[0].nodeValue
+ })
+ ).to.eql(["0", "1", "2", "3", "4", "5"])
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/157
+ it("doesn't render extra child nodes if none are given (1)", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("input", {value: "a"}))
+ m.render(root, m("input", {value: "aa"}))
+ expect(root.childNodes[0].childNodes).to.be.empty
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/157
+ it("doesn't render extra child nodes if none are given (2)", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("br", {class: "a"}))
+ m.render(root, m("br", {class: "aa"}))
+ expect(root.childNodes[0].childNodes).to.be.empty
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/194
+ it("removes removed contained keyed elements from DOM", function () {
+ var root = mock.document.createElement("div")
+
+ m.render(root, m("ul", [
+ m("li", {key: 0}, 0),
+ m("li", {key: 1}, 1),
+ m("li", {key: 2}, 2),
+ m("li", {key: 3}, 3),
+ m("li", {key: 4}, 4),
+ m("li", {key: 5}, 5)
+ ]))
+
+ m.render(root, m("ul", [
+ m("li", {key: 0}, 0),
+ m("li", {key: 1}, 1),
+ m("li", {key: 2}, 2),
+ m("li", {key: 4}, 4),
+ m("li", {key: 5}, 5)
+ ]))
+
+ expect(
+ root.childNodes[0].childNodes.map(function (n) {
+ return n.childNodes[0].nodeValue
+ })
+ ).to.eql(["0", "1", "2", "4", "5"])
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/194
+ it("removes removed list of keyed elements from DOM", function () {
+ var root = mock.document.createElement("div")
+
+ m.render(root, m("ul", [
+ m("li", {key: 0}, 0),
+ m("li", {key: 1}, 1),
+ m("li", {key: 2}, 2),
+ m("li", {key: 3}, 3),
+ m("li", {key: 4}, 4),
+ m("li", {key: 5}, 5)
+ ]))
+
+ m.render(root, m("ul", [
+ m("li", {key: 1}, 1),
+ m("li", {key: 2}, 2),
+ m("li", {key: 3}, 3),
+ m("li", {key: 4}, 4),
+ m("li", {key: 5}, 5),
+ m("li", {key: 6}, 6)
+ ]))
+
+ m.render(root, m("ul", [
+ m("li", {key: 12}, 12),
+ m("li", {key: 13}, 13),
+ m("li", {key: 14}, 14),
+ m("li", {key: 15}, 15),
+ m("li", {key: 16}, 16),
+ m("li", {key: 17}, 17)
+ ]))
+
+ expect(
+ root.childNodes[0].childNodes.map(function (n) {
+ return n.childNodes[0].nodeValue
+ })
+ ).to.eql(["12", "13", "14", "15", "16", "17"])
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/206
+ it("removes `undefined` children", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("div", undefined))
+ m.render(root, m("div", [m("div")]))
+ expect(root.childNodes[0].childNodes).to.have.length(1)
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/206
+ it("removes `null` children", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("div", null))
+ m.render(root, m("div", [m("div")]))
+ expect(root.childNodes[0].childNodes).to.have.length(1)
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/200
+ it("calls onunload when updating a rendered collection", function () {
+ var root = mock.document.createElement("div")
+
+ var onunload1 = sinon.spy()
+ var onunload2 = sinon.spy()
+
+ m.render(root, [m("div", {
+ config: function (el, init, ctx) {
+ ctx.onunload = onunload1
+ }
+ })])
+
+ m.render(root, [])
+
+ m.render(root, [m("div", {
+ config: function (el, init, ctx) {
+ ctx.onunload = onunload2
+ }
+ })])
+
+ m.render(root, [])
+
+ expect(onunload1).to.be.called
+ expect(onunload2).to.be.called
+ })
+
+ it("should prepend new DOM elements", function () {
+ var root = mock.document.createElement("div")
+
+ m.render(root, [m("div.blue")])
+
+ m.render(root, [
+ m("div.green", [m("div")]),
+ m("div.blue")
+ ])
+
+ expect(root.childNodes).to.have.length(2)
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/277
+ it("adds objects that look like virtual nodes", function () {
+ var root = mock.document.createElement("div")
+ function Field() {
+ this.tag = "div"
+ this.attrs = {}
+ this.children = "hello"
+ }
+ m.render(root, new Field())
+ expect(root.childNodes).to.have.length(1)
+ })
+
+ it("doesn't add objects that don't look like virtual nodes", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, {foo: 123})
+ expect(root.childNodes).to.have.length(0)
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/299
+ it("retains key order in the presence of `null`s (1)", function () {
+ var root = mock.document.createElement("div")
+
+ m.render(root, m("div", [
+ m("div", {key: 1}, 1),
+ m("div", {key: 2}, 2),
+ m("div", {key: 3}, 3),
+ m("div", {key: 4}, 4),
+ m("div", {key: 5}, 5),
+ null, null, null, null, null, null, null, null, null, null
+ ]))
+
+ m.render(root, m("div", [
+ null, null,
+ m("div", {key: 3}, 3),
+ null, null,
+ m("div", {key: 6}, 6),
+ null, null,
+ m("div", {key: 9}, 9),
+ null, null,
+ m("div", {key: 12}, 12),
+ null, null,
+ m("div", {key: 15}, 15)
+ ]))
+
+ m.render(root, m("div", [
+ m("div", {key: 1}, 1),
+ m("div", {key: 2}, 2),
+ m("div", {key: 3}, 3),
+ m("div", {key: 4}, 4),
+ m("div", {key: 5}, 5),
+ null, null, null, null, null, null, null, null, null, null
+ ]))
+
+ expect(
+ root.childNodes[0].childNodes.map(function (c) {
+ return c.childNodes ? c.childNodes[0].nodeValue : c.nodeValue
+ }).slice(0, 5)
+ ).to.eql(["1", "2", "3", "4", "5"])
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/299
+ // https://github.com/lhorie/mithril.js/issues/377
+ it("retains key order in the presence of `null`s (2)", function () {
+ var root = mock.document.createElement("div")
+
+ m.render(root, m("div", [
+ m("div", 1),
+ m("div", 2),
+ [
+ m("div", {key: 3}, 3),
+ m("div", {key: 4}, 4),
+ m("div", {key: 5}, 5)
+ ],
+ [m("div", {key: 6}, 6)]
+ ]))
+
+ m.render(root, m("div", [
+ m("div", 1),
+ null,
+ [
+ m("div", {key: 3}, 3),
+ m("div", {key: 4}, 4),
+ m("div", {key: 5}, 5)
+ ],
+ [m("div", {key: 6}, 6)]
+ ]))
+
+ expect(
+ root.childNodes[0].childNodes.map(function (c) {
+ return c.childNodes ? c.childNodes[0].nodeValue : c.nodeValue
+ })
+ ).to.eql(["1", "", "3", "4", "5", "6"])
+ })
+
+ it("doesn't throw trying to render result of console.log()", function () {
+ var root = mock.document.createElement("div")
+ expect(function () {
+ /* eslint-disable no-console */
+ m.render(root, m("div", [console.log()]))
+ /* eslint-enable no-console */
+ }).to.not.throw()
+ })
+
+ it("retains key order for ids", function () {
+ var root = mock.document.createElement("div")
+
+ m.render(root, [
+ m("#div-1", {key: 1}),
+ m("#div-2", {key: 2}),
+ m("#div-3", {key: 3})
+ ])
+
+ root.appendChild(root.childNodes[1])
+
+ m.render(root, [
+ m("#div-1", {key: 1}),
+ m("#div-3", {key: 3}),
+ m("#div-2", {key: 2})
+ ])
+
+ expect(
+ root.childNodes.map(function (node) { return node.id })
+ ).to.eql(["div-1", "div-3", "div-2"])
+ })
+
+ it("doesn't render functions as nodes", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("div", function () {}))
+ expect(root.childNodes[0].childNodes).to.have.length(0)
+ })
+
+ it("removes nodes that result in only a single text node", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("div", "foo", m("a")))
+ m.render(root, m("div", "test"))
+ expect(root.childNodes[0].childNodes).to.have.length(1)
+ })
+
+ it("keeps identity if the element is preceded by conditional", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("div", [m("a"), m("input[autofocus]")]))
+ var before = root.childNodes[0].childNodes[1]
+ m.render(root, m("div", [undefined, m("input[autofocus]")]))
+ var after = root.childNodes[0].childNodes[1]
+ expect(before).to.equal(after)
+ })
+
+ it("keeps unkeyed identity if mixed with keyed elements and identity can be inferred", function () { // eslint-disable-line
+ var root = mock.document.createElement("div")
+
+ m.render(root, m("div", [
+ m("a", {key: 1}),
+ m("a", {key: 2}),
+ m("a", {key: 3}),
+ m("i")
+ ]))
+ var before = root.childNodes[0].childNodes[3]
+
+ m.render(root, m("div", [
+ m("b", {key: 3}),
+ m("b", {key: 4}),
+ m("i"),
+ m("b", {key: 1})
+ ]))
+ var after = root.childNodes[0].childNodes[2]
+
+ expect(before).to.equal(after)
+ })
+
+ it("keeps unkeyed identity if mixed with keyed/text elements and identity can be inferred", function () { // eslint-disable-line
+ var root = mock.document.createElement("div")
+
+ m.render(root, m("div", [
+ m("a", {key: 1}),
+ m("a", {key: 2}),
+ "foo",
+ m("a", {key: 3}),
+ m("i")
+ ]))
+ var before = root.childNodes[0].childNodes[4]
+
+ m.render(root, m("div", [
+ m("a", {key: 3}),
+ m("a", {key: 4}),
+ "bar",
+ m("i"),
+ m("a", {key: 1})
+ ]))
+ var after = root.childNodes[0].childNodes[3]
+
+ expect(before).to.equal(after)
+ })
+
+ it("keeps unkeyed identity if mixed with elements/nulls and identity can be inferred", function () { // eslint-disable-line
+ var root = mock.document.createElement("div")
+
+ m.render(root, m("div", [
+ m("a", {key: 1}),
+ m("a", {key: 2}),
+ null,
+ m("a", {key: 3}),
+ m("i")
+ ]))
+ var before = root.childNodes[0].childNodes[4]
+
+ m.render(root, m("div", [
+ m("a", {key: 3}),
+ m("a", {key: 4}),
+ null,
+ m("i"),
+ m("a", {key: 1})
+ ]))
+ var after = root.childNodes[0].childNodes[3]
+
+ expect(before).to.equal(after)
+ })
+
+ it("keeps unkeyed identity if mixed with elements/undefined and identity can be inferred", function () { // eslint-disable-line
+ var root = mock.document.createElement("div")
+
+ m.render(root, m("div", [
+ m("a", {key: 1}),
+ m("a", {key: 2}),
+ undefined,
+ m("a", {key: 3}),
+ m("i")
+ ]))
+ var before = root.childNodes[0].childNodes[4]
+
+ m.render(root, m("div", [
+ m("a", {key: 3}),
+ m("a", {key: 4}),
+ undefined,
+ m("i"),
+ m("a", {key: 1})
+ ]))
+ var after = root.childNodes[0].childNodes[3]
+
+ expect(before).to.equal(after)
+ })
+
+ // FIXME: implement document.createRange().createContextualFragment() in the
+ // mock document to fix this test
+ xit("keeps unkeyed identity if mixed with elements/trusted text and identity can be inferred", function () { // eslint-disable-line
+ var root = mock.document.createElement("div")
+
+ m.render(root, m("div", [
+ m("a", {key: 1}),
+ m("a", {key: 2}),
+ m.trust("a"),
+ m("a", {key: 3}),
+ m("i")
+ ]))
+ var before = root.childNodes[0].childNodes[4]
+
+ m.render(root, m("div", [
+ m("a", {key: 3}),
+ m("a", {key: 4}),
+ m.trust("a"),
+ m("i"),
+ m("a", {key: 1})
+ ]))
+ var after = root.childNodes[0].childNodes[3]
+
+ expect(before).to.equal(after)
+ })
+
+ it("uses the syntax class if it's given as `undefined` in attr", function () { // eslint-disable-line
+ var root = mock.document.createElement("div")
+ var vdom = m("div.a", {class: undefined})
+ m.render(root, vdom)
+ expect(root.childNodes[0].class).to.equal("a")
+ })
+
+ it("updates div with syntax class and removed body", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m(".a", [1]))
+ m.render(root, m(".a", []))
+ expect(root.childNodes[0].childNodes).to.have.length(0)
+ })
+
+ it("renders removed elements in div with empty attrs correctly", function () { // eslint-disable-line
+ var root = mock.document.createElement("div")
+ m.render(root, m("div", {}, [
+ m("div", {}, "0"),
+ m("div", {}, "1"),
+ m("div", {}, "2")
+ ]))
+
+ expect(
+ root.childNodes[0].childNodes.map(function (node) {
+ return node.childNodes[0].nodeValue
+ })
+ ).to.eql(["0", "1", "2"])
+
+ m.render(root, m("div", {}, [
+ m("div", {}, "0")
+ ]))
+
+ expect(
+ root.childNodes[0].childNodes.map(function (node) {
+ return node.childNodes[0].nodeValue
+ })
+ ).to.eql(["0"])
+ })
+
+ it("renders removed elements in span with empty attrs correctly", function () { // eslint-disable-line
+ var root = mock.document.createElement("div")
+ m.render(root, m("span", {}, [
+ m("div", {}, "0"),
+ m("div", {}, "1"),
+ m("div", {}, "2")
+ ]))
+
+ expect(
+ root.childNodes[0].childNodes.map(function (node) {
+ return node.childNodes[0].nodeValue
+ })
+ ).to.eql(["0", "1", "2"])
+
+ m.render(root, m("span", {}, [
+ m("div", {}, "0")
+ ]))
+
+ expect(
+ root.childNodes[0].childNodes.map(function (node) {
+ return node.childNodes[0].nodeValue
+ })
+ ).to.eql(["0"])
+ })
+
+ function emit(el, ev) {
+ el[ev]({currentTarget: el})
+ }
+
+ // https://github.com/lhorie/mithril.js/issues/214
+ it("keeps all input events", function () {
+ var root = mock.document.createElement("div")
+
+ var ctrl = m.mount(root, {
+ controller: function () {
+ this.inputValue = m.prop("")
+ },
+ view: function (ctrl) {
+ return m("input", {
+ value: ctrl.inputValue(),
+ onkeyup: m.withAttr("value", ctrl.inputValue)
+ })
+ }
+ })
+ mock.requestAnimationFrame.$resolve()
+
+ var input = mock.document.activeElement = root.childNodes[0]
+ var expected = ""
+ var keys = "0123456789abcdef"
+
+ function writeKey(key) {
+ input.value += key[0]
+ emit(input, "onkeyup")
+ mock.requestAnimationFrame.$resolve()
+ }
+
+ for (var i = 0; i < 4; i++) {
+ expected += keys
+ keys.split("").forEach(writeKey)
+ }
+
+ expect(ctrl.inputValue()).to.equal(expected)
+ expect(input.value).to.equal(expected)
+
+ mock.document.activeElement = null
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/288
+ it("doesn't reset if the input value is submitted with ", function () { // eslint-disable-line
+ var root = mock.document.createElement("div")
+
+ var ctrl = m.mount(root, {
+ controller: function () {
+ this.inputValue = m.prop("")
+
+ this.submit = function () {
+ if (this.inputValue()) {
+ this.inputValue("")
+ }
+ }.bind(this)
+ },
+
+ view: function (ctrl) {
+ return m("form", {onsubmit: ctrl.submit}, [
+ m("input", {
+ onkeyup: m.withAttr("value", ctrl.inputValue),
+ value: ctrl.inputValue()
+ }),
+ m("button[type=submit]")
+ ])
+ }
+ })
+
+ var form = root.childNodes[0]
+ var input = mock.document.activeElement = form.childNodes[0]
+
+ function writeKey(key) {
+ if (key === "[enter]") {
+ emit(form, "onsubmit")
+ } else {
+ input.value += key[0]
+ emit(input, "onkeyup")
+ }
+ mock.requestAnimationFrame.$resolve()
+ }
+
+ writeKey("a")
+ writeKey("b")
+ writeKey("c")
+ writeKey("d")
+ writeKey("[enter]")
+
+ expect(ctrl.inputValue()).to.equal("")
+ expect(input.value).to.equal("")
+
+ mock.document.activeElement = null
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/278
+ it("renders multiple select correctly", function () {
+ var root = mock.document.createElement("div")
+
+ m.mount(root, {
+ controller: function () {
+ this.values = [1, 2, 3, 4, 5]
+ this.value = m.prop([2, 3])
+ },
+
+ view: function (ctrl) {
+ return m("select", {
+ size: ctrl.values.length,
+ multiple: "multiple"
+ }, [
+ ctrl.values.map(function (v) {
+ var opts = {value: v}
+ if (ctrl.value().indexOf(v) !== -1) {
+ opts.selected = "selected"
+ }
+ return m("option", opts, v)
+ })
+ ])
+ }
+ })
+
+ mock.requestAnimationFrame.$resolve()
+
+ var select = root.childNodes[0]
+
+ expect(select.childNodes[0].selected).to.not.be.ok
+ expect(select.childNodes[1].selected).to.be.ok
+ expect(select.childNodes[2].selected).to.be.ok
+ expect(select.childNodes[3].selected).to.not.be.ok
+ expect(select.childNodes[4].selected).to.not.be.ok
+ })
+
+ it("doesn't treat 0 as an empty string", function () {
+ var root = mock.document.createElement("div")
+ m.render(root, m("div", {class: ""}))
+ m.render(root, m("div", {class: 0}))
+ expect(root.childNodes[0].class).to.equal("0")
+ })
+
+ dom(function () {
+ it("renders empty `value` in ')
+ })
+ })
+})
diff --git a/test/mithril.request.js b/test/mithril.request.js
new file mode 100644
index 00000000..fd8ab1f4
--- /dev/null
+++ b/test/mithril.request.js
@@ -0,0 +1,348 @@
+describe("m.request()", function () {
+ "use strict"
+
+ // Much easier to read
+ function resolve() {
+ var xhr = mock.XMLHttpRequest.$instances.pop()
+ xhr.onreadystatechange()
+ return xhr
+ }
+
+ // Common abstraction: request(opts, ...callbacks)
+ function request(opts) {
+ var ret = m.request(opts)
+ for (var i = 0; i < arguments.length; i++) {
+ ret = ret.then(arguments[i])
+ }
+ resolve()
+ return ret
+ }
+
+ it("sets the correct properties on `GET`", function () {
+ var prop = request({
+ method: "GET",
+ url: "test"
+ })
+
+ expect(prop()).to.contain.keys({
+ method: "GET",
+ url: "test"
+ })
+ })
+
+ it("returns a Mithril promise (1)", function () {
+ var prop = request(
+ {method: "GET", url: "test"},
+ function () { return "foo" })
+
+ expect(prop()).to.equal("foo")
+ })
+
+ it("returns a Mithril promise (2)", function () {
+ var prop = request({method: "GET", url: "test"})
+ var result = prop()
+
+ expect(prop.then(function (value) { return value })()).to.equal(result)
+ })
+
+ it("sets the correct properties on `POST`", function () {
+ var prop = request({
+ method: "POST",
+ url: "http://domain.com:80",
+ data: {}
+ })
+
+ expect(prop()).to.contain.keys({
+ method: "POST",
+ url: "http://domain.com:80"
+ })
+ })
+
+ it("sets the correct arguments", function () {
+ expect(request({
+ method: "POST",
+ url: "http://domain.com:80/:test1",
+ data: {test1: "foo"}
+ })().url).to.equal("http://domain.com:80/foo")
+ })
+
+ it("propogates errors through the promise (1)", function () {
+ var error = m.prop()
+
+ var prop = m.request({
+ method: "GET",
+ url: "test",
+ deserialize: function () { throw new Error("error occurred") }
+ }).then(null, error)
+ resolve()
+
+ expect(prop().message).to.equal("error occurred")
+ expect(error().message).to.equal("error occurred")
+ })
+
+ it("propogates errors through the promise (2)", function () {
+ var error = m.prop()
+
+ var prop = m.request({
+ method: "GET",
+ url: "test",
+ deserialize: function () { throw new Error("error occurred") }
+ }).catch(error)
+ resolve()
+
+ expect(prop().message).to.equal("error occurred")
+ expect(error().message).to.equal("error occurred")
+ })
+
+ it("does not propogate results to `finally`", function () {
+ // Data returned by then() functions do *not* propagate to finally().
+ var data = m.prop()
+ var prop = m.request({
+ method: "GET",
+ url: "test"
+ })
+ .then(function () { return "foo" })
+ .finally(data)
+
+ resolve()
+
+ expect(prop()).to.equal("foo")
+ expect(data()).to.not.exist
+ })
+
+ it("does not propogate `finally` results to the next promise", function () {
+ var data = m.prop()
+
+ var prop = m.request({method: "GET", url: "test"})
+ .then(function () { return "foo" })
+ .finally(function () { return "bar" })
+ .then(data)
+ resolve()
+
+ expect(prop()).to.equal("foo")
+ expect(data()).to.equal("foo")
+ })
+
+ it("propogates `finally` errors", function () {
+ var error = m.prop()
+
+ var prop = m.request({method: "GET", url: "test"})
+ .then(function () { return "foo" })
+ .finally(function () { throw new Error("error occurred") })
+ .catch(error)
+
+ resolve()
+ expect(prop().message).to.equal("error occurred")
+ expect(error().message).to.equal("error occurred")
+ })
+
+ it("runs successive `finally` after `catch`", function () {
+ var error = m.prop()
+
+ var prop = m.request({
+ method: "GET",
+ url: "test",
+ deserialize: function () { throw new Error("error occurred") }
+ })
+ .catch(error)
+ .finally(function () { error("finally") })
+
+ resolve()
+ expect(prop().message).to.equal("error occurred")
+ expect(error()).to.equal("finally")
+ })
+
+ it("synchronously throws TypeErrors", function () {
+ var error = m.prop()
+ var exception
+ var prop = m.request({
+ method: "GET",
+ url: "test",
+ deserialize: function () { throw new TypeError("error occurred") }
+ }).then(null, error)
+
+ try {
+ resolve()
+ } catch (e) {
+ exception = e
+ }
+
+ expect(prop()).to.not.exist
+ expect(error()).to.not.exist
+ expect(exception.message).to.equal("error occurred")
+ })
+
+ it("sets correct Content-Type when given data", function () {
+ var error = m.prop()
+
+ m.request({
+ method: "POST",
+ url: "test",
+ data: {foo: 1}
+ }).then(null, error)
+
+ var xhr = mock.XMLHttpRequest.$instances.pop()
+ xhr.onreadystatechange()
+
+ expect(xhr.$headers).to.have.property(
+ "Content-Type",
+ "application/json; charset=utf-8")
+ })
+
+ it("doesn't set Content-Type when it doesn't have data", function () {
+ var error = m.prop()
+
+ m.request({
+ method: "POST",
+ url: "test"
+ }).then(null, error)
+
+ var xhr = mock.XMLHttpRequest.$instances.pop()
+ xhr.onreadystatechange()
+
+ expect(xhr.$headers).to.not.have.property("Content-Type")
+ })
+
+ it("correctly sets initial value", function () {
+ var prop = m.request({
+ method: "POST",
+ url: "test",
+ initialValue: "foo"
+ })
+
+ var initialValue = prop()
+ resolve()
+
+ expect(initialValue).to.equal("foo")
+ })
+
+ it("correctly propogates initial value when not completed", function () {
+ var prop = m.request({
+ method: "POST",
+ url: "test",
+ initialValue: "foo"
+ }).then(function (value) { return value })
+
+ var initialValue = prop()
+ resolve()
+
+ expect(initialValue).to.equal("foo")
+ })
+
+ it("resolves `then` correctly with an initialValue", function () {
+ var prop = m.request({
+ method: "POST",
+ url: "test",
+ initialValue: "foo"
+ }).then(function () { return "bar" })
+
+ resolve()
+ expect(prop()).to.equal("bar")
+ })
+
+ it("appends query strings to `url` from `data` for `GET`", function () {
+ var prop = m.request({method: "GET", url: "/test", data: {foo: 1}})
+ resolve()
+ expect(prop().url).to.equal("/test?foo=1")
+ })
+
+ it("doesn't append query strings to `url` from `data` for `POST`", function () { // eslint-disable-line
+ var prop = m.request({method: "POST", url: "/test", data: {foo: 1}})
+ resolve()
+ expect(prop().url).to.equal("/test")
+ })
+
+ it("appends children in query strings to `url` from `data` for `GET`", function () { // eslint-disable-line
+ var prop = m.request({method: "GET", url: "test", data: {foo: [1, 2]}})
+ resolve()
+ expect(prop().url).to.equal("test?foo=1&foo=2")
+ })
+
+ it("propogates initial value in call before request is completed", function () { // eslint-disable-line
+ var value
+ var prop1 = m.request({method: "GET", url: "test", initialValue: 123})
+ expect(prop1()).to.equal(123)
+ var prop2 = prop1.then(function () { return 1 })
+ expect(prop2()).to.equal(123)
+ var prop3 = prop1.then(function (v) { value = v })
+ expect(prop3()).to.equal(123)
+ resolve()
+
+ expect(value.method).to.equal("GET")
+ expect(value.url).to.equal("test")
+ })
+
+ context("over jsonp", function () {
+ /* eslint-disable no-invalid-this */
+ beforeEach(function () {
+ var body = this.body = mock.document.createElement("body")
+ mock.document.body = body
+ mock.document.appendChild(body)
+ })
+
+ afterEach(function () {
+ mock.document.removeChild(this.body)
+ })
+ /* eslint-enable no-invalid-this */
+
+ function request(data, callbackKey) {
+ return m.request({
+ url: "/test",
+ dataType: "jsonp",
+ data: data,
+ callbackKey: callbackKey
+ })
+ }
+
+ function find(list, item, prop) {
+ var res
+ for (var i = 0; i < list.length; i++) {
+ var entry = list[i]
+ if (prop != null) entry = entry[prop]
+ if (entry.indexOf(item) >= 0) res = entry
+ }
+ return res
+ }
+
+ function resolve(data) {
+ var callback = find(Object.keys(mock), "mithril_callback")
+ var url = find(mock.document.getElementsByTagName("script"),
+ callback, "src")
+ mock[callback](data)
+ return url
+ }
+
+ it("sets the `GET` url with the correct query parameters", function () {
+ request({foo: "bar"})
+ expect(resolve({foo: "bar"})).to.contain("foo=bar")
+ })
+
+ it("correctly gets the value, without appending the script on the document", function () { // eslint-disable-line
+ var data = m.prop()
+
+ request().then(data)
+
+ var url = resolve({foo: "bar"})
+
+ expect(url).to.contain("/test?callback=mithril_callback")
+ expect(data()).to.eql({foo: "bar"})
+ })
+
+ it("correctly gets the value with a custom `callbackKey`, without appending the script on the document", function () { // eslint-disable-line
+ var data = m.prop()
+
+ request(null, "jsonpCallback").then(data)
+
+ var url = resolve({foo: "bar1"})
+
+ expect(url).to.contain("/test?jsonpCallback=mithril_callback")
+ expect(data()).to.eql({foo: "bar1"})
+ })
+
+ it("correctly gets the value on calling the function", function () {
+ var req = request()
+ resolve({foo: "bar1"})
+ expect(req()).to.eql({foo: "bar1"})
+ })
+ })
+})
diff --git a/test/mithril.route.buildQueryString.js b/test/mithril.route.buildQueryString.js
new file mode 100644
index 00000000..bd53ad5e
--- /dev/null
+++ b/test/mithril.route.buildQueryString.js
@@ -0,0 +1,26 @@
+describe("m.route.buildQueryString()", function () {
+ "use strict"
+
+ it("exists", function () {
+ expect(m.route.buildQueryString).to.be.a("function")
+ })
+
+ it("converts an empty object to an empty string", function () {
+ expect(m.route.buildQueryString({})).to.equal("")
+ })
+
+ it("converts an object into a correct query string", function () {
+ expect(
+ m.route.buildQueryString({
+ foo: "bar",
+ hello: ["world", "mars", "mars"],
+ world: {
+ test: 3
+ },
+ bam: "",
+ yup: null,
+ removed: undefined
+ })
+ ).to.equal("foo=bar&hello=world&hello=mars&world%5Btest%5D=3&bam=&yup")
+ })
+})
diff --git a/test/mithril.route.js b/test/mithril.route.js
new file mode 100644
index 00000000..7317d268
--- /dev/null
+++ b/test/mithril.route.js
@@ -0,0 +1,1240 @@
+describe("m.route()", function () {
+ "use strict"
+
+ // Use this instead of m.route() unless you have to call m.route and do
+ // something else in the same frame.
+ function route() {
+ var res = m.route.apply(null, arguments)
+ mock.requestAnimationFrame.$resolve()
+ return res
+ }
+
+ var mode = (function () {
+ var types = {
+ search: "?",
+ hash: "#",
+ pathname: "/"
+ }
+
+ return function (type) {
+ if (!{}.hasOwnProperty.call(types, type)) {
+ throw new RangeError("bad mode type")
+ }
+ mock.location[type] = types[type]
+ m.route.mode = type
+ }
+ })()
+
+ // Little helper utility
+ function noop() {}
+
+ // Use this if all you need to do is render a view (i.e. a pure component).
+ function pure(view) {
+ return {
+ controller: noop,
+ view: view
+ }
+ }
+
+ // Use these instead of `it` and `xit` in this set of tests if you need a
+ // root element.
+ var dit = makeIt(it)
+ var xdit = makeIt(xit)
+
+ // Wraps the `it` function for dependency injection that doesn't require
+ // `this`
+ /* eslint-disable no-invalid-this */
+ function makeIt(it) {
+ return function (name, callback) {
+ return it(name, function () {
+ var args = [this.root]
+ for (var i = 0; i < arguments.length; i++) {
+ args.push(arguments[i])
+ }
+ callback.apply(null, args)
+ })
+ }
+ }
+
+ beforeEach(function () {
+ mock.requestAnimationFrame.$resolve()
+ this.root = mock.document.createElement("div")
+ })
+
+ afterEach(function () {
+ m.mount(this.root, null)
+ })
+ /* eslint-enable no-invalid-this */
+
+ it("exists", function () {
+ expect(m.route).to.be.a("function")
+ })
+
+ dit("routes to the right location by default", function (root) {
+ mode("search")
+
+ route(root, "/test1", {
+ "/test1": pure(function () { return "foo" })
+ })
+
+ expect(mock.location.search).to.equal("?/test1")
+ expect(root.childNodes[0].nodeValue).to.equal("foo")
+ })
+
+ dit("gets the right right location when routed to it", function (root) {
+ mode("search")
+
+ var route1, route2
+ route(root, "/", {
+ "/": {
+ controller: function () { route1 = m.route() },
+ view: noop
+ },
+ "/test13": {
+ controller: function () { route2 = m.route() },
+ view: noop
+ }
+ })
+
+ m.route("/test13")
+
+ expect(route1).to.equal("/")
+ expect(route2).to.equal("/test13")
+ })
+
+ // FIXME: this causes others to fail
+ xdit("skips route change if component ctrl.onunload calls preventDefault", function (root) { // eslint-disable-line
+ mode("search")
+ var spy = sinon.spy()
+
+ var sub = {
+ controller: function () {
+ this.onunload = function (e) { e.preventDefault() }
+ },
+ view: function () {
+ return m("div")
+ }
+ }
+
+ route(root, "/a", {
+ "/a": pure(function () { return sub }),
+
+ "/b": {
+ controller: spy,
+ view: noop
+ }
+ })
+
+ route("/b")
+
+ expect(spy).to.not.have.been.called
+ })
+
+ // FIXME: this causes others to fail
+ xdit("skips route change if subcomponent ctrl.onunload calls preventDefault", function (root) { // eslint-disable-line
+ mode("search")
+
+ var spy = sinon.spy()
+
+ var subsub = {
+ controller: function () {
+ this.onunload = function (e) { e.preventDefault() }
+ },
+ view: function () {
+ return m("div")
+ }
+ }
+
+ var sub = pure(function () { return subsub })
+
+ route(root, "/a", {
+ "/a": pure(function () { return sub }),
+
+ "/b": {
+ controller: spy,
+ view: noop
+ }
+ })
+
+ route("/b")
+
+ expect(spy).to.not.have.been.called
+ })
+
+ // FIXME: this causes others to fail
+ xdit("skips route change if non-curried component ctrl.onunload calls preventDefault", function (root) { // eslint-disable-line
+ mode("search")
+
+ var spy = sinon.spy()
+
+ var sub = {
+ controller: function () {
+ this.onunload = function (e) { e.preventDefault() }
+ },
+ view: function () {
+ return m("div")
+ }
+ }
+
+ route(root, "/a", {
+ "/a": pure(function () { return sub }),
+
+ "/b": {
+ controller: spy,
+ view: noop
+ }
+ })
+
+ route("/b")
+
+ expect(spy).to.not.have.been.called
+ })
+
+ // FIXME: this causes others to fail
+ xdit("skips route change if non-curried subcomponent ctrl.onunload calls preventDefault", function (root) { // eslint-disable-line
+ mode("search")
+
+ var spy = sinon.spy()
+
+ var subsub = {
+ controller: function () {
+ this.onunload = function (e) { e.preventDefault() }
+ },
+ view: function () {
+ return m("div")
+ }
+ }
+
+ var sub = pure(function () { return subsub })
+
+ route(root, "/a", {
+ "/a": pure(function () { return sub }),
+
+ "/b": {
+ controller: spy,
+ view: noop
+ }
+ })
+
+ route("/b")
+
+ expect(spy).to.not.have.been.called
+ })
+
+ dit("initializes a component's constructor on route change", function (root) { // eslint-disable-line
+ mode("search")
+
+ var ctrl1 = sinon.spy()
+ var ctrl2 = sinon.spy()
+
+ var sub1 = {
+ controller: ctrl1,
+ view: function () { return m("div") }
+ }
+
+ var sub2 = {
+ controller: ctrl2,
+ view: function () { return m("div") }
+ }
+
+ route(root, "/a", {
+ "/a": pure(function () {
+ return m(".page-a", [
+ m("h1"), m.component(sub1, {x: 11})
+ ])
+ }),
+
+ "/b": pure(function () {
+ return m(".page-b", [
+ m("h2"), m.component(sub2, {y: 22})
+ ])
+ })
+ })
+
+ route("/b")
+ route("/a")
+
+ expect(ctrl1).to.have.been.calledTwice
+ expect(ctrl2).to.have.been.calledOnce
+ })
+
+ dit("doesn't require components to have a view", function (root) {
+ mode("search")
+
+ var Component = pure(function () { return m(".comp") })
+
+ route(root, "/foo", {
+ "/foo": pure(function () { return [Component] })
+ })
+
+ expect(root.childNodes[0].nodeName).to.equal("DIV")
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/555
+ dit("reinstantiates the controller when redraw strategy is `all`", function (root) { // eslint-disable-line
+ var MyComponent = {
+ controller: function (args) {
+ this.name = args.name
+ },
+
+ view: function (ctrl) {
+ return m("div", ctrl.name)
+ }
+ }
+
+ route(root, "/", {
+ "/": pure(function () {
+ return m("div", [
+ m("a[href=/]", {config: m.route}, "foo"),
+ m("a[href=/bar]", {config: m.route}, "bar"),
+ m.component(MyComponent, {name: "Jane"})
+ ])
+ }),
+
+ "/bar": pure(function () {
+ return m("div", [
+ m("a[href=/]", {config: m.route}, "foo"),
+ m("a[href=/bar]", {config: m.route}, "bar"),
+ m.component(MyComponent, {name: "Bob"})
+ ])
+ })
+ })
+
+ route("/bar")
+
+ expect(root.childNodes[0].childNodes[2].childNodes[0].nodeValue)
+ .to.equal("Bob")
+ })
+
+ dit("sets the correct href with config: m.route", function (root) {
+ mode("pathname")
+
+ route(root, "/test2", {
+ "/test2": pure(function () {
+ return [
+ "foo",
+ m("a", {href: "/test2", config: m.route}, "Test2")
+ ]
+ })
+ })
+
+ expect(mock.location.pathname).to.equal("/test2")
+ expect(root.childNodes[0].nodeValue).to.equal("foo")
+ expect(root.childNodes[1].href).to.equal("/test2")
+ })
+
+ dit("can use a hash", function (root) {
+ mode("hash")
+
+ route(root, "/test3", {
+ "/test3": pure(function () { return "foo" })
+ })
+
+ expect(mock.location.hash).to.equal("#/test3")
+ expect(root.childNodes[0].nodeValue).to.equal("foo")
+ })
+
+ dit("can use a query", function (root) {
+ mode("search")
+
+ route(root, "/test4/foo", {
+ "/test4/:test": pure(function () { return m.route.param("test") })
+ })
+
+ expect(mock.location.search).to.equal("?/test4/foo")
+ expect(root.childNodes[0].nodeValue).to.equal("foo")
+ })
+
+ context("m.route.param()", function () {
+ it("exists", function () {
+ expect(m.route.param).to.be.a("function")
+ })
+
+ dit("can get params (1)", function (root) {
+ mode("search")
+
+ var component = pure(function () { return m.route.param("test") })
+
+ m.route(root, "/test5/foo", {
+ "/": component,
+ "/test5/:test": component
+ })
+
+ var paramValueBefore = m.route.param("test")
+
+ mock.requestAnimationFrame.$resolve()
+ m.route("/")
+
+ var paramValueAfter = m.route.param("test")
+
+ mock.requestAnimationFrame.$resolve()
+
+ expect(mock.location.search).to.equal("?/")
+ expect(paramValueBefore).to.equal("foo")
+ expect(paramValueAfter).to.not.exist
+ })
+
+ dit("can deal with params (2)", function (root) {
+ mode("search")
+
+ var component = pure(function () { return m.route.param("a1") })
+
+ m.route(root, "/test6/foo", {
+ "/": component,
+ "/test6/:a1": component
+ })
+
+ var paramValueBefore = m.route.param("a1")
+
+ mock.requestAnimationFrame.$resolve()
+ m.route("/")
+
+ var paramValueAfter = m.route.param("a1")
+
+ mock.requestAnimationFrame.$resolve()
+
+ expect(mock.location.search).to.equal("?/")
+ expect(paramValueBefore).to.equal("foo")
+ expect(paramValueAfter).to.not.exist
+ })
+
+ // https://github.com/lhorie/mithril.js/issues/61
+ dit("can get the route via m.route()", function (root) {
+ mode("search")
+
+ var component = pure(function () { return m.route.param("a1") })
+
+ m.route(root, "/test7/foo", {
+ "/": component,
+ "/test7/:a1": component
+ })
+
+ var routeValueBefore = m.route()
+
+ mock.requestAnimationFrame.$resolve()
+ m.route("/")
+
+ var routeValueAfter = m.route()
+
+ mock.requestAnimationFrame.$resolve()
+
+ expect(routeValueBefore).to.equal("/test7/foo")
+ expect(routeValueAfter).to.equal("/")
+ })
+
+ dit("can deal with rest paths at the end", function (root) {
+ mode("search")
+
+ route(root, "/test8/foo/SEP/bar/baz", {
+ "/test8/:test/SEP/:path...": pure(function () {
+ return m.route.param("test") + "_" + m.route.param("path")
+ })
+ })
+
+ expect(mock.location.search).to.equal("?/test8/foo/SEP/bar/baz")
+ expect(root.childNodes[0].nodeValue).to.equal("foo_bar/baz")
+ })
+
+ dit("can deal with rest paths in the middle", function (root) {
+ mode("search")
+
+ route(root, "/test9/foo/bar/SEP/baz", {
+ "/test9/:test.../SEP/:path": pure(function () {
+ return m.route.param("test") + "_" + m.route.param("path")
+ })
+ })
+
+ expect(mock.location.search).to.equal("?/test9/foo/bar/SEP/baz")
+ expect(root.childNodes[0].nodeValue).to.equal("foo/bar_baz")
+ })
+
+ dit("unescapes urls for m.route.param()", function (root) {
+ mode("search")
+
+ route(root, "/test10/foo%20bar", {
+ "/test10/:test": pure(function () {
+ return m.route.param("test")
+ })
+ })
+
+ expect(root.childNodes[0].nodeValue).to.equal("foo bar")
+ })
+
+ dit("renders the correct path", function (root) {
+ mode("search")
+
+ route(root, "/", {
+ "/": pure(function () { return "foo" }),
+ "/test11": pure(function () { return "bar" })
+ })
+
+ route("/test11/")
+
+ expect(mock.location.search).to.equal("?/test11/")
+ expect(root.childNodes[0].nodeValue).to.equal("bar")
+ })
+
+ dit("reads params by parsing query string", function (root) {
+ mode("search")
+
+ route(root, "/", {
+ "/": pure(noop),
+ "/test12": pure(noop)
+ })
+
+ route("/test12?a=foo&b=bar")
+
+ expect(mock.location.search).to.equal("?/test12?a=foo&b=bar")
+ expect(m.route.param("a")).to.equal("foo")
+ expect(m.route.param("b")).to.equal("bar")
+ })
+
+ dit("prefers local params to global params", function (root) {
+ mode("search")
+
+ route(root, "/", {
+ "/": pure(function () { return "bar" }),
+ "/test13/:test": pure(function () {
+ return m.route.param("test")
+ })
+ })
+
+ route("/test13/foo?test=bar")
+
+ expect(mock.location.search).to.equal("?/test13/foo?test=bar")
+ expect(root.childNodes[0].nodeValue).to.equal("foo")
+ })
+
+ dit("reads global params", function (root) {
+ mode("search")
+
+ route(root, "/", {
+ "/": pure(function () { return "bar" }),
+ "/test14": pure(function () { return "foo" })
+ })
+
+ route("/test14?test&test2=")
+
+ expect(mock.location.search).to.equal("?/test14?test&test2=")
+ expect(m.route.param("test")).to.not.exist
+ expect(m.route.param("test2")).to.equal("")
+ })
+
+ dit("parses params when using m.route(path, params)", function (root) {
+ mode("search")
+
+ route(root, "/", {
+ "/": pure(noop),
+ "/test12": pure(noop)
+ })
+
+ route("/test12", {a: "foo", b: "bar"})
+
+ expect(mock.location.search).to.equal("?/test12?a=foo&b=bar")
+ expect(m.route.param("a")).to.equal("foo")
+ expect(m.route.param("b")).to.equal("bar")
+ })
+
+ dit("gets params object by using m.route.param()", function (root) {
+ mode("search")
+
+ route(root, "/", {
+ "/": pure(noop),
+ "/test12": pure(noop)
+ })
+
+ route("/test12", {a: "foo", b: "bar"})
+
+ var params = m.route.param()
+
+ expect(params.a).to.equal("foo")
+ expect(params.b).to.equal("bar")
+ })
+ })
+
+ dit("only calls onunload once when routed away (1)", function (root) {
+ mode("search")
+
+ var onunload = sinon.spy()
+
+ route(root, "/", {
+ "/": pure(function () {
+ return m("div", {
+ config: function (el, init, ctx) {
+ ctx.onunload = onunload
+ }
+ })
+ }),
+ "/test14": pure(noop)
+ })
+
+ route("/test14")
+
+ expect(onunload).to.be.calledOnce
+ })
+
+ dit("only calls onunload once when routed away (2)", function (root) {
+ mode("search")
+
+ var onunload = sinon.spy()
+
+ route(root, "/", {
+ "/": pure(function () {
+ return [
+ m("div"),
+ m("div", {
+ config: function (el, init, ctx) {
+ ctx.onunload = onunload
+ }
+ })
+ ]
+ }),
+ "/test15": pure(function () { return [m("div")] })
+ })
+
+ route("/test15")
+
+ expect(onunload).to.be.calledOnce
+ })
+
+ dit("only calls onunload once when routed away (3)", function (root) {
+ mode("search")
+
+ var onunload = sinon.spy()
+
+ route(root, "/", {
+ "/": pure(function () {
+ return m("div", {
+ config: function (el, init, ctx) {
+ ctx.onunload = onunload
+ }
+ })
+ }),
+ "/test16": pure(function () { return m("a") })
+ })
+
+ route("/test16")
+
+ expect(onunload).to.be.calledOnce
+ })
+
+ dit("only calls onunload once when routed away (4)", function (root) {
+ mode("search")
+
+ var onunload = sinon.spy()
+
+ route(root, "/", {
+ "/": pure(function () {
+ return [
+ m("div", {
+ config: function (el, init, ctx) {
+ ctx.onunload = onunload
+ }
+ })
+ ]
+ }),
+ "/test17": pure(function () { return m("a") })
+ })
+
+ route("/test17")
+
+ expect(onunload).to.be.calledOnce
+ })
+
+ dit("only calls onunload once when routed away (5)", function (root) {
+ mode("search")
+
+ var onunload = sinon.spy()
+
+ route(root, "/", {
+ "/": pure(function () {
+ return m("div", {
+ config: function (el, init, ctx) {
+ ctx.onunload = onunload
+ }
+ })
+ }),
+ "/test18": pure(function () { return [m("a")] })
+ })
+
+ route("/test18")
+
+ expect(onunload).to.be.calledOnce
+ })
+
+ dit("only calls onunload once when routed away (6)", function (root) {
+ mode("search")
+
+ var onunload = sinon.spy()
+
+ route(root, "/", {
+ "/": pure(function () {
+ return [
+ m("div", {
+ key: 1,
+ config: function (el, init, ctx) {
+ ctx.onunload = onunload
+ }
+ })
+ ]
+ }),
+ "/test20": pure(function () {
+ return [
+ m("div", {
+ key: 2,
+ config: function (el, init, ctx) {
+ ctx.onunload = onunload
+ }
+ })
+ ]
+ })
+ })
+
+ route("/test20")
+
+ expect(onunload).to.be.calledOnce
+ })
+
+ dit("only calls onunload once when routed away (7)", function (root) {
+ mode("search")
+
+ var onunload = sinon.spy()
+
+ route(root, "/", {
+ "/": pure(function () {
+ return [
+ m("div", {
+ key: 1,
+ config: function (el, init, ctx) {
+ ctx.onunload = onunload
+ }
+ })
+ ]
+ }),
+ "/test21": pure(function () {
+ return [
+ m("div", {
+ config: function (el, init, ctx) {
+ ctx.onunload = onunload
+ }
+ })
+ ]
+ })
+ })
+
+ route("/test21")
+
+ expect(onunload).to.be.calledOnce
+ })
+
+ dit("renders the right virtual node when routed to it", function (root) {
+ mode("search")
+
+ route(root, "/foo", {
+ "/foo": pure(function () { return m("div", "foo") }),
+ "/bar": pure(function () { return m("div", "bar") })
+ })
+ var foo = root.childNodes[0].childNodes[0].nodeValue
+
+ route("/bar")
+ var bar = root.childNodes[0].childNodes[0].nodeValue
+
+ expect(foo).to.equal("foo")
+ expect(bar).to.equal("bar")
+ })
+
+ dit("keeps identity with unchanged nodes", function (root) {
+ mode("search")
+
+ var onunload = sinon.spy()
+ function config(el, init, ctx) {
+ ctx.onunload = onunload
+ }
+
+ route(root, "/foo1", {
+ "/foo1": pure(function () {
+ return m("div", m("a", {config: config}, "foo"))
+ }),
+ "/bar1": pure(function () {
+ return m("main", m("a", {config: config}, "foo"))
+ })
+ })
+
+ route("/bar1")
+
+ expect(onunload).to.be.calledOnce
+ })
+
+ dit("allows illegal URL characters in paths", function (root) {
+ mode("search")
+
+ var value
+ m.route(root, "/foo+bar", {
+ "/:arg": {
+ controller: function () { value = m.route.param("arg") },
+ view: function () {
+ return ""
+ }
+ }
+ })
+ expect(value).to.equal("foo+bar")
+ })
+
+ dit("allows trailing slashes in paths", function (root) {
+ mode("search")
+
+ route(root, "/", {
+ "/": pure(function () { return "foo" }),
+ "/test22": pure(function () { return "bar" })
+ })
+
+ m.route("/test22/")
+
+ expect(mock.location.search).to.equal("?/test22/")
+ expect(root.childNodes[0].nodeValue).to.equal("bar")
+ })
+
+ dit("reads non-primitive String objects in route changes", function (root) {
+ mode("search")
+
+ route(root, "/", {
+ "/": pure(function () { return "foo" }),
+ "/test23": pure(function () { return "bar" })
+ })
+
+ route(new String("/test23/")) // eslint-disable-line no-new-wrappers
+
+ expect(mock.location.search).to.equal("?/test23/")
+ expect(root.childNodes[0].nodeValue).to.equal("bar")
+ })
+
+ dit("reads primitive Strings in default routes", function (root) {
+ mode("search")
+
+ var value
+ m.route(root, "/foo+bar", {
+ "/:arg": {
+ controller: function () { value = m.route.param("arg") },
+ view: function () {
+ return ""
+ }
+ }
+ })
+ expect(value).to.equal("foo+bar")
+ })
+
+ dit("reads non-primitive Strings in default routes", function (root) {
+ mode("search")
+
+ var value
+ m.route(root, new String("/foo+bar"), { // eslint-disable-line
+ "/:arg": {
+ controller: function () { value = m.route.param("arg") },
+ view: function () {
+ return ""
+ }
+ }
+ })
+
+ expect(value).to.equal("foo+bar")
+ })
+
+ dit("can redirect to another route while loading default", function (root) { // eslint-disable-line
+ mode("search")
+
+ route(root, "/a", {
+ "/a": {
+ controller: function () { m.route("/b") },
+ view: function () { return "a" }
+ },
+
+ "/b": pure(function () { return "b" })
+ })
+
+ expect(root.childNodes[0].nodeValue).to.equal("b")
+ })
+
+ dit("can redirect to another route with params while loading default", function (root) { // eslint-disable-line
+ mode("search")
+
+ route(root, "/", {
+ "/": {
+ controller: function () {
+ m.route("/b?foo=1", {foo: 2})
+ },
+ view: function () { return "a" }
+ },
+ "/b": pure(function () { return "b" })
+ })
+
+ expect(mock.location.search).to.equal("?/b?foo=2")
+ })
+
+ dit("modifies history when changing route", function (root) {
+ mode("search")
+ mock.history.$$length = 0
+
+ route(root, "/a", {
+ "/a": pure(function () { return "a" }),
+ "/b": pure(function () { return "b" })
+ })
+
+ route("/b")
+
+ expect(mock.history.$$length).to.equal(1)
+ })
+
+ dit("doesn't modify history when redirecting to same route", function (root) { // eslint-disable-line
+ mode("search")
+ mock.history.$$length = 0
+
+ route(root, "/a", {
+ "/a": pure(function () { return "a" }),
+ "/b": pure(function () { return "b" })
+ })
+
+ route("/a")
+
+ expect(mock.history.$$length).to.equal(0)
+ })
+
+ context("m.route.strategy() === \"all\", identical views", function () {
+ context("parent nodes", function () {
+ dit("renders routes independently", function (root) {
+ mode("search")
+ var initCount = 0
+
+ var a = pure(function () {
+ return m("a", {
+ config: function (el, init) {
+ if (!init) initCount++
+ }
+ })
+ })
+
+ route(root, "/a", {
+ "/a": a,
+ "/b": pure(a.view)
+ })
+
+ route("/b")
+
+ expect(initCount).to.equal(2)
+ })
+
+ dit("renders routes independently with `context.retain === false`", function (root) { // eslint-disable-line
+ mode("search")
+ var initCount = 0
+
+ var a = pure(function () {
+ return m("a", {
+ config: function (el, init, ctx) {
+ ctx.retain = false
+ if (!init) initCount++
+ }
+ })
+ })
+
+ route(root, "/a", {
+ "/a": a,
+ "/b": pure(a.view)
+ })
+
+ route("/b")
+
+ expect(initCount).to.equal(2)
+ })
+
+ dit("renders routes independently with `context.retain === true`", function (root) { // eslint-disable-line
+ mode("search")
+ var initCount = 0
+
+ var a = pure(function () {
+ return m("a", {
+ config: function (el, init, ctx) {
+ ctx.retain = true
+ if (!init) initCount++
+ }
+ })
+ })
+
+ route(root, "/a", {
+ "/a": a,
+ "/b": pure(a.view)
+ })
+
+ route("/b")
+
+ expect(initCount).to.equal(1)
+ })
+ })
+
+ context("child nodes", function () {
+ dit("reinitialize unchanged child nodes without `context.retain`", function (root) { // eslint-disable-line
+ mode("search")
+ var initCount = 0
+
+ function config(el, init) {
+ if (!init) initCount++
+ }
+
+ route(root, "/a", {
+ "/a": pure(function () {
+ return m("div", m("a", {config: config}))
+ }),
+ "/b": pure(function () {
+ return m("section", m("a", {config: config}))
+ })
+ })
+
+ route("/b")
+
+ expect(initCount).to.equal(2)
+ })
+
+ dit("doesn't reinitialize unchanged child nodes with `context.retain === true`", function (root) { // eslint-disable-line
+ mode("search")
+ var initCount = 0
+ function config(el, init, ctx) {
+ ctx.retain = true
+ if (!init) initCount++
+ }
+
+ route(root, "/a", {
+ "/a": pure(function () {
+ return m("div", m("a", {config: config}))
+ }),
+ "/b": pure(function () {
+ return m("section", m("a", {config: config}))
+ })
+ })
+
+ route("/b")
+
+ expect(initCount).to.equal(1)
+ })
+
+ dit("reinitializes unchanged child nodes with `context.retain === false`", function (root) { // eslint-disable-line
+ mode("search")
+ var initCount = 0
+ function config(el, init, ctx) {
+ ctx.retain = false
+ if (!init) initCount++
+ }
+
+ route(root, "/a", {
+ "/a": pure(function () {
+ return m("div", m("a", {config: config}))
+ }),
+ "/b": pure(function () {
+ return m("section", m("a", {config: config}))
+ })
+ })
+
+ route("/b")
+
+ expect(initCount).to.equal(2)
+ })
+ })
+ })
+
+ context("m.route.strategy() === \"diff\"", function () {
+ function diff(view) {
+ return {
+ controller: function () { m.redraw.strategy("diff") },
+ view: view
+ }
+ }
+
+ context("parent nodes", function () {
+ dit("renders routes independently", function (root) {
+ mode("search")
+ var initCount = 0
+
+ var a = diff(function () {
+ return m("a", {
+ config: function (el, init) {
+ if (!init) initCount++
+ }
+ })
+ })
+
+ route(root, "/a", {
+ "/a": a,
+ "/b": diff(a.view)
+ })
+
+ route("/b")
+
+ expect(initCount).to.equal(1)
+ })
+
+ dit("renders routes independently with `context.retain === false`", function (root) { // eslint-disable-line
+ mode("search")
+ var initCount = 0
+
+ var a = diff(function () {
+ return m("a", {
+ config: function (el, init, ctx) {
+ ctx.retain = true
+ if (!init) initCount++
+ }
+ })
+ })
+
+ route(root, "/a", {
+ "/a": a,
+ "/b": diff(a.view)
+ })
+
+ route("/b")
+
+ expect(initCount).to.equal(1)
+ })
+
+ dit("renders routes independently with `context.retain === true`", function (root) { // eslint-disable-line
+ mode("search")
+ var initCount = 0
+
+ var a = diff(function () {
+ return m("a", {
+ config: function (el, init, ctx) {
+ ctx.retain = false
+ if (!init) initCount++
+ }
+ })
+ })
+
+ route(root, "/a", {
+ "/a": a,
+ "/b": diff(a.view)
+ })
+
+ route("/b")
+
+ expect(initCount).to.equal(2)
+ })
+ })
+
+ context("child nodes", function () {
+ dit("reinitialize unchanged child nodes without `context.retain`", function (root) { // eslint-disable-line
+ mode("search")
+ var initCount = 0
+
+ function config(el, init) {
+ if (!init) initCount++
+ }
+
+ route(root, "/a", {
+ "/a": diff(function () {
+ return m("div", m("a", {config: config}))
+ }),
+ "/b": diff(function () {
+ return m("section", m("a", {config: config}))
+ })
+ })
+
+ route("/b")
+
+ expect(initCount).to.equal(1)
+ })
+
+ dit("doesn't reinitialize unchanged child nodes with `context.retain === true`", function (root) { // eslint-disable-line
+ mode("search")
+ var initCount = 0
+
+ function config(el, init, ctx) {
+ ctx.retain = true
+ if (!init) initCount++
+ }
+
+ route(root, "/a", {
+ "/a": diff(function () {
+ return m("div", m("a", {config: config}))
+ }),
+ "/b": diff(function () {
+ return m("section", m("a", {config: config}))
+ })
+ })
+
+ route("/b")
+
+ expect(initCount).to.equal(1)
+ })
+
+ dit("reinitializes unchanged child nodes with `context.retain === false`", function (root) { // eslint-disable-line
+ mode("search")
+ var initCount = 0
+
+ function config(el, init, ctx) {
+ ctx.retain = false
+ if (!init) initCount++
+ }
+
+ route(root, "/a", {
+ "/a": diff(function () {
+ return m("div", m("a", {config: config}))
+ }),
+ "/b": diff(function () {
+ return m("section", m("a", {config: config}))
+ })
+ })
+
+ m.route("/b")
+
+ expect(initCount).to.equal(2)
+ })
+ })
+ })
+
+ dit("honors retain flag inside child components during route change", function (root) { // eslint-disable-line
+ mode("search")
+ var initCount = 0
+
+ function config(el, init, ctx) {
+ ctx.retain = true
+ if (!init) initCount++
+ }
+
+ var a = pure(function () {
+ return m("div", m("a", {config: config}))
+ })
+
+ var b = {
+ controller: function () { m.redraw.strategy("diff") },
+ view: function () {
+ return m("section", m("a", {config: config}))
+ }
+ }
+
+ route(root, "/a", {
+ "/a": pure(function () { return m("div", a) }),
+ "/b": pure(function () { return m("div", b) })
+ })
+
+ route("/b")
+
+ expect(initCount).to.equal(1)
+ })
+
+ // https://github.com/lhorie/mithril.js/pull/571
+ dit("clears nodes with config on route change", function (root) {
+ mode("search")
+
+ route(root, "/a", {
+ "/a": pure(function () {
+ return m("div", {
+ config: function (el) {
+ el.childNodes[0].modified = true
+ }
+ }, m("div"))
+ }),
+ "/b": pure(function () { return m("div", m("div")) })
+ })
+
+ route("/b")
+
+ expect(root.childNodes[0].childNodes[0])
+ .to.not.have.property("modified")
+ })
+})
diff --git a/test/mithril.route.parseQueryString.js b/test/mithril.route.parseQueryString.js
new file mode 100644
index 00000000..3acc68ba
--- /dev/null
+++ b/test/mithril.route.parseQueryString.js
@@ -0,0 +1,34 @@
+describe("m.route.parseQueryString()", function () {
+ "use strict"
+
+ it("exists", function () {
+ expect(m.route.parseQueryString).to.be.a("function")
+ })
+
+ it("parses an empty string as an empty object", function () {
+ var args = m.route.parseQueryString("")
+ expect(args).to.eql({})
+ })
+
+ it("parses multiple parameters correctly", function () {
+ var args = m.route.parseQueryString("foo=bar&hello=world&hello=mars" +
+ "&bam=&yup")
+
+ expect(args).to.eql({
+ foo: "bar",
+ hello: ["world", "mars"],
+ bam: "",
+ yup: null
+ })
+ })
+
+ it("parses escapes correctly", function () {
+ var args = m.route.parseQueryString("foo=bar&hello%5B%5D=world&" +
+ "hello%5B%5D=mars&hello%5B%5D=pluto")
+
+ expect(args).to.eql({
+ foo: "bar",
+ "hello[]": ["world", "mars", "pluto"]
+ })
+ })
+})
diff --git a/test/mithril.startComputation.js b/test/mithril.startComputation.js
new file mode 100644
index 00000000..cfa58063
--- /dev/null
+++ b/test/mithril.startComputation.js
@@ -0,0 +1,28 @@
+describe("m.startComputation(), m.endComputation()", function () {
+ "use strict"
+
+ it("exists", function () {
+ expect(m.startComputation).to.be.a("function")
+ expect(m.endComputation).to.be.a("function")
+ })
+
+ it("blocks automatic rendering", function () {
+ mock.requestAnimationFrame.$resolve()
+
+ var root = mock.document.createElement("div")
+ var controller = m.mount(root, {
+ controller: function () {},
+ view: function (ctrl) { return ctrl.value }
+ })
+
+ mock.requestAnimationFrame.$resolve()
+
+ m.startComputation()
+ controller.value = "foo"
+ m.endComputation()
+ mock.requestAnimationFrame.$resolve()
+ expect(root.childNodes[0].nodeValue).to.equal("foo")
+ })
+
+ // FIXME: this needs to be better tested
+})
diff --git a/test/mithril.sync.js b/test/mithril.sync.js
new file mode 100644
index 00000000..48be4605
--- /dev/null
+++ b/test/mithril.sync.js
@@ -0,0 +1,56 @@
+describe("m.sync()", function () {
+ "use strict"
+
+ it("exists", function () {
+ expect(m.sync).to.be.a("function")
+ })
+
+ it("joins multiple promises in order to an array", 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")
+
+ expect(value).to.eql(["test", "foo"])
+ })
+
+ it("joins multiple promises out of order to an array", 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")
+
+ expect(value).to.eql(["test", "foo"])
+ })
+
+ // FIXME: bad behavior?
+ it("rejects to an array if one promise rejects", function () {
+ var value
+ var deferred = m.deferred()
+ m.sync([deferred.promise]).catch(function (data) { value = data })
+ deferred.reject("fail")
+ expect(value).to.eql(["fail"])
+ })
+
+ it("resolves immediately if given an empty array", function () {
+ var value = 1
+ m.sync([]).then(function () { value = 2 })
+ expect(value).to.equal(2)
+ })
+
+ it("resolves to an empty array if given an empty array", function () {
+ var value
+ m.sync([]).then(function (data) { value = data })
+ expect(value).to.eql([])
+ })
+})
diff --git a/test/mithril.trust.js b/test/mithril.trust.js
new file mode 100644
index 00000000..748fa75e
--- /dev/null
+++ b/test/mithril.trust.js
@@ -0,0 +1,58 @@
+describe("m.trust()", function () {
+ "use strict"
+
+ it("exists", function () {
+ expect(m.trust).to.be.a("function")
+ })
+
+ it("returns an instance of String", function () {
+ expect(m.trust("foo")).to.be.an.instanceof(String)
+ })
+
+ it("does not modify the string", function () {
+ expect(m.trust("foo").valueOf()).to.equal("foo")
+ })
+
+ it("is not identical to the string", function () {
+ expect(m.trust("foo")).to.not.equal("foo")
+ })
+
+ // FIXME: implement document.createRange().createContextualFragment() in the
+ // mock window for these tests
+ dom(function () {
+ it("isn't escaped in m.render()", function () {
+ var root = document.createElement("div")
+ m.render(root, m("div", "a", m.trust("&"), "b"))
+ expect(root.childNodes[0].innerHTML).to.equal("a&b")
+ })
+
+ it("works with mixed trusted content in div", function () {
+ var root = document.createElement("div")
+ m.render(root, [m.trust("
1
2
"), m("i", "foo")])
+ expect(root.childNodes[2].tagName).to.equal("I")
+ })
+
+ it("works with mixed trusted content in text nodes", function () {
+ var root = document.createElement("div")
+ m.render(root, [
+ m.trust("
1
123
2
"),
+ m("i", "foo")
+ ])
+ expect(root.childNodes[3].tagName).to.equal("I")
+ })
+
+ // FIXME: this is a bug (trusted string's contents rendered as just
+ // textual contents)
+ xit("works with mixed trusted content in td", function () {
+ var root = document.createElement("table")
+ root.appendChild(root = document.createElement("tr"))
+
+ m.render(root, [
+ m.trust("
1
2
"),
+ m("td", "foo")
+ ])
+
+ expect(root.childNodes[2].tagName).to.equal("td")
+ })
+ })
+})
diff --git a/test/mithril.withAttr.js b/test/mithril.withAttr.js
new file mode 100644
index 00000000..aae26656
--- /dev/null
+++ b/test/mithril.withAttr.js
@@ -0,0 +1,17 @@
+describe("m.withAttr()", function () {
+ "use strict"
+
+ it("calls the handler with the right value/context without callbackThis", function () { // eslint-disable-line
+ var spy = sinon.spy()
+ var object = {}
+ m.withAttr("test", spy).call(object, {currentTarget: {test: "foo"}})
+ expect(spy).to.be.calledOn(object).and.calledWith("foo")
+ })
+
+ it("calls the handler with the right value/context with callbackThis", function () { // eslint-disable-line
+ var spy = sinon.spy()
+ var object = {}
+ m.withAttr("test", spy, object)({currentTarget: {test: "foo"}})
+ expect(spy).to.be.calledOn(object).and.calledWith("foo")
+ })
+})
diff --git a/test/svg.html b/test/svg.html
new file mode 100644
index 00000000..322d1045
--- /dev/null
+++ b/test/svg.html
@@ -0,0 +1,214 @@
+
+SVG test
+
+
+
+ This page is for testing SVG support in Mithril. This page should contain:
+
+
+
an HTML link labeled "HTML link"
+
an SVG link labeled "SVG link"
+
a tilted blue square
+
a cat picture
+
an animated line drawing
+
a clock with the current time
+
+
+
+ The links should open in a new tab. All items should display title tooltips
+ when hovered over.
+
+
+
+
+
diff --git a/tests/e2e/libs/qunit.css b/tests/e2e/libs/qunit.css
deleted file mode 100644
index 2a6a02bf..00000000
--- a/tests/e2e/libs/qunit.css
+++ /dev/null
@@ -1,244 +0,0 @@
-/**
- * QUnit v1.12.0 - A JavaScript Unit Testing Framework
- *
- * http://qunitjs.com
- *
- * Copyright 2012 jQuery Foundation and other contributors
- * Released under the MIT license.
- * http://jquery.org/license
- */
-
-/** Font Family and Sizes */
-
-#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
- font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
-}
-
-#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
-#qunit-tests { font-size: smaller; }
-
-
-/** Resets */
-
-#qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
- margin: 0;
- padding: 0;
-}
-
-
-/** Header */
-
-#qunit-header {
- padding: 0.5em 0 0.5em 1em;
-
- color: #8699a4;
- background-color: #0d3349;
-
- font-size: 1.5em;
- line-height: 1em;
- font-weight: normal;
-
- border-radius: 5px 5px 0 0;
- -moz-border-radius: 5px 5px 0 0;
- -webkit-border-top-right-radius: 5px;
- -webkit-border-top-left-radius: 5px;
-}
-
-#qunit-header a {
- text-decoration: none;
- color: #c2ccd1;
-}
-
-#qunit-header a:hover,
-#qunit-header a:focus {
- color: #fff;
-}
-
-#qunit-testrunner-toolbar label {
- display: inline-block;
- padding: 0 .5em 0 .1em;
-}
-
-#qunit-banner {
- height: 5px;
-}
-
-#qunit-testrunner-toolbar {
- padding: 0.5em 0 0.5em 2em;
- color: #5E740B;
- background-color: #eee;
- overflow: hidden;
-}
-
-#qunit-userAgent {
- padding: 0.5em 0 0.5em 2.5em;
- background-color: #2b81af;
- color: #fff;
- text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
-}
-
-#qunit-modulefilter-container {
- float: right;
-}
-
-/** Tests: Pass/Fail */
-
-#qunit-tests {
- list-style-position: inside;
-}
-
-#qunit-tests li {
- padding: 0.4em 0.5em 0.4em 2.5em;
- border-bottom: 1px solid #fff;
- list-style-position: inside;
-}
-
-#qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running {
- display: none;
-}
-
-#qunit-tests li strong {
- cursor: pointer;
-}
-
-#qunit-tests li a {
- padding: 0.5em;
- color: #c2ccd1;
- text-decoration: none;
-}
-#qunit-tests li a:hover,
-#qunit-tests li a:focus {
- color: #000;
-}
-
-#qunit-tests li .runtime {
- float: right;
- font-size: smaller;
-}
-
-.qunit-assert-list {
- margin-top: 0.5em;
- padding: 0.5em;
-
- background-color: #fff;
-
- border-radius: 5px;
- -moz-border-radius: 5px;
- -webkit-border-radius: 5px;
-}
-
-.qunit-collapsed {
- display: none;
-}
-
-#qunit-tests table {
- border-collapse: collapse;
- margin-top: .2em;
-}
-
-#qunit-tests th {
- text-align: right;
- vertical-align: top;
- padding: 0 .5em 0 0;
-}
-
-#qunit-tests td {
- vertical-align: top;
-}
-
-#qunit-tests pre {
- margin: 0;
- white-space: pre-wrap;
- word-wrap: break-word;
-}
-
-#qunit-tests del {
- background-color: #e0f2be;
- color: #374e0c;
- text-decoration: none;
-}
-
-#qunit-tests ins {
- background-color: #ffcaca;
- color: #500;
- text-decoration: none;
-}
-
-/*** Test Counts */
-
-#qunit-tests b.counts { color: black; }
-#qunit-tests b.passed { color: #5E740B; }
-#qunit-tests b.failed { color: #710909; }
-
-#qunit-tests li li {
- padding: 5px;
- background-color: #fff;
- border-bottom: none;
- list-style-position: inside;
-}
-
-/*** Passing Styles */
-
-#qunit-tests li li.pass {
- color: #3c510c;
- background-color: #fff;
- border-left: 10px solid #C6E746;
-}
-
-#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
-#qunit-tests .pass .test-name { color: #366097; }
-
-#qunit-tests .pass .test-actual,
-#qunit-tests .pass .test-expected { color: #999999; }
-
-#qunit-banner.qunit-pass { background-color: #C6E746; }
-
-/*** Failing Styles */
-
-#qunit-tests li li.fail {
- color: #710909;
- background-color: #fff;
- border-left: 10px solid #EE5757;
- white-space: pre;
-}
-
-#qunit-tests > li:last-child {
- border-radius: 0 0 5px 5px;
- -moz-border-radius: 0 0 5px 5px;
- -webkit-border-bottom-right-radius: 5px;
- -webkit-border-bottom-left-radius: 5px;
-}
-
-#qunit-tests .fail { color: #000000; background-color: #EE5757; }
-#qunit-tests .fail .test-name,
-#qunit-tests .fail .module-name { color: #000000; }
-
-#qunit-tests .fail .test-actual { color: #EE5757; }
-#qunit-tests .fail .test-expected { color: green; }
-
-#qunit-banner.qunit-fail { background-color: #EE5757; }
-
-
-/** Result */
-
-#qunit-testresult {
- padding: 0.5em 0.5em 0.5em 2.5em;
-
- color: #2b81af;
- background-color: #D2E0E6;
-
- border-bottom: 1px solid white;
-}
-#qunit-testresult .module-name {
- font-weight: bold;
-}
-
-/** Fixture */
-
-#qunit-fixture {
- position: absolute;
- top: -10000px;
- left: -10000px;
- width: 1000px;
- height: 1000px;
-}
\ No newline at end of file
diff --git a/tests/e2e/libs/qunit.js b/tests/e2e/libs/qunit.js
deleted file mode 100644
index ba99f1ab..00000000
--- a/tests/e2e/libs/qunit.js
+++ /dev/null
@@ -1,2212 +0,0 @@
-/**
- * QUnit v1.12.0 - A JavaScript Unit Testing Framework
- *
- * http://qunitjs.com
- *
- * Copyright 2013 jQuery Foundation and other contributors
- * Released under the MIT license.
- * https://jquery.org/license/
- */
-
-(function( window ) {
-
-var QUnit,
- assert,
- config,
- onErrorFnPrev,
- testId = 0,
- fileName = (sourceFromStacktrace( 0 ) || "" ).replace(/(:\d+)+\)?/, "").replace(/.+\//, ""),
- toString = Object.prototype.toString,
- hasOwn = Object.prototype.hasOwnProperty,
- // Keep a local reference to Date (GH-283)
- Date = window.Date,
- setTimeout = window.setTimeout,
- defined = {
- setTimeout: typeof window.setTimeout !== "undefined",
- sessionStorage: (function() {
- var x = "qunit-test-string";
- try {
- sessionStorage.setItem( x, x );
- sessionStorage.removeItem( x );
- return true;
- } catch( e ) {
- return false;
- }
- }())
- },
- /**
- * Provides a normalized error string, correcting an issue
- * with IE 7 (and prior) where Error.prototype.toString is
- * not properly implemented
- *
- * Based on http://es5.github.com/#x15.11.4.4
- *
- * @param {String|Error} error
- * @return {String} error message
- */
- errorString = function( error ) {
- var name, message,
- errorString = error.toString();
- if ( errorString.substring( 0, 7 ) === "[object" ) {
- name = error.name ? error.name.toString() : "Error";
- message = error.message ? error.message.toString() : "";
- if ( name && message ) {
- return name + ": " + message;
- } else if ( name ) {
- return name;
- } else if ( message ) {
- return message;
- } else {
- return "Error";
- }
- } else {
- return errorString;
- }
- },
- /**
- * Makes a clone of an object using only Array or Object as base,
- * and copies over the own enumerable properties.
- *
- * @param {Object} obj
- * @return {Object} New object with only the own properties (recursively).
- */
- objectValues = function( obj ) {
- // Grunt 0.3.x uses an older version of jshint that still has jshint/jshint#392.
- /*jshint newcap: false */
- var key, val,
- vals = QUnit.is( "array", obj ) ? [] : {};
- for ( key in obj ) {
- if ( hasOwn.call( obj, key ) ) {
- val = obj[key];
- vals[key] = val === Object(val) ? objectValues(val) : val;
- }
- }
- return vals;
- };
-
-function Test( settings ) {
- extend( this, settings );
- this.assertions = [];
- this.testNumber = ++Test.count;
-}
-
-Test.count = 0;
-
-Test.prototype = {
- init: function() {
- var a, b, li,
- tests = id( "qunit-tests" );
-
- if ( tests ) {
- b = document.createElement( "strong" );
- b.innerHTML = this.nameHtml;
-
- // `a` initialized at top of scope
- a = document.createElement( "a" );
- a.innerHTML = "Rerun";
- a.href = QUnit.url({ testNumber: this.testNumber });
-
- li = document.createElement( "li" );
- li.appendChild( b );
- li.appendChild( a );
- li.className = "running";
- li.id = this.id = "qunit-test-output" + testId++;
-
- tests.appendChild( li );
- }
- },
- setup: function() {
- if (
- // Emit moduleStart when we're switching from one module to another
- this.module !== config.previousModule ||
- // They could be equal (both undefined) but if the previousModule property doesn't
- // yet exist it means this is the first test in a suite that isn't wrapped in a
- // module, in which case we'll just emit a moduleStart event for 'undefined'.
- // Without this, reporters can get testStart before moduleStart which is a problem.
- !hasOwn.call( config, "previousModule" )
- ) {
- if ( hasOwn.call( config, "previousModule" ) ) {
- runLoggingCallbacks( "moduleDone", QUnit, {
- name: config.previousModule,
- failed: config.moduleStats.bad,
- passed: config.moduleStats.all - config.moduleStats.bad,
- total: config.moduleStats.all
- });
- }
- config.previousModule = this.module;
- config.moduleStats = { all: 0, bad: 0 };
- runLoggingCallbacks( "moduleStart", QUnit, {
- name: this.module
- });
- }
-
- config.current = this;
-
- this.testEnvironment = extend({
- setup: function() {},
- teardown: function() {}
- }, this.moduleTestEnvironment );
-
- this.started = +new Date();
- runLoggingCallbacks( "testStart", QUnit, {
- name: this.testName,
- module: this.module
- });
-
- /*jshint camelcase:false */
-
-
- /**
- * Expose the current test environment.
- *
- * @deprecated since 1.12.0: Use QUnit.config.current.testEnvironment instead.
- */
- QUnit.current_testEnvironment = this.testEnvironment;
-
- /*jshint camelcase:true */
-
- if ( !config.pollution ) {
- saveGlobal();
- }
- if ( config.notrycatch ) {
- this.testEnvironment.setup.call( this.testEnvironment, QUnit.assert );
- return;
- }
- try {
- this.testEnvironment.setup.call( this.testEnvironment, QUnit.assert );
- } catch( e ) {
- QUnit.pushFailure( "Setup failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) );
- }
- },
- run: function() {
- config.current = this;
-
- var running = id( "qunit-testresult" );
-
- if ( running ) {
- running.innerHTML = "Running: " + this.nameHtml;
- }
-
- if ( this.async ) {
- QUnit.stop();
- }
-
- this.callbackStarted = +new Date();
-
- if ( config.notrycatch ) {
- this.callback.call( this.testEnvironment, QUnit.assert );
- this.callbackRuntime = +new Date() - this.callbackStarted;
- return;
- }
-
- try {
- this.callback.call( this.testEnvironment, QUnit.assert );
- this.callbackRuntime = +new Date() - this.callbackStarted;
- } catch( e ) {
- this.callbackRuntime = +new Date() - this.callbackStarted;
-
- QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + " " + this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) );
- // else next test will carry the responsibility
- saveGlobal();
-
- // Restart the tests if they're blocking
- if ( config.blocking ) {
- QUnit.start();
- }
- }
- },
- teardown: function() {
- config.current = this;
- if ( config.notrycatch ) {
- if ( typeof this.callbackRuntime === "undefined" ) {
- this.callbackRuntime = +new Date() - this.callbackStarted;
- }
- this.testEnvironment.teardown.call( this.testEnvironment, QUnit.assert );
- return;
- } else {
- try {
- this.testEnvironment.teardown.call( this.testEnvironment, QUnit.assert );
- } catch( e ) {
- QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) );
- }
- }
- checkPollution();
- },
- finish: function() {
- config.current = this;
- if ( config.requireExpects && this.expected === null ) {
- QUnit.pushFailure( "Expected number of assertions to be defined, but expect() was not called.", this.stack );
- } else if ( this.expected !== null && this.expected !== this.assertions.length ) {
- QUnit.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack );
- } else if ( this.expected === null && !this.assertions.length ) {
- QUnit.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.", this.stack );
- }
-
- var i, assertion, a, b, time, li, ol,
- test = this,
- good = 0,
- bad = 0,
- tests = id( "qunit-tests" );
-
- this.runtime = +new Date() - this.started;
- config.stats.all += this.assertions.length;
- config.moduleStats.all += this.assertions.length;
-
- if ( tests ) {
- ol = document.createElement( "ol" );
- ol.className = "qunit-assert-list";
-
- for ( i = 0; i < this.assertions.length; i++ ) {
- assertion = this.assertions[i];
-
- li = document.createElement( "li" );
- li.className = assertion.result ? "pass" : "fail";
- li.innerHTML = assertion.message || ( assertion.result ? "okay" : "failed" );
- ol.appendChild( li );
-
- if ( assertion.result ) {
- good++;
- } else {
- bad++;
- config.stats.bad++;
- config.moduleStats.bad++;
- }
- }
-
- // store result when possible
- if ( QUnit.config.reorder && defined.sessionStorage ) {
- if ( bad ) {
- sessionStorage.setItem( "qunit-test-" + this.module + "-" + this.testName, bad );
- } else {
- sessionStorage.removeItem( "qunit-test-" + this.module + "-" + this.testName );
- }
- }
-
- if ( bad === 0 ) {
- addClass( ol, "qunit-collapsed" );
- }
-
- // `b` initialized at top of scope
- b = document.createElement( "strong" );
- b.innerHTML = this.nameHtml + " (" + bad + ", " + good + ", " + this.assertions.length + ")";
-
- addEvent(b, "click", function() {
- var next = b.parentNode.lastChild,
- collapsed = hasClass( next, "qunit-collapsed" );
- ( collapsed ? removeClass : addClass )( next, "qunit-collapsed" );
- });
-
- addEvent(b, "dblclick", function( e ) {
- var target = e && e.target ? e.target : window.event.srcElement;
- if ( target.nodeName.toLowerCase() === "span" || target.nodeName.toLowerCase() === "b" ) {
- target = target.parentNode;
- }
- if ( window.location && target.nodeName.toLowerCase() === "strong" ) {
- window.location = QUnit.url({ testNumber: test.testNumber });
- }
- });
-
- // `time` initialized at top of scope
- time = document.createElement( "span" );
- time.className = "runtime";
- time.innerHTML = this.runtime + " ms";
-
- // `li` initialized at top of scope
- li = id( this.id );
- li.className = bad ? "fail" : "pass";
- li.removeChild( li.firstChild );
- a = li.firstChild;
- li.appendChild( b );
- li.appendChild( a );
- li.appendChild( time );
- li.appendChild( ol );
-
- } else {
- for ( i = 0; i < this.assertions.length; i++ ) {
- if ( !this.assertions[i].result ) {
- bad++;
- config.stats.bad++;
- config.moduleStats.bad++;
- }
- }
- }
-
- runLoggingCallbacks( "testDone", QUnit, {
- name: this.testName,
- module: this.module,
- failed: bad,
- passed: this.assertions.length - bad,
- total: this.assertions.length,
- duration: this.runtime
- });
-
- QUnit.reset();
-
- config.current = undefined;
- },
-
- queue: function() {
- var bad,
- test = this;
-
- synchronize(function() {
- test.init();
- });
- function run() {
- // each of these can by async
- synchronize(function() {
- test.setup();
- });
- synchronize(function() {
- test.run();
- });
- synchronize(function() {
- test.teardown();
- });
- synchronize(function() {
- test.finish();
- });
- }
-
- // `bad` initialized at top of scope
- // defer when previous test run passed, if storage is available
- bad = QUnit.config.reorder && defined.sessionStorage &&
- +sessionStorage.getItem( "qunit-test-" + this.module + "-" + this.testName );
-
- if ( bad ) {
- run();
- } else {
- synchronize( run, true );
- }
- }
-};
-
-// Root QUnit object.
-// `QUnit` initialized at top of scope
-QUnit = {
-
- // call on start of module test to prepend name to all tests
- module: function( name, testEnvironment ) {
- config.currentModule = name;
- config.currentModuleTestEnvironment = testEnvironment;
- config.modules[name] = true;
- },
-
- asyncTest: function( testName, expected, callback ) {
- if ( arguments.length === 2 ) {
- callback = expected;
- expected = null;
- }
-
- QUnit.test( testName, expected, callback, true );
- },
-
- test: function( testName, expected, callback, async ) {
- var test,
- nameHtml = "" + escapeText( testName ) + "";
-
- if ( arguments.length === 2 ) {
- callback = expected;
- expected = null;
- }
-
- if ( config.currentModule ) {
- nameHtml = "" + escapeText( config.currentModule ) + ": " + nameHtml;
- }
-
- test = new Test({
- nameHtml: nameHtml,
- testName: testName,
- expected: expected,
- async: async,
- callback: callback,
- module: config.currentModule,
- moduleTestEnvironment: config.currentModuleTestEnvironment,
- stack: sourceFromStacktrace( 2 )
- });
-
- if ( !validTest( test ) ) {
- return;
- }
-
- test.queue();
- },
-
- // Specify the number of expected assertions to guarantee that failed test (no assertions are run at all) don't slip through.
- expect: function( asserts ) {
- if (arguments.length === 1) {
- config.current.expected = asserts;
- } else {
- return config.current.expected;
- }
- },
-
- start: function( count ) {
- // QUnit hasn't been initialized yet.
- // Note: RequireJS (et al) may delay onLoad
- if ( config.semaphore === undefined ) {
- QUnit.begin(function() {
- // This is triggered at the top of QUnit.load, push start() to the event loop, to allow QUnit.load to finish first
- setTimeout(function() {
- QUnit.start( count );
- });
- });
- return;
- }
-
- config.semaphore -= count || 1;
- // don't start until equal number of stop-calls
- if ( config.semaphore > 0 ) {
- return;
- }
- // ignore if start is called more often then stop
- if ( config.semaphore < 0 ) {
- config.semaphore = 0;
- QUnit.pushFailure( "Called start() while already started (QUnit.config.semaphore was 0 already)", null, sourceFromStacktrace(2) );
- return;
- }
- // A slight delay, to avoid any current callbacks
- if ( defined.setTimeout ) {
- setTimeout(function() {
- if ( config.semaphore > 0 ) {
- return;
- }
- if ( config.timeout ) {
- clearTimeout( config.timeout );
- }
-
- config.blocking = false;
- process( true );
- }, 13);
- } else {
- config.blocking = false;
- process( true );
- }
- },
-
- stop: function( count ) {
- config.semaphore += count || 1;
- config.blocking = true;
-
- if ( config.testTimeout && defined.setTimeout ) {
- clearTimeout( config.timeout );
- config.timeout = setTimeout(function() {
- QUnit.ok( false, "Test timed out" );
- config.semaphore = 1;
- QUnit.start();
- }, config.testTimeout );
- }
- }
-};
-
-// `assert` initialized at top of scope
-// Assert helpers
-// All of these must either call QUnit.push() or manually do:
-// - runLoggingCallbacks( "log", .. );
-// - config.current.assertions.push({ .. });
-// We attach it to the QUnit object *after* we expose the public API,
-// otherwise `assert` will become a global variable in browsers (#341).
-assert = {
- /**
- * Asserts rough true-ish result.
- * @name ok
- * @function
- * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" );
- */
- ok: function( result, msg ) {
- if ( !config.current ) {
- throw new Error( "ok() assertion outside test context, was " + sourceFromStacktrace(2) );
- }
- result = !!result;
- msg = msg || (result ? "okay" : "failed" );
-
- var source,
- details = {
- module: config.current.module,
- name: config.current.testName,
- result: result,
- message: msg
- };
-
- msg = "" + escapeText( msg ) + "";
-
- if ( !result ) {
- source = sourceFromStacktrace( 2 );
- if ( source ) {
- details.source = source;
- msg += "
Source:
" + escapeText( source ) + "
";
- }
- }
- runLoggingCallbacks( "log", QUnit, details );
- config.current.assertions.push({
- result: result,
- message: msg
- });
- },
-
- /**
- * Assert that the first two arguments are equal, with an optional message.
- * Prints out both actual and expected values.
- * @name equal
- * @function
- * @example equal( format( "Received {0} bytes.", 2), "Received 2 bytes.", "format() replaces {0} with next argument" );
- */
- equal: function( actual, expected, message ) {
- /*jshint eqeqeq:false */
- QUnit.push( expected == actual, actual, expected, message );
- },
-
- /**
- * @name notEqual
- * @function
- */
- notEqual: function( actual, expected, message ) {
- /*jshint eqeqeq:false */
- QUnit.push( expected != actual, actual, expected, message );
- },
-
- /**
- * @name propEqual
- * @function
- */
- propEqual: function( actual, expected, message ) {
- actual = objectValues(actual);
- expected = objectValues(expected);
- QUnit.push( QUnit.equiv(actual, expected), actual, expected, message );
- },
-
- /**
- * @name notPropEqual
- * @function
- */
- notPropEqual: function( actual, expected, message ) {
- actual = objectValues(actual);
- expected = objectValues(expected);
- QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message );
- },
-
- /**
- * @name deepEqual
- * @function
- */
- deepEqual: function( actual, expected, message ) {
- QUnit.push( QUnit.equiv(actual, expected), actual, expected, message );
- },
-
- /**
- * @name notDeepEqual
- * @function
- */
- notDeepEqual: function( actual, expected, message ) {
- QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message );
- },
-
- /**
- * @name strictEqual
- * @function
- */
- strictEqual: function( actual, expected, message ) {
- QUnit.push( expected === actual, actual, expected, message );
- },
-
- /**
- * @name notStrictEqual
- * @function
- */
- notStrictEqual: function( actual, expected, message ) {
- QUnit.push( expected !== actual, actual, expected, message );
- },
-
- "throws": function( block, expected, message ) {
- var actual,
- expectedOutput = expected,
- ok = false;
-
- // 'expected' is optional
- if ( typeof expected === "string" ) {
- message = expected;
- expected = null;
- }
-
- config.current.ignoreGlobalErrors = true;
- try {
- block.call( config.current.testEnvironment );
- } catch (e) {
- actual = e;
- }
- config.current.ignoreGlobalErrors = false;
-
- if ( actual ) {
- // we don't want to validate thrown error
- if ( !expected ) {
- ok = true;
- expectedOutput = null;
- // expected is a regexp
- } else if ( QUnit.objectType( expected ) === "regexp" ) {
- ok = expected.test( errorString( actual ) );
- // expected is a constructor
- } else if ( actual instanceof expected ) {
- ok = true;
- // expected is a validation function which returns true is validation passed
- } else if ( expected.call( {}, actual ) === true ) {
- expectedOutput = null;
- ok = true;
- }
-
- QUnit.push( ok, actual, expectedOutput, message );
- } else {
- QUnit.pushFailure( message, null, "No exception was thrown." );
- }
- }
-};
-
-/**
- * @deprecated since 1.8.0
- * Kept assertion helpers in root for backwards compatibility.
- */
-extend( QUnit, assert );
-
-/**
- * @deprecated since 1.9.0
- * Kept root "raises()" for backwards compatibility.
- * (Note that we don't introduce assert.raises).
- */
-QUnit.raises = assert[ "throws" ];
-
-/**
- * @deprecated since 1.0.0, replaced with error pushes since 1.3.0
- * Kept to avoid TypeErrors for undefined methods.
- */
-QUnit.equals = function() {
- QUnit.push( false, false, false, "QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead" );
-};
-QUnit.same = function() {
- QUnit.push( false, false, false, "QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead" );
-};
-
-// We want access to the constructor's prototype
-(function() {
- function F() {}
- F.prototype = QUnit;
- QUnit = new F();
- // Make F QUnit's constructor so that we can add to the prototype later
- QUnit.constructor = F;
-}());
-
-/**
- * Config object: Maintain internal state
- * Later exposed as QUnit.config
- * `config` initialized at top of scope
- */
-config = {
- // The queue of tests to run
- queue: [],
-
- // block until document ready
- blocking: true,
-
- // when enabled, show only failing tests
- // gets persisted through sessionStorage and can be changed in UI via checkbox
- hidepassed: false,
-
- // by default, run previously failed tests first
- // very useful in combination with "Hide passed tests" checked
- reorder: true,
-
- // by default, modify document.title when suite is done
- altertitle: true,
-
- // when enabled, all tests must call expect()
- requireExpects: false,
-
- // add checkboxes that are persisted in the query-string
- // when enabled, the id is set to `true` as a `QUnit.config` property
- urlConfig: [
- {
- id: "noglobals",
- label: "Check for Globals",
- tooltip: "Enabling this will test if any test introduces new properties on the `window` object. Stored as query-strings."
- },
- {
- id: "notrycatch",
- label: "No try-catch",
- tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging exceptions in IE reasonable. Stored as query-strings."
- }
- ],
-
- // Set of all modules.
- modules: {},
-
- // logging callback queues
- begin: [],
- done: [],
- log: [],
- testStart: [],
- testDone: [],
- moduleStart: [],
- moduleDone: []
-};
-
-// Export global variables, unless an 'exports' object exists,
-// in that case we assume we're in CommonJS (dealt with on the bottom of the script)
-if ( typeof exports === "undefined" ) {
- extend( window, QUnit.constructor.prototype );
-
- // Expose QUnit object
- window.QUnit = QUnit;
-}
-
-// Initialize more QUnit.config and QUnit.urlParams
-(function() {
- var i,
- location = window.location || { search: "", protocol: "file:" },
- params = location.search.slice( 1 ).split( "&" ),
- length = params.length,
- urlParams = {},
- current;
-
- if ( params[ 0 ] ) {
- for ( i = 0; i < length; i++ ) {
- current = params[ i ].split( "=" );
- current[ 0 ] = decodeURIComponent( current[ 0 ] );
- // allow just a key to turn on a flag, e.g., test.html?noglobals
- current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true;
- urlParams[ current[ 0 ] ] = current[ 1 ];
- }
- }
-
- QUnit.urlParams = urlParams;
-
- // String search anywhere in moduleName+testName
- config.filter = urlParams.filter;
-
- // Exact match of the module name
- config.module = urlParams.module;
-
- config.testNumber = parseInt( urlParams.testNumber, 10 ) || null;
-
- // Figure out if we're running the tests from a server or not
- QUnit.isLocal = location.protocol === "file:";
-}());
-
-// Extend QUnit object,
-// these after set here because they should not be exposed as global functions
-extend( QUnit, {
- assert: assert,
-
- config: config,
-
- // Initialize the configuration options
- init: function() {
- extend( config, {
- stats: { all: 0, bad: 0 },
- moduleStats: { all: 0, bad: 0 },
- started: +new Date(),
- updateRate: 1000,
- blocking: false,
- autostart: true,
- autorun: false,
- filter: "",
- queue: [],
- semaphore: 1
- });
-
- var tests, banner, result,
- qunit = id( "qunit" );
-
- if ( qunit ) {
- qunit.innerHTML =
- "
" + escapeText( document.title ) + "
" +
- "" +
- "" +
- "" +
- "";
- }
-
- tests = id( "qunit-tests" );
- banner = id( "qunit-banner" );
- result = id( "qunit-testresult" );
-
- if ( tests ) {
- tests.innerHTML = "";
- }
-
- if ( banner ) {
- banner.className = "";
- }
-
- if ( result ) {
- result.parentNode.removeChild( result );
- }
-
- if ( tests ) {
- result = document.createElement( "p" );
- result.id = "qunit-testresult";
- result.className = "result";
- tests.parentNode.insertBefore( result, tests );
- result.innerHTML = "Running... ";
- }
- },
-
- // Resets the test setup. Useful for tests that modify the DOM.
- /*
- DEPRECATED: Use multiple tests instead of resetting inside a test.
- Use testStart or testDone for custom cleanup.
- This method will throw an error in 2.0, and will be removed in 2.1
- */
- reset: function() {
- var fixture = id( "qunit-fixture" );
- if ( fixture ) {
- fixture.innerHTML = config.fixture;
- }
- },
-
- // Trigger an event on an element.
- // @example triggerEvent( document.body, "click" );
- triggerEvent: function( elem, type, event ) {
- if ( document.createEvent ) {
- event = document.createEvent( "MouseEvents" );
- event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView,
- 0, 0, 0, 0, 0, false, false, false, false, 0, null);
-
- elem.dispatchEvent( event );
- } else if ( elem.fireEvent ) {
- elem.fireEvent( "on" + type );
- }
- },
-
- // Safe object type checking
- is: function( type, obj ) {
- return QUnit.objectType( obj ) === type;
- },
-
- objectType: function( obj ) {
- if ( typeof obj === "undefined" ) {
- return "undefined";
- // consider: typeof null === object
- }
- if ( obj === null ) {
- return "null";
- }
-
- var match = toString.call( obj ).match(/^\[object\s(.*)\]$/),
- type = match && match[1] || "";
-
- switch ( type ) {
- case "Number":
- if ( isNaN(obj) ) {
- return "nan";
- }
- return "number";
- case "String":
- case "Boolean":
- case "Array":
- case "Date":
- case "RegExp":
- case "Function":
- return type.toLowerCase();
- }
- if ( typeof obj === "object" ) {
- return "object";
- }
- return undefined;
- },
-
- push: function( result, actual, expected, message ) {
- if ( !config.current ) {
- throw new Error( "assertion outside test context, was " + sourceFromStacktrace() );
- }
-
- var output, source,
- details = {
- module: config.current.module,
- name: config.current.testName,
- result: result,
- message: message,
- actual: actual,
- expected: expected
- };
-
- message = escapeText( message ) || ( result ? "okay" : "failed" );
- message = "" + message + "";
- output = message;
-
- if ( !result ) {
- expected = escapeText( QUnit.jsDump.parse(expected) );
- actual = escapeText( QUnit.jsDump.parse(actual) );
- output += "
- * The following clicks an input element with id='description'
- * and then types 'Hello World'.
- *
- @codestart
- Syn.click({},'description')
- .type("Hello World")
- @codeend
- *
User Actions and Events
- *
Syn is typically used to simulate user actions as opposed to triggering events. Typing characters
- * is an example of a user action. The keypress that represents an 'a'
- * character being typed is an example of an event.
- *
- *
- * While triggering events is supported, it's much more useful to simulate actual user behavior. The
- * following actions are supported by Syn:
- *
- *
- *
[Syn.prototype.click click] - a mousedown, focus, mouseup, and click.
- *
[Syn.prototype.dblclick dblclick] - two click! events followed by a dblclick.
- *
[Syn.prototype.key key] - types a single character (keydown, keypress, keyup).
- *
[Syn.prototype.type type] - types multiple characters into an element.
- *
[Syn.prototype.move move] - moves the mouse from one position to another (triggering mouseover / mouseouts).
- *
[Syn.prototype.drag drag] - a mousedown, followed by mousemoves, and a mouseup.
- *
- * All actions run asynchronously.
- * Click on the links above for more
- * information on how to use the specific action.
- *
Asynchronous Callbacks
- * Actions don't complete immediately. This is almost
- * entirely because focus()
- * doesn't run immediately in IE.
- * If you provide a callback function to Syn, it will
- * be called after the action is completed.
- * The following checks that "Hello World" was entered correctly:
- @codestart
- Syn.click({},'description')
- .type("Hello World", function(){
-
- ok("Hello World" == document.getElementById('description').value)
- })
- @codeend
-
Asynchronous Chaining
-
You might have noticed the [Syn.prototype.then then] method. It provides chaining
- so you can do a sequence of events with a single (final) callback.
-
- If an element isn't provided to then, it uses the previous Syn's element.
-
- The following does a lot of stuff before checking the result:
- @codestart
- Syn.type('ice water','title')
- .type('ice and water','description')
- .click({},'create')
- .drag({to: 'favorites'},'newRecipe',
- function(){
- ok($('#newRecipe').parents('#favorites').length);
- })
- @codeend
-
-
jQuery Helper
- If jQuery is present, Syn adds a triggerSyn helper you can use like:
- @codestart
- $("#description").triggerSyn("type","Hello World");
- @codeend
- *
Key Event Recording
- *
Every browser has very different rules for dispatching key events.
- * As there is no way to feature detect how a browser handles key events,
- * synthetic uses a description of how the browser behaves generated
- * by a recording application.
- *
- * If you want to support a browser not currently supported, you can
- * record that browser's key event description and add it to
- * Syn.key.browsers by it's navigator agent.
- *
- * Syn fully supports IE 6+, FF 3+, Chrome, Safari, Opera 10+.
- * With FF 1+, drag / move events are only partially supported. They will
- * not trigger mouseover / mouseout events.
- * Safari crashes when a mousedown is triggered on a select. Syn will not
- * create this event.
- *
Contributing to Syn
- * Have we missed something? We happily accept patches. The following are
- * important objects and properties of Syn:
- *
- *
Syn.create - contains methods to setup, convert options, and create an event of a specific type.
- *
Syn.defaults - default behavior by event type (except for keys).
- *
Syn.key.defaults - default behavior by key.
- *
Syn.keycodes - supported keys you can type.
- *
- *
Roll Your Own Functional Test Framework
- *
Syn is really the foundation of JavaScriptMVC's functional testing framework - [FuncUnit].
- * But, we've purposely made Syn work without any dependencies in the hopes that other frameworks or
- * testing solutions can use it as well.
- *
- * @constructor
- * @signature `Syn(type, options, element, callback)`
- * Creates a synthetic event on the element.
- * @param {Object} type
- * @param {Object} options
- * @param {Object} element
- * @param {Object} callback
- * @return {Syn} returns the Syn object.
- */
- Syn = function (type, options, element, callback) {
- return (new Syn.init(type, options, element, callback));
- };
-
- Syn.config = opts;
-
- // helper for supporting IE8 and below:
- // focus will throw in some circumnstances, like element being invisible
- Syn.__tryFocus = function tryFocus(element) {
- try {
- element.focus();
- } catch (e) {}
- };
-
- bind = function (el, ev, f) {
- return el.addEventListener ? el.addEventListener(ev, f, false) : el.attachEvent("on" + ev, f);
- };
- unbind = function (el, ev, f) {
- return el.addEventListener ? el.removeEventListener(ev, f, false) : el.detachEvent("on" + ev, f);
- };
-
- schedule = Syn.config.schedule || function (fn, ms) {
- setTimeout(fn, ms);
- };
- /**
- * @Static
- */
- extend(Syn, {
- /**
- * Creates a new synthetic event instance
- * @hide
- * @param {Object} type
- * @param {Object} options
- * @param {Object} element
- * @param {Object} callback
- */
- init: function (type, options, element, callback) {
- var args = Syn.args(options, element, callback),
- self = this;
- this.queue = [];
- this.element = args.element;
-
- //run event
- if (typeof this[type] === "function") {
- this[type](args.options, args.element, function (defaults, el) {
- if (args.callback) {
- args.callback.apply(self, arguments);
- }
- self.done.apply(self, arguments);
- });
- } else {
- this.result = Syn.trigger(type, args.options, args.element);
- if (args.callback) {
- args.callback.call(this, args.element, this.result);
- }
- }
- },
- jquery: function (el, fast) {
- if (window.FuncUnit && window.FuncUnit.jQuery) {
- return window.FuncUnit.jQuery;
- }
- if (el) {
- return Syn.helpers.getWindow(el)
- .jQuery || window.jQuery;
- } else {
- return window.jQuery;
- }
- },
- /**
- * Returns an object with the args for a Syn.
- * @hide
- * @return {Object}
- */
- args: function () {
- var res = {},
- i = 0;
- for (; i < arguments.length; i++) {
- if (typeof arguments[i] === 'function') {
- res.callback = arguments[i];
- } else if (arguments[i] && arguments[i].jquery) {
- res.element = arguments[i][0];
- } else if (arguments[i] && arguments[i].nodeName) {
- res.element = arguments[i];
- } else if (res.options && typeof arguments[i] === 'string') { //we can get by id
- res.element = document.getElementById(arguments[i]);
- } else if (arguments[i]) {
- res.options = arguments[i];
- }
- }
- return res;
- },
- click: function (options, element, callback) {
- Syn('click!', options, element, callback);
- },
- /**
- * @hide
- * @attribute defaults
- * Default actions for events. Each default function is called with this as its
- * element. It should return true if a timeout
- * should happen after it. If it returns an element, a timeout will happen
- * and the next event will happen on that element.
- */
- defaults: {
- focus: function focus() {
- if (!Syn.support.focusChanges) {
- var element = this,
- nodeName = element.nodeName.toLowerCase();
- Syn.data(element, "syntheticvalue", element.value);
-
- //TODO, this should be textarea too
- //and this might be for only text style inputs ... hmmmmm ....
- if (nodeName === "input" || nodeName === "textarea") {
- bind(element, "blur", function () {
- if (Syn.data(element, "syntheticvalue") !== element.value) {
-
- Syn.trigger("change", {}, element);
- }
- unbind(element, "blur", focus);
- });
-
- }
- }
- },
- submit: function () {
- Syn.onParents(this, function (el) {
- if (el.nodeName.toLowerCase() === 'form') {
- el.submit();
- return false;
- }
- });
- }
- },
- changeOnBlur: function (element, prop, value) {
-
- bind(element, "blur", function onblur() {
- if (value !== element[prop]) {
- Syn.trigger("change", {}, element);
- }
- unbind(element, "blur", onblur);
- });
-
- },
- /**
- * Returns the closest element of a particular type.
- * @hide
- * @param {Object} el
- * @param {Object} type
- */
- closest: function (el, type) {
- while (el && el.nodeName.toLowerCase() !== type.toLowerCase()) {
- el = el.parentNode;
- }
- return el;
- },
- /**
- * adds jQuery like data (adds an expando) and data exists FOREVER :)
- * @hide
- * @param {Object} el
- * @param {Object} key
- * @param {Object} value
- */
- data: function (el, key, value) {
- var d;
- if (!el[expando]) {
- el[expando] = id++;
- }
- if (!data[el[expando]]) {
- data[el[expando]] = {};
- }
- d = data[el[expando]];
- if (value) {
- data[el[expando]][key] = value;
- } else {
- return data[el[expando]][key];
- }
- },
- /**
- * Calls a function on the element and all parents of the element until the function returns
- * false.
- * @hide
- * @param {Object} el
- * @param {Object} func
- */
- onParents: function (el, func) {
- var res;
- while (el && res !== false) {
- res = func(el);
- el = el.parentNode;
- }
- return el;
- },
- //regex to match focusable elements
- focusable: /^(a|area|frame|iframe|label|input|select|textarea|button|html|object)$/i,
- /**
- * Returns if an element is focusable
- * @hide
- * @param {Object} elem
- */
- isFocusable: function (elem) {
- var attributeNode;
-
- // IE8 Standards doesn't like this on some elements
- if (elem.getAttributeNode) {
- attributeNode = elem.getAttributeNode("tabIndex");
- }
-
- return this.focusable.test(elem.nodeName) ||
- (attributeNode && attributeNode.specified) &&
- Syn.isVisible(elem);
- },
- /**
- * Returns if an element is visible or not
- * @hide
- * @param {Object} elem
- */
- isVisible: function (elem) {
- return (elem.offsetWidth && elem.offsetHeight) || (elem.clientWidth && elem.clientHeight);
- },
- /**
- * Gets the tabIndex as a number or null
- * @hide
- * @param {Object} elem
- */
- tabIndex: function (elem) {
- var attributeNode = elem.getAttributeNode("tabIndex");
- return attributeNode && attributeNode.specified && (parseInt(elem.getAttribute('tabIndex')) || 0);
- },
- bind: bind,
- unbind: unbind,
- /**
- * @function Syn.schedule schedule()
- * @param {Function} fn Function to be ran
- * @param {Number} ms Milliseconds to way before calling fn
- * @signature `Syn.schedule(fn, ms)`
- * @parent config
- *
- * Schedules a function to be ran later.
- * Must be registered prior to Syn loading, otherwise `setTimeout` will be
- * used as the scheduler.
- * @codestart
- * Syn = {
- * schedule: function(fn, ms) {
- * Platform.run.later(fn, ms);
- * }
- * };
- * @codeend
- */
- schedule: schedule,
- browser: browser,
- //some generic helpers
- helpers: {
- createEventObject: createEventObject,
- createBasicStandardEvent: function (type, defaults, doc) {
- var event;
- try {
- event = doc.createEvent("Events");
- } catch (e2) {
- event = doc.createEvent("UIEvents");
- } finally {
- event.initEvent(type, true, true);
- extend(event, defaults);
- }
- return event;
- },
- inArray: function (item, array) {
- var i = 0;
- for (; i < array.length; i++) {
- if (array[i] === item) {
- return i;
- }
- }
- return -1;
- },
- getWindow: function (element) {
- if (element.ownerDocument) {
- return element.ownerDocument.defaultView || element.ownerDocument.parentWindow;
- }
- },
- extend: extend,
- scrollOffset: function (win, set) {
- var doc = win.document.documentElement,
- body = win.document.body;
- if (set) {
- window.scrollTo(set.left, set.top);
-
- } else {
- return {
- left: (doc && doc.scrollLeft || body && body.scrollLeft || 0) + (doc.clientLeft || 0),
- top: (doc && doc.scrollTop || body && body.scrollTop || 0) + (doc.clientTop || 0)
- };
- }
-
- },
- scrollDimensions: function (win) {
- var doc = win.document.documentElement,
- body = win.document.body,
- docWidth = doc.clientWidth,
- docHeight = doc.clientHeight,
- compat = win.document.compatMode === "CSS1Compat";
-
- return {
- height: compat && docHeight ||
- body.clientHeight || docHeight,
- width: compat && docWidth ||
- body.clientWidth || docWidth
- };
- },
- addOffset: function (options, el) {
- var jq = Syn.jquery(el),
- off;
- if (typeof options === 'object' && options.clientX === undefined && options.clientY === undefined && options.pageX === undefined && options.pageY === undefined && jq) {
- el = jq(el);
- off = el.offset();
- options.pageX = off.left + el.width() / 2;
- options.pageY = off.top + el.height() / 2;
- }
- }
- },
- // place for key data
- key: {
- ctrlKey: null,
- altKey: null,
- shiftKey: null,
- metaKey: null
- },
- //triggers an event on an element, returns true if default events should be run
- /**
- * Dispatches an event and returns true if default events should be run.
- * @hide
- * @param {Object} event
- * @param {Object} element
- * @param {Object} type
- * @param {Object} autoPrevent
- */
- dispatch: function (event, element, type, autoPrevent) {
-
- // dispatchEvent doesn't always work in IE (mostly in a popup)
- if (element.dispatchEvent && event) {
- var preventDefault = event.preventDefault,
- prevents = autoPrevent ? -1 : 0;
-
- //automatically prevents the default behavior for this event
- //this is to protect agianst nasty browser freezing bug in safari
- if (autoPrevent) {
- bind(element, type, function ontype(ev) {
- ev.preventDefault();
- unbind(this, type, ontype);
- });
- }
-
- event.preventDefault = function () {
- prevents++;
- if (++prevents > 0) {
- preventDefault.apply(this, []);
- }
- };
- element.dispatchEvent(event);
- return prevents <= 0;
- } else {
- try {
- window.event = event;
- } catch (e) {}
- //source element makes sure element is still in the document
- return element.sourceIndex <= 0 || (element.fireEvent && element.fireEvent('on' + type, event));
- }
- },
- /**
- * @attribute
- * @hide
- * An object of eventType -> function that create that event.
- */
- create: {
- //-------- PAGE EVENTS ---------------------
- page: {
- event: function (type, options, element) {
- var doc = Syn.helpers.getWindow(element)
- .document || document,
- event;
- if (doc.createEvent) {
- event = doc.createEvent("Events");
-
- event.initEvent(type, true, true);
- return event;
- } else {
- try {
- event = createEventObject(type, options, element);
- } catch (e) {}
- return event;
- }
- }
- },
- // unique events
- focus: {
- event: function (type, options, element) {
- Syn.onParents(element, function (el) {
- if (Syn.isFocusable(el)) {
- if (el.nodeName.toLowerCase() !== 'html') {
- Syn.__tryFocus(el);
- activeElement = el;
- } else if (activeElement) {
- // TODO: The HTML element isn't focasable in IE, but it is
- // in FF. We should detect this and do a true focus instead
- // of just a blur
- var doc = Syn.helpers.getWindow(element)
- .document;
- if (doc !== window.document) {
- return false;
- } else if (doc.activeElement) {
- doc.activeElement.blur();
- activeElement = null;
- } else {
- activeElement.blur();
- activeElement = null;
- }
-
- }
- return false;
- }
- });
- return true;
- }
- }
- },
- /**
- * @attribute support
- * @hide
- *
- * Feature detected properties of a browser's event system.
- * Support has the following properties:
- *
- * - `backspaceWorks` - typing a backspace removes a character
- * - `clickChanges` - clicking on an option element creates a change event.
- * - `clickSubmits` - clicking on a form button submits the form.
- * - `focusChanges` - focus/blur creates a change event.
- * - `keypressOnAnchorClicks` - Keying enter on an anchor triggers a click.
- * - `keypressSubmits` - enter key submits
- * - `keyCharacters` - typing a character shows up
- * - `keysOnNotFocused` - enters keys when not focused.
- * - `linkHrefJS` - An achor's href JavaScript is run.
- * - `mouseDownUpClicks` - A mousedown followed by mouseup creates a click event.
- * - `mouseupSubmits` - a mouseup on a form button submits the form.
- * - `radioClickChanges` - clicking a radio button changes the radio.
- * - `tabKeyTabs` - A tab key changes tabs.
- * - `textareaCarriage` - a new line in a textarea creates a carriage return.
- *
- *
- */
- support: {
- clickChanges: false,
- clickSubmits: false,
- keypressSubmits: false,
- mouseupSubmits: false,
- radioClickChanges: false,
- focusChanges: false,
- linkHrefJS: false,
- keyCharacters: false,
- backspaceWorks: false,
- mouseDownUpClicks: false,
- tabKeyTabs: false,
- keypressOnAnchorClicks: false,
- optionClickBubbles: false,
- ready: 0
- },
- /**
- * @function Syn.trigger trigger()
- * @parent actions
- * @signature `Syn.trigger(type, options, element)`
- * Creates a synthetic event and dispatches it on the element.
- * This will run any default actions for the element.
- * Typically you want to use Syn, but if you want the return value, use this.
- * @param {String} type
- * @param {Object} options
- * @param {HTMLElement} element
- * @return {Boolean} true if default events were run, false if otherwise.
- */
- trigger: function (type, options, element) {
- if (!options) {
- options = {};
- }
-
- var create = Syn.create,
- setup = create[type] && create[type].setup,
- kind = key.test(type) ? 'key' : (page.test(type) ? "page" : "mouse"),
- createType = create[type] || {},
- createKind = create[kind],
- event, ret, autoPrevent, dispatchEl = element;
-
- //any setup code?
- if (Syn.support.ready === 2 && setup) {
- setup(type, options, element);
- }
-
- autoPrevent = options._autoPrevent;
- //get kind
- delete options._autoPrevent;
-
- if (createType.event) {
- ret = createType.event(type, options, element);
- } else {
- //convert options
- options = createKind.options ? createKind.options(type, options, element) : options;
-
- if (!Syn.support.changeBubbles && /option/i.test(element.nodeName)) {
- dispatchEl = element.parentNode; //jQuery expects clicks on select
- }
-
- //create the event
- event = createKind.event(type, options, dispatchEl);
-
- //send the event
- ret = Syn.dispatch(event, dispatchEl, type, autoPrevent);
- }
-
- if (ret && Syn.support.ready === 2 && Syn.defaults[type]) {
- Syn.defaults[type].call(element, options, autoPrevent);
- }
- return ret;
- },
- eventSupported: function (eventName) {
- var el = document.createElement("div");
- eventName = "on" + eventName;
-
- var isSupported = (eventName in el);
- if (!isSupported) {
- el.setAttribute(eventName, "return;");
- isSupported = typeof el[eventName] === "function";
- }
- el = null;
-
- return isSupported;
- }
-
- });
- /**
- * @Prototype
- */
- extend(Syn.init.prototype, {
- /**
- * @function Syn.then then()
- * @parent chained
- *
- * Then is used to chain a sequence of actions to be run one after the other.
- * This is useful when many asynchronous actions need to be performed before some
- * final check needs to be made.
- *
- *
The following clicks and types into the id='age' element and then checks that only numeric characters can be entered.
- *
Example
- * @codestart
- * Syn('click',{},'age')
- * .then('type','I am 12',function(){
- * equals($('#age').val(),"12")
- * })
- * @codeend
- * If the element argument is undefined, then the last element is used.
- *
- * @param {String} type The type of event or action to create: "_click", "_dblclick", "_drag", "_type".
- * @param {Object} options Optiosn to pass to the event.
- * @param {String|HTMLElement} [element] A element's id or an element. If undefined, defaults to the previous element.
- * @param {Function} [callback] A function to callback after the action has run, but before any future chained actions are run.
- */
- then: function (type, options, element, callback) {
- if (Syn.autoDelay) {
- this.delay();
- }
- var args = Syn.args(options, element, callback),
- self = this;
-
- //if stack is empty run right away
- //otherwise ... unshift it
- this.queue.unshift(function (el, prevented) {
-
- if (typeof this[type] === "function") {
- this.element = args.element || el;
- this[type](args.options, this.element, function (defaults, el) {
- if (args.callback) {
- args.callback.apply(self, arguments);
- }
- self.done.apply(self, arguments);
- });
- } else {
- this.result = Syn.trigger(type, args.options, args.element);
- if (args.callback) {
- args.callback.call(this, args.element, this.result);
- }
- return this;
- }
- });
- return this;
- },
- /**
- * @function Syn.delay delay()
- * @parent chained
- * Delays the next command a set timeout.
- * @param {Number} [timeout]
- * @param {Function} [callback]
- */
- delay: function (timeout, callback) {
- if (typeof timeout === 'function') {
- callback = timeout;
- timeout = null;
- }
- timeout = timeout || 600;
- var self = this;
- this.queue.unshift(function () {
- schedule(function () {
- if (callback) {
- callback.apply(self, []);
- }
- self.done.apply(self, arguments);
- }, timeout);
- });
- return this;
- },
- done: function (defaults, el) {
- if (el) {
- this.element = el;
- }
- if (this.queue.length) {
- this.queue.pop()
- .call(this, this.element, defaults);
- }
-
- },
- /**
- * @function Syn.click click()
- * @parent mouse
- * @signature `Syn.click(options, element, callback, force)`
- * Clicks an element by triggering a mousedown,
- * mouseup,
- * and a click event.
- *
Example
- * @codestart
- * Syn.click({},'create',function(){
- * //check something
- * })
- * @codeend
- * You can also provide the coordinates of the click.
- * If jQuery is present, it will set clientX and clientY
- * for you. Here's how to set it yourself:
- * @codestart
- * Syn.click(
- * {clientX: 20, clientY: 100},
- * 'create',
- * function(){
- * //check something
- * })
- * @codeend
- * You can also provide pageX and pageY and Syn will convert it for you.
- * @param {Object} options
- * @param {HTMLElement} element
- * @param {Function} callback
- */
- "_click": function (options, element, callback, force) {
- Syn.helpers.addOffset(options, element);
- Syn.trigger("mousedown", options, element);
-
- //timeout is b/c IE is stupid and won't call focus handlers
- schedule(function () {
- Syn.trigger("mouseup", options, element);
- if (!Syn.support.mouseDownUpClicks || force) {
- Syn.trigger("click", options, element);
- callback(true);
- } else {
- //we still have to run the default (presumably)
- Syn.create.click.setup('click', options, element);
- Syn.defaults.click.call(element);
- //must give time for callback
- schedule(function () {
- callback(true);
- }, 1);
- }
-
- }, 1);
- },
- /**
- * @function Syn.rightClick rightClick()
- * @parent mouse
- * @signature `Syn.rightClick(options, element, callback)`
- * Right clicks in browsers that support it (everyone but opera).
- * @param {Object} options
- * @param {Object} element
- * @param {Object} callback
- */
- "_rightClick": function (options, element, callback) {
- Syn.helpers.addOffset(options, element);
- var mouseopts = extend(extend({}, Syn.mouse.browser.right.mouseup), options);
-
- Syn.trigger("mousedown", mouseopts, element);
-
- //timeout is b/c IE is stupid and won't call focus handlers
- schedule(function () {
- Syn.trigger("mouseup", mouseopts, element);
- if (Syn.mouse.browser.right.contextmenu) {
- Syn.trigger("contextmenu", extend(extend({}, Syn.mouse.browser.right.contextmenu), options), element);
- }
- callback(true);
- }, 1);
- },
- /**
- * @function Syn.dblclick dblclick()
- * @parent mouse
- * @signature `Syn.dblclick(options, element, callback)`
- * Dblclicks an element. This runs two [Syn.click click] events followed by
- * a dblclick on the element.
- *
Example
- * @codestart
- * Syn.dblclick({},'open')
- * @codeend
- * @param {Object} options
- * @param {HTMLElement} element
- * @param {Function} callback
- */
- "_dblclick": function (options, element, callback) {
- Syn.helpers.addOffset(options, element);
- var self = this;
- this._click(options, element, function () {
- schedule(function () {
- self._click(options, element, function () {
- Syn.trigger("dblclick", options, element);
- callback(true);
- }, true);
- }, 2);
-
- });
- }
- });
-
- var actions = ["click", "dblclick", "move", "drag", "key", "type", 'rightClick'],
- makeAction = function (name) {
- Syn[name] = function (options, element, callback) {
- return Syn("_" + name, options, element, callback);
- };
- Syn.init.prototype[name] = function (options, element, callback) {
- return this.then("_" + name, options, element, callback);
- };
- },
- i = 0;
-
- for (; i < actions.length; i++) {
- makeAction(actions[i]);
- }
-
- return Syn;
-})();
-
-// ## src/mouse.js
-var __m3 = (function (Syn) {
- //handles mosue events
-
- var h = Syn.helpers,
- getWin = h.getWindow;
-
- Syn.mouse = {};
- h.extend(Syn.defaults, {
- mousedown: function (options) {
- Syn.trigger("focus", {}, this);
- },
- click: function () {
- // prevents the access denied issue in IE if the click causes the element to be destroyed
- var element = this,
- href, type, createChange, radioChanged, nodeName, scope;
- try {
- href = element.href;
- type = element.type;
- createChange = Syn.data(element, "createChange");
- radioChanged = Syn.data(element, "radioChanged");
- scope = getWin(element);
- nodeName = element.nodeName.toLowerCase();
- } catch (e) {
- return;
- }
- //get old values
-
- //this code was for restoring the href attribute to prevent popup opening
- //if ((href = Syn.data(element, "href"))) {
- // element.setAttribute('href', href)
- //}
-
- //run href javascript
- if (!Syn.support.linkHrefJS && /^\s*javascript:/.test(href)) {
- //eval js
- var code = href.replace(/^\s*javascript:/, "");
-
- //try{
- if (code !== "//" && code.indexOf("void(0)") === -1) {
- if (window.selenium) {
- eval("with(selenium.browserbot.getCurrentWindow()){" + code + "}");
- } else {
- eval("with(scope){" + code + "}");
- }
- }
- }
-
- //submit a form
- if (!(Syn.support.clickSubmits) && (nodeName === "input" &&
- type === "submit") ||
- nodeName === 'button') {
-
- var form = Syn.closest(element, "form");
- if (form) {
- Syn.trigger("submit", {}, form);
- }
-
- }
- //follow a link, probably needs to check if in an a.
- if (nodeName === "a" && element.href && !/^\s*javascript:/.test(href)) {
- scope.location.href = href;
-
- }
-
- //change a checkbox
- if (nodeName === "input" && type === "checkbox") {
-
- //if(!Syn.support.clickChecks && !Syn.support.changeChecks){
- // element.checked = !element.checked;
- //}
- if (!Syn.support.clickChanges) {
- Syn.trigger("change", {}, element);
- }
- }
-
- //change a radio button
- if (nodeName === "input" && type === "radio") { // need to uncheck others if not checked
- if (radioChanged && !Syn.support.radioClickChanges) {
- Syn.trigger("change", {}, element);
- }
- }
- // change options
- if (nodeName === "option" && createChange) {
- Syn.trigger("change", {}, element.parentNode); //does not bubble
- Syn.data(element, "createChange", false);
- }
- }
- });
-
- //add create and setup behavior for mosue events
- h.extend(Syn.create, {
- mouse: {
- options: function (type, options, element) {
- var doc = document.documentElement,
- body = document.body,
- center = [options.pageX || 0, options.pageY || 0],
- //browser might not be loaded yet (doing support code)
- left = Syn.mouse.browser && Syn.mouse.browser.left[type],
- right = Syn.mouse.browser && Syn.mouse.browser.right[type];
- return h.extend({
- bubbles: true,
- cancelable: true,
- view: window,
- detail: 1,
- screenX: 1,
- screenY: 1,
- clientX: options.clientX || center[0] - (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc.clientLeft || 0),
- clientY: options.clientY || center[1] - (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc.clientTop || 0),
- ctrlKey: !! Syn.key.ctrlKey,
- altKey: !! Syn.key.altKey,
- shiftKey: !! Syn.key.shiftKey,
- metaKey: !! Syn.key.metaKey,
- button: left && left.button !== null ? left.button : right && right.button || (type === 'contextmenu' ? 2 : 0),
- relatedTarget: document.documentElement
- }, options);
- },
- event: function (type, defaults, element) { //Everyone Else
- var doc = getWin(element)
- .document || document,
- event;
- if (doc.createEvent) {
- try {
- event = doc.createEvent('MouseEvents');
- event.initMouseEvent(type, defaults.bubbles, defaults.cancelable,
- defaults.view, defaults.detail,
- defaults.screenX, defaults.screenY,
- defaults.clientX, defaults.clientY,
- defaults.ctrlKey, defaults.altKey,
- defaults.shiftKey, defaults.metaKey,
- defaults.button, defaults.relatedTarget);
- } catch (e) {
- event = h.createBasicStandardEvent(type, defaults, doc);
- }
- event.synthetic = true;
- return event;
- } else {
- try {
- event = h.createEventObject(type, defaults, element);
- } catch (e) {}
-
- return event;
- }
-
- }
- },
- click: {
- setup: function (type, options, element) {
- var nodeName = element.nodeName.toLowerCase();
-
- //we need to manually 'check' in browser that can't check
- //so checked has the right value
- if (!Syn.support.clickChecks && !Syn.support.changeChecks && nodeName === "input") {
- type = element.type.toLowerCase(); //pretty sure lowercase isn't needed
- if (type === 'checkbox') {
- element.checked = !element.checked;
- }
- if (type === "radio") {
- //do the checks manually
- if (!element.checked) { //do nothing, no change
- try {
- Syn.data(element, "radioChanged", true);
- } catch (e) {}
- element.checked = true;
- }
- }
- }
-
- if (nodeName === "a" && element.href && !/^\s*javascript:/.test(element.href)) {
-
- //save href
- Syn.data(element, "href", element.href);
-
- //remove b/c safari/opera will open a new tab instead of changing the page
- // this has been removed because newer versions don't have this problem
- //element.setAttribute('href', 'javascript://')
- //however this breaks scripts using the href
- //we need to listen to this and prevent the default behavior
- //and run the default behavior ourselves. Boo!
- }
- //if select or option, save old value and mark to change
- if (/option/i.test(element.nodeName)) {
- var child = element.parentNode.firstChild,
- i = -1;
- while (child) {
- if (child.nodeType === 1) {
- i++;
- if (child === element) {
- break;
- }
- }
- child = child.nextSibling;
- }
- if (i !== element.parentNode.selectedIndex) {
- //shouldn't this wait on triggering
- //change?
- element.parentNode.selectedIndex = i;
- Syn.data(element, "createChange", true);
- }
- }
-
- }
- },
- mousedown: {
- setup: function (type, options, element) {
- var nn = element.nodeName.toLowerCase();
- //we have to auto prevent default to prevent freezing error in safari
- if (Syn.browser.safari && (nn === "select" || nn === "option")) {
- options._autoPrevent = true;
- }
- }
- }
- });
-
- return Syn;
-})(__m2);
-
-// ## src/browsers.js
-var __m4 = (function (Syn) {
- Syn.key.browsers = {
- webkit: {
- 'prevent': {
- "keyup": [],
- "keydown": ["char", "keypress"],
- "keypress": ["char"]
- },
- 'character': {
- "keydown": [0, "key"],
- "keypress": ["char", "char"],
- "keyup": [0, "key"]
- },
- 'specialChars': {
- "keydown": [0, "char"],
- "keyup": [0, "char"]
- },
- 'navigation': {
- "keydown": [0, "key"],
- "keyup": [0, "key"]
- },
- 'special': {
- "keydown": [0, "key"],
- "keyup": [0, "key"]
- },
- 'tab': {
- "keydown": [0, "char"],
- "keyup": [0, "char"]
- },
- 'pause-break': {
- "keydown": [0, "key"],
- "keyup": [0, "key"]
- },
- 'caps': {
- "keydown": [0, "key"],
- "keyup": [0, "key"]
- },
- 'escape': {
- "keydown": [0, "key"],
- "keyup": [0, "key"]
- },
- 'num-lock': {
- "keydown": [0, "key"],
- "keyup": [0, "key"]
- },
- 'scroll-lock': {
- "keydown": [0, "key"],
- "keyup": [0, "key"]
- },
- 'print': {
- "keyup": [0, "key"]
- },
- 'function': {
- "keydown": [0, "key"],
- "keyup": [0, "key"]
- },
- '\r': {
- "keydown": [0, "key"],
- "keypress": ["char", "key"],
- "keyup": [0, "key"]
- }
- },
- gecko: {
- 'prevent': {
- "keyup": [],
- "keydown": ["char"],
- "keypress": ["char"]
- },
- 'character': {
- "keydown": [0, "key"],
- "keypress": ["char", 0],
- "keyup": [0, "key"]
- },
- 'specialChars': {
- "keydown": [0, "key"],
- "keypress": [0, "key"],
- "keyup": [0, "key"]
- },
- 'navigation': {
- "keydown": [0, "key"],
- "keypress": [0, "key"],
- "keyup": [0, "key"]
- },
- 'special': {
- "keydown": [0, "key"],
- "keyup": [0, "key"]
- },
- '\t': {
- "keydown": [0, "key"],
- "keypress": [0, "key"],
- "keyup": [0, "key"]
- },
- 'pause-break': {
- "keydown": [0, "key"],
- "keypress": [0, "key"],
- "keyup": [0, "key"]
- },
- 'caps': {
- "keydown": [0, "key"],
- "keyup": [0, "key"]
- },
- 'escape': {
- "keydown": [0, "key"],
- "keypress": [0, "key"],
- "keyup": [0, "key"]
- },
- 'num-lock': {
- "keydown": [0, "key"],
- "keyup": [0, "key"]
- },
- 'scroll-lock': {
- "keydown": [0, "key"],
- "keyup": [0, "key"]
- },
- 'print': {
- "keyup": [0, "key"]
- },
- 'function': {
- "keydown": [0, "key"],
- "keyup": [0, "key"]
- },
- '\r': {
- "keydown": [0, "key"],
- "keypress": [0, "key"],
- "keyup": [0, "key"]
- }
- },
- msie: {
- 'prevent': {
- "keyup": [],
- "keydown": ["char", "keypress"],
- "keypress": ["char"]
- },
- 'character': {
- "keydown": [null, "key"],
- "keypress": [null, "char"],
- "keyup": [null, "key"]
- },
- 'specialChars': {
- "keydown": [null, "char"],
- "keyup": [null, "char"]
- },
- 'navigation': {
- "keydown": [null, "key"],
- "keyup": [null, "key"]
- },
- 'special': {
- "keydown": [null, "key"],
- "keyup": [null, "key"]
- },
- 'tab': {
- "keydown": [null, "char"],
- "keyup": [null, "char"]
- },
- 'pause-break': {
- "keydown": [null, "key"],
- "keyup": [null, "key"]
- },
- 'caps': {
- "keydown": [null, "key"],
- "keyup": [null, "key"]
- },
- 'escape': {
- "keydown": [null, "key"],
- "keypress": [null, "key"],
- "keyup": [null, "key"]
- },
- 'num-lock': {
- "keydown": [null, "key"],
- "keyup": [null, "key"]
- },
- 'scroll-lock': {
- "keydown": [null, "key"],
- "keyup": [null, "key"]
- },
- 'print': {
- "keyup": [null, "key"]
- },
- 'function': {
- "keydown": [null, "key"],
- "keyup": [null, "key"]
- },
- '\r': {
- "keydown": [null, "key"],
- "keypress": [null, "key"],
- "keyup": [null, "key"]
- }
- },
- opera: {
- 'prevent': {
- "keyup": [],
- "keydown": [],
- "keypress": ["char"]
- },
- 'character': {
- "keydown": [null, "key"],
- "keypress": [null, "char"],
- "keyup": [null, "key"]
- },
- 'specialChars': {
- "keydown": [null, "char"],
- "keypress": [null, "char"],
- "keyup": [null, "char"]
- },
- 'navigation': {
- "keydown": [null, "key"],
- "keypress": [null, "key"]
- },
- 'special': {
- "keydown": [null, "key"],
- "keypress": [null, "key"],
- "keyup": [null, "key"]
- },
- 'tab': {
- "keydown": [null, "char"],
- "keypress": [null, "char"],
- "keyup": [null, "char"]
- },
- 'pause-break': {
- "keydown": [null, "key"],
- "keypress": [null, "key"],
- "keyup": [null, "key"]
- },
- 'caps': {
- "keydown": [null, "key"],
- "keyup": [null, "key"]
- },
- 'escape': {
- "keydown": [null, "key"],
- "keypress": [null, "key"]
- },
- 'num-lock': {
- "keyup": [null, "key"],
- "keydown": [null, "key"],
- "keypress": [null, "key"]
- },
- 'scroll-lock': {
- "keydown": [null, "key"],
- "keypress": [null, "key"],
- "keyup": [null, "key"]
- },
- 'print': {},
- 'function': {
- "keydown": [null, "key"],
- "keypress": [null, "key"],
- "keyup": [null, "key"]
- },
- '\r': {
- "keydown": [null, "key"],
- "keypress": [null, "key"],
- "keyup": [null, "key"]
- }
- }
- };
-
- Syn.mouse.browsers = {
- webkit: {
- "right": {
- "mousedown": {
- "button": 2,
- "which": 3
- },
- "mouseup": {
- "button": 2,
- "which": 3
- },
- "contextmenu": {
- "button": 2,
- "which": 3
- }
- },
- "left": {
- "mousedown": {
- "button": 0,
- "which": 1
- },
- "mouseup": {
- "button": 0,
- "which": 1
- },
- "click": {
- "button": 0,
- "which": 1
- }
- }
- },
- opera: {
- "right": {
- "mousedown": {
- "button": 2,
- "which": 3
- },
- "mouseup": {
- "button": 2,
- "which": 3
- }
- },
- "left": {
- "mousedown": {
- "button": 0,
- "which": 1
- },
- "mouseup": {
- "button": 0,
- "which": 1
- },
- "click": {
- "button": 0,
- "which": 1
- }
- }
- },
- msie: {
- "right": {
- "mousedown": {
- "button": 2
- },
- "mouseup": {
- "button": 2
- },
- "contextmenu": {
- "button": 0
- }
- },
- "left": {
- "mousedown": {
- "button": 1
- },
- "mouseup": {
- "button": 1
- },
- "click": {
- "button": 0
- }
- }
- },
- chrome: {
- "right": {
- "mousedown": {
- "button": 2,
- "which": 3
- },
- "mouseup": {
- "button": 2,
- "which": 3
- },
- "contextmenu": {
- "button": 2,
- "which": 3
- }
- },
- "left": {
- "mousedown": {
- "button": 0,
- "which": 1
- },
- "mouseup": {
- "button": 0,
- "which": 1
- },
- "click": {
- "button": 0,
- "which": 1
- }
- }
- },
- gecko: {
- "left": {
- "mousedown": {
- "button": 0,
- "which": 1
- },
- "mouseup": {
- "button": 0,
- "which": 1
- },
- "click": {
- "button": 0,
- "which": 1
- }
- },
- "right": {
- "mousedown": {
- "button": 2,
- "which": 3
- },
- "mouseup": {
- "button": 2,
- "which": 3
- },
- "contextmenu": {
- "button": 2,
- "which": 3
- }
- }
- }
- };
-
- //set browser
- Syn.key.browser =
- (function () {
- if (Syn.key.browsers[window.navigator.userAgent]) {
- return Syn.key.browsers[window.navigator.userAgent];
- }
- for (var browser in Syn.browser) {
- if (Syn.browser[browser] && Syn.key.browsers[browser]) {
- return Syn.key.browsers[browser];
- }
- }
- return Syn.key.browsers.gecko;
- })();
-
- Syn.mouse.browser =
- (function () {
- if (Syn.mouse.browsers[window.navigator.userAgent]) {
- return Syn.mouse.browsers[window.navigator.userAgent];
- }
- for (var browser in Syn.browser) {
- if (Syn.browser[browser] && Syn.mouse.browsers[browser]) {
- return Syn.mouse.browsers[browser];
- }
- }
- return Syn.mouse.browsers.gecko;
- })();
- return Syn;
-})(__m2, __m3);
-
-// ## src/typeable.js
-var __m6 = (function (Syn) {
- // Holds functions that test for typeability
- var typeables = [];
-
- // IE <= 8 doesn't implement [].indexOf.
- // This shim was extracted from CoffeeScript:
- var __indexOf = [].indexOf || function (item) {
- for (var i = 0, l = this.length; i < l; i++) {
- if (i in this && this[i] === item) {
- return i;
- }
- }
- return -1;
- };
-
- /*
- * @function typeable
- * Registers a function that is used to determine if an
- * element can be typed into. The user can define as many
- * test functions as needed. By default there are 2 typeable
- * functions, one for inputs and textareas, and another
- * for contenteditable elements.
- *
- * @param {Function} fn Function to register.
- */
- Syn.typeable = function (fn) {
- if (__indexOf.call(typeables, fn) === -1) {
- typeables.push(fn);
- }
- };
-
- /*
- * @function test
- * Tests whether an element can be typed into using the test
- * functions registered by [Syn.typeable typeable]. If any of the
- * test functions returns true, `test` will return true and allow
- * the element to be typed into.
- *
- * @param {HTMLElement} el the element to test.
- * @return {Boolean} true if the element can be typed into.
- */
- Syn.typeable.test = function (el) {
- for (var i = 0, len = typeables.length; i < len; i++) {
- if (typeables[i](el)) {
- return true;
- }
- }
- return false;
- };
-
- var type = Syn.typeable;
-
- // Inputs and textareas
- var typeableExp = /input|textarea/i;
- type(function (el) {
- return typeableExp.test(el.nodeName);
- });
-
- // Content editable
- type(function (el) {
- return __indexOf.call(["", "true"], el.getAttribute("contenteditable")) !== -1;
- });
-
- return Syn;
-})(__m2);
-
-// ## src/key.js
-var __m5 = (function (Syn) {
- var h = Syn.helpers,
-
- // gets the selection of an input or textarea
- getSelection = function (el) {
- var real, r, start;
-
- // use selectionStart if we can
- if (el.selectionStart !== undefined) {
- // this is for opera, so we don't have to focus to type how we think we would
- if (document.activeElement && document.activeElement !== el &&
- el.selectionStart === el.selectionEnd && el.selectionStart === 0) {
- return {
- start: el.value.length,
- end: el.value.length
- };
- }
- return {
- start: el.selectionStart,
- end: el.selectionEnd
- };
- } else {
- //check if we aren't focused
- try {
- //try 2 different methods that work differently (IE breaks depending on type)
- if (el.nodeName.toLowerCase() === 'input') {
- real = h.getWindow(el)
- .document.selection.createRange();
- r = el.createTextRange();
- r.setEndPoint("EndToStart", real);
-
- start = r.text.length;
- return {
- start: start,
- end: start + real.text.length
- };
- } else {
- real = h.getWindow(el)
- .document.selection.createRange();
- r = real.duplicate();
- var r2 = real.duplicate(),
- r3 = real.duplicate();
- r2.collapse();
- r3.collapse(false);
- r2.moveStart('character', -1);
- r3.moveStart('character', -1);
- //select all of our element
- r.moveToElementText(el);
- //now move our endpoint to the end of our real range
- r.setEndPoint('EndToEnd', real);
- start = r.text.length - real.text.length;
- var end = r.text.length;
- if (start !== 0 && r2.text === "") {
- start += 2;
- }
- if (end !== 0 && r3.text === "") {
- end += 2;
- }
- //if we aren't at the start, but previous is empty, we are at start of newline
- return {
- start: start,
- end: end
- };
- }
- } catch (e) {
- var prop = formElExp.test(el.nodeName) ? "value" : "textContent";
-
- return {
- start: el[prop].length,
- end: el[prop].length
- };
- }
- }
- },
- // gets all focusable elements
- getFocusable = function (el) {
- var document = h.getWindow(el)
- .document,
- res = [];
-
- var els = document.getElementsByTagName('*'),
- len = els.length;
-
- for (var i = 0; i < len; i++) {
- if (Syn.isFocusable(els[i]) && els[i] !== document.documentElement) {
- res.push(els[i]);
- }
- }
- return res;
- },
- formElExp = /input|textarea/i,
- textProperty = (function(){
- var el = document.createElement("span");
- return el.textContent != null ? 'textContent' : 'innerText';
- })(),
-
- // Get the text from an element.
- getText = function (el) {
- if (formElExp.test(el.nodeName)) {
- return el.value;
- }
- return el[textProperty];
- },
- // Set the text of an element.
- setText = function (el, value) {
- if (formElExp.test(el.nodeName)) {
- el.value = value;
- } else {
- el[textProperty] = value;
- }
- };
-
- /**
- *
- */
- h.extend(Syn, {
- /**
- * @attribute
- * @parent keys
- * A list of the keys and their keycodes codes you can type.
- * You can add type keys with
- * @codestart
- * Syn('key','delete','title');
- *
- * //or
- *
- * Syn('type','One Two Three[left][left][delete]','title')
- * @codeend
- *
- * The following are a list of keys you can type:
- * @codestart text
- * \b - backspace
- * \t - tab
- * \r - enter
- * ' ' - space
- * a-Z 0-9 - normal characters
- * /!@#$*,.? - All other typeable characters
- * page-up - scrolls up
- * page-down - scrolls down
- * end - scrolls to bottom
- * home - scrolls to top
- * insert - changes how keys are entered
- * delete - deletes the next character
- * left - moves cursor left
- * right - moves cursor right
- * up - moves the cursor up
- * down - moves the cursor down
- * f1-12 - function buttons
- * shift, ctrl, alt - special keys
- * pause-break - the pause button
- * scroll-lock - locks scrolling
- * caps - makes caps
- * escape - escape button
- * num-lock - allows numbers on keypad
- * print - screen capture
- * subtract - subtract (keypad) -
- * dash - dash -
- * divide - divide (keypad) /
- * forward-slash - forward slash /
- * decimal - decimal (keypad) .
- * period - period .
- * @codeend
- */
- keycodes: {
- //backspace
- '\b': 8,
-
- //tab
- '\t': 9,
-
- //enter
- '\r': 13,
-
- //special
- 'shift': 16,
- 'ctrl': 17,
- 'alt': 18,
-
- //weird
- 'pause-break': 19,
- 'caps': 20,
- 'escape': 27,
- 'num-lock': 144,
- 'scroll-lock': 145,
- 'print': 44,
-
- //navigation
- 'page-up': 33,
- 'page-down': 34,
- 'end': 35,
- 'home': 36,
- 'left': 37,
- 'up': 38,
- 'right': 39,
- 'down': 40,
- 'insert': 45,
- 'delete': 46,
-
- //normal characters
- ' ': 32,
- '0': 48,
- '1': 49,
- '2': 50,
- '3': 51,
- '4': 52,
- '5': 53,
- '6': 54,
- '7': 55,
- '8': 56,
- '9': 57,
- 'a': 65,
- 'b': 66,
- 'c': 67,
- 'd': 68,
- 'e': 69,
- 'f': 70,
- 'g': 71,
- 'h': 72,
- 'i': 73,
- 'j': 74,
- 'k': 75,
- 'l': 76,
- 'm': 77,
- 'n': 78,
- 'o': 79,
- 'p': 80,
- 'q': 81,
- 'r': 82,
- 's': 83,
- 't': 84,
- 'u': 85,
- 'v': 86,
- 'w': 87,
- 'x': 88,
- 'y': 89,
- 'z': 90,
- //normal-characters, numpad
- 'num0': 96,
- 'num1': 97,
- 'num2': 98,
- 'num3': 99,
- 'num4': 100,
- 'num5': 101,
- 'num6': 102,
- 'num7': 103,
- 'num8': 104,
- 'num9': 105,
- '*': 106,
- '+': 107,
- 'subtract': 109,
- 'decimal': 110,
- //normal-characters, others
- 'divide': 111,
- ';': 186,
- '=': 187,
- ',': 188,
- 'dash': 189,
- '-': 189,
- 'period': 190,
- '.': 190,
- 'forward-slash': 191,
- '/': 191,
- '`': 192,
- '[': 219,
- '\\': 220,
- ']': 221,
- "'": 222,
-
- //ignore these, you shouldn't use them
- 'left window key': 91,
- 'right window key': 92,
- 'select key': 93,
-
- 'f1': 112,
- 'f2': 113,
- 'f3': 114,
- 'f4': 115,
- 'f5': 116,
- 'f6': 117,
- 'f7': 118,
- 'f8': 119,
- 'f9': 120,
- 'f10': 121,
- 'f11': 122,
- 'f12': 123
- },
-
- // selects text on an element
- selectText: function (el, start, end) {
- if (el.setSelectionRange) {
- if (!end) {
- Syn.__tryFocus(el);
- el.setSelectionRange(start, start);
- } else {
- el.selectionStart = start;
- el.selectionEnd = end;
- }
- } else if (el.createTextRange) {
- //Syn.__tryFocus(el);
- var r = el.createTextRange();
- r.moveStart('character', start);
- end = end || start;
- r.moveEnd('character', end - el.value.length);
-
- r.select();
- }
- },
- getText: function (el) {
- //first check if the el has anything selected ..
- if (Syn.typeable.test(el)) {
- var sel = getSelection(el);
- return el.value.substring(sel.start, sel.end);
- }
- //otherwise get from page
- var win = Syn.helpers.getWindow(el);
- if (win.getSelection) {
- return win.getSelection()
- .toString();
- } else if (win.document.getSelection) {
- return win.document.getSelection()
- .toString();
- } else {
- return win.document.selection.createRange()
- .text;
- }
- },
- getSelection: getSelection
- });
-
- h.extend(Syn.key, {
- // retrieves a description of what events for this character should look like
- data: function (key) {
- //check if it is described directly
- if (Syn.key.browser[key]) {
- return Syn.key.browser[key];
- }
- for (var kind in Syn.key.kinds) {
- if (h.inArray(key, Syn.key.kinds[kind]) > -1) {
- return Syn.key.browser[kind];
- }
- }
- return Syn.key.browser.character;
- },
-
- //returns the special key if special
- isSpecial: function (keyCode) {
- var specials = Syn.key.kinds.special;
- for (var i = 0; i < specials.length; i++) {
- if (Syn.keycodes[specials[i]] === keyCode) {
- return specials[i];
- }
- }
- },
- /**
- * @hide
- * gets the options for a key and event type ...
- * @param {Object} key
- * @param {Object} event
- */
- options: function (key, event) {
- var keyData = Syn.key.data(key);
-
- if (!keyData[event]) {
- //we shouldn't be creating this event
- return null;
- }
-
- var charCode = keyData[event][0],
- keyCode = keyData[event][1],
- result = {};
-
- if (keyCode === 'key') {
- result.keyCode = Syn.keycodes[key];
- } else if (keyCode === 'char') {
- result.keyCode = key.charCodeAt(0);
- } else {
- result.keyCode = keyCode;
- }
-
- if (charCode === 'char') {
- result.charCode = key.charCodeAt(0);
- } else if (charCode !== null) {
- result.charCode = charCode;
- }
-
- // all current browsers have which property to normalize keyCode/charCode
- if (result.keyCode) {
- result.which = result.keyCode;
- } else {
- result.which = result.charCode;
- }
-
- return result;
- },
- //types of event keys
- kinds: {
- special: ["shift", 'ctrl', 'alt', 'caps'],
- specialChars: ["\b"],
- navigation: ["page-up", 'page-down', 'end', 'home', 'left', 'up', 'right', 'down', 'insert', 'delete'],
- 'function': ['f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10', 'f11', 'f12']
- },
- //returns the default function
- // some keys have default functions
- // some 'kinds' of keys have default functions
- getDefault: function (key) {
- //check if it is described directly
- if (Syn.key.defaults[key]) {
- return Syn.key.defaults[key];
- }
- for (var kind in Syn.key.kinds) {
- if (h.inArray(key, Syn.key.kinds[kind]) > -1 && Syn.key.defaults[kind]) {
- return Syn.key.defaults[kind];
- }
- }
- return Syn.key.defaults.character;
- },
- // default behavior when typing
- defaults: {
- 'character': function (options, scope, key, force, sel) {
- if (/num\d+/.test(key)) {
- key = key.match(/\d+/)[0];
- }
-
- if (force || (!Syn.support.keyCharacters && Syn.typeable.test(this))) {
- var current = getText(this),
- before = current.substr(0, sel.start),
- after = current.substr(sel.end),
- character = key;
-
- setText(this, before + character + after);
- //handle IE inserting \r\n
- var charLength = character === "\n" && Syn.support.textareaCarriage ? 2 : character.length;
- Syn.selectText(this, before.length + charLength);
- }
- },
- 'c': function (options, scope, key, force, sel) {
- if (Syn.key.ctrlKey) {
- Syn.key.clipboard = Syn.getText(this);
- } else {
- Syn.key.defaults.character.apply(this, arguments);
- }
- },
- 'v': function (options, scope, key, force, sel) {
- if (Syn.key.ctrlKey) {
- Syn.key.defaults.character.call(this, options, scope, Syn.key.clipboard, true, sel);
- } else {
- Syn.key.defaults.character.apply(this, arguments);
- }
- },
- 'a': function (options, scope, key, force, sel) {
- if (Syn.key.ctrlKey) {
- Syn.selectText(this, 0, getText(this)
- .length);
- } else {
- Syn.key.defaults.character.apply(this, arguments);
- }
- },
- 'home': function () {
- Syn.onParents(this, function (el) {
- if (el.scrollHeight !== el.clientHeight) {
- el.scrollTop = 0;
- return false;
- }
- });
- },
- 'end': function () {
- Syn.onParents(this, function (el) {
- if (el.scrollHeight !== el.clientHeight) {
- el.scrollTop = el.scrollHeight;
- return false;
- }
- });
- },
- 'page-down': function () {
- //find the first parent we can scroll
- Syn.onParents(this, function (el) {
- if (el.scrollHeight !== el.clientHeight) {
- var ch = el.clientHeight;
- el.scrollTop += ch;
- return false;
- }
- });
- },
- 'page-up': function () {
- Syn.onParents(this, function (el) {
- if (el.scrollHeight !== el.clientHeight) {
- var ch = el.clientHeight;
- el.scrollTop -= ch;
- return false;
- }
- });
- },
- '\b': function (options, scope, key, force, sel) {
- //this assumes we are deleting from the end
- if (!Syn.support.backspaceWorks && Syn.typeable.test(this)) {
- var current = getText(this),
- before = current.substr(0, sel.start),
- after = current.substr(sel.end);
-
- if (sel.start === sel.end && sel.start > 0) {
- //remove a character
- setText(this, before.substring(0, before.length - 1) + after);
- Syn.selectText(this, sel.start - 1);
- } else {
- setText(this, before + after);
- Syn.selectText(this, sel.start);
- }
-
- //set back the selection
- }
- },
- 'delete': function (options, scope, key, force, sel) {
- if (!Syn.support.backspaceWorks && Syn.typeable.test(this)) {
- var current = getText(this),
- before = current.substr(0, sel.start),
- after = current.substr(sel.end);
- if (sel.start === sel.end && sel.start <= getText(this)
- .length - 1) {
- setText(this, before + after.substring(1));
- } else {
- setText(this, before + after);
- }
- Syn.selectText(this, sel.start);
- }
- },
- '\r': function (options, scope, key, force, sel) {
-
- var nodeName = this.nodeName.toLowerCase();
- // submit a form
- if (nodeName === 'input') {
- Syn.trigger("change", {}, this);
- }
-
- if (!Syn.support.keypressSubmits && nodeName === 'input') {
- var form = Syn.closest(this, "form");
- if (form) {
- Syn.trigger("submit", {}, form);
- }
-
- }
- //newline in textarea
- if (!Syn.support.keyCharacters && nodeName === 'textarea') {
- Syn.key.defaults.character.call(this, options, scope, "\n",
- undefined, sel);
- }
- // 'click' hyperlinks
- if (!Syn.support.keypressOnAnchorClicks && nodeName === 'a') {
- Syn.trigger("click", {}, this);
- }
- },
- //
- // Gets all focusable elements. If the element (this)
- // doesn't have a tabindex, finds the next element after.
- // If the element (this) has a tabindex finds the element
- // with the next higher tabindex OR the element with the same
- // tabindex after it in the document.
- // @return the next element
- //
- '\t': function (options, scope) {
- // focusable elements
- var focusEls = getFocusable(this),
- // will be set to our guess for the next element
- current = null,
- i = 0,
- el,
- //the tabindex of the tabable element we are looking at
- firstNotIndexed,
- orders = [];
- for (; i < focusEls.length; i++) {
- orders.push([focusEls[i], i]);
- }
- var sort = function (order1, order2) {
- var el1 = order1[0],
- el2 = order2[0],
- tab1 = Syn.tabIndex(el1) || 0,
- tab2 = Syn.tabIndex(el2) || 0;
- if (tab1 === tab2) {
- return order1[1] - order2[1];
- } else {
- if (tab1 === 0) {
- return 1;
- } else if (tab2 === 0) {
- return -1;
- } else {
- return tab1 - tab2;
- }
- }
- };
- orders.sort(sort);
- //now find current
- for (i = 0; i < orders.length; i++) {
- el = orders[i][0];
- if (this === el) {
- if (!Syn.key.shiftKey) {
- current = orders[i + 1][0];
- if (!current) {
- current = orders[0][0];
- }
- } else {
- current = orders[i - 1][0];
- if (!current) {
- current = orders[focusEls.length - 1][0];
- }
- }
- }
- }
-
- //restart if we didn't find anything
- if (!current) {
- current = firstNotIndexed;
- } else {
- Syn.__tryFocus(current);
- }
- return current;
- },
- 'left': function (options, scope, key, force, sel) {
- if (Syn.typeable.test(this)) {
- if (Syn.key.shiftKey) {
- Syn.selectText(this, sel.start === 0 ? 0 : sel.start - 1, sel.end);
- } else {
- Syn.selectText(this, sel.start === 0 ? 0 : sel.start - 1);
- }
- }
- },
- 'right': function (options, scope, key, force, sel) {
- if (Syn.typeable.test(this)) {
- if (Syn.key.shiftKey) {
- Syn.selectText(this, sel.start, sel.end + 1 > getText(this)
- .length ? getText(this)
- .length : sel.end + 1);
- } else {
- Syn.selectText(this, sel.end + 1 > getText(this)
- .length ? getText(this)
- .length : sel.end + 1);
- }
- }
- },
- 'up': function () {
- if (/select/i.test(this.nodeName)) {
-
- this.selectedIndex = this.selectedIndex ? this.selectedIndex - 1 : 0;
- //set this to change on blur?
- }
- },
- 'down': function () {
- if (/select/i.test(this.nodeName)) {
- Syn.changeOnBlur(this, "selectedIndex", this.selectedIndex);
- this.selectedIndex = this.selectedIndex + 1;
- //set this to change on blur?
- }
- },
- 'shift': function () {
- return null;
- },
- 'ctrl': function () {
- return null;
- }
- }
- });
-
- h.extend(Syn.create, {
- keydown: {
- setup: function (type, options, element) {
- if (h.inArray(options, Syn.key.kinds.special) !== -1) {
- Syn.key[options + "Key"] = element;
- }
- }
- },
- keypress: {
- setup: function (type, options, element) {
- // if this browsers supports writing keys on events
- // but doesn't write them if the element isn't focused
- // focus on the element (ignored if already focused)
- if (Syn.support.keyCharacters && !Syn.support.keysOnNotFocused) {
- Syn.__tryFocus(element);
- }
- }
- },
- keyup: {
- setup: function (type, options, element) {
- if (h.inArray(options, Syn.key.kinds.special) !== -1) {
- Syn.key[options + "Key"] = null;
- }
- }
- },
- key: {
- // return the options for a key event
- options: function (type, options, element) {
- //check if options is character or has character
- options = typeof options !== "object" ? {
- character: options
- } : options;
-
- //don't change the orignial
- options = h.extend({}, options);
- if (options.character) {
- h.extend(options, Syn.key.options(options.character, type));
- delete options.character;
- }
-
- options = h.extend({
- ctrlKey: !! Syn.key.ctrlKey,
- altKey: !! Syn.key.altKey,
- shiftKey: !! Syn.key.shiftKey,
- metaKey: !! Syn.key.metaKey
- }, options);
-
- return options;
- },
- // creates a key event
- event: function (type, options, element) { //Everyone Else
- var doc = h.getWindow(element)
- .document || document,
- event;
- if (doc.createEvent) {
- try {
- event = doc.createEvent("KeyEvents");
- event.initKeyEvent(type, true, true, window, options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.keyCode, options.charCode);
- } catch (e) {
- event = h.createBasicStandardEvent(type, options, doc);
- }
- event.synthetic = true;
- return event;
- } else {
- try {
- event = h.createEventObject.apply(this, arguments);
- h.extend(event, options);
- } catch (e) {}
-
- return event;
- }
- }
- }
- });
-
- var convert = {
- "enter": "\r",
- "backspace": "\b",
- "tab": "\t",
- "space": " "
- };
-
- /**
- *
- */
- h.extend(Syn.init.prototype, {
- /**
- * @function Syn.key key()
- * @parent keys
- * @signature `Syn.key(options, element, callback)`
- * Types a single key. The key should be
- * a string that matches a
- * [Syn.static.keycodes].
- *
- * The following sends a carridge return
- * to the 'name' element.
- * @codestart
- * Syn.key('\r','name')
- * @codeend
- * For each character, a keydown, keypress, and keyup is triggered if
- * appropriate.
- * @param {String|Number} options
- * @param {HTMLElement} [element]
- * @param {Function} [callback]
- * @return {HTMLElement} the element currently focused.
- */
- _key: function (options, element, callback) {
- //first check if it is a special up
- if (/-up$/.test(options) && h.inArray(options.replace("-up", ""),
- Syn.key.kinds.special) !== -1) {
- Syn.trigger('keyup', options.replace("-up", ""), element);
- return callback(true, element);
- }
-
- // keep reference to current activeElement
- var activeElement = h.getWindow(element)
- .document.activeElement,
- caret = Syn.typeable.test(element) && getSelection(element),
- key = convert[options] || options,
- // should we run default events
- runDefaults = Syn.trigger('keydown', key, element),
-
- // a function that gets the default behavior for a key
- getDefault = Syn.key.getDefault,
-
- // how this browser handles preventing default events
- prevent = Syn.key.browser.prevent,
-
- // the result of the default event
- defaultResult,
-
- keypressOptions = Syn.key.options(key, 'keypress');
-
- if (runDefaults) {
- //if the browser doesn't create keypresses for this key, run default
- if (!keypressOptions) {
- defaultResult = getDefault(key)
- .call(element, keypressOptions, h.getWindow(element),
- key, undefined, caret);
- } else {
- //do keypress
- // check if activeElement changed b/c someone called focus in keydown
- if (activeElement !== h.getWindow(element)
- .document.activeElement) {
- element = h.getWindow(element)
- .document.activeElement;
- }
-
- runDefaults = Syn.trigger('keypress', keypressOptions, element);
- if (runDefaults) {
- defaultResult = getDefault(key)
- .call(element, keypressOptions, h.getWindow(element),
- key, undefined, caret);
- }
- }
- } else {
- //canceled ... possibly don't run keypress
- if (keypressOptions && h.inArray('keypress', prevent.keydown) === -1) {
- // check if activeElement changed b/c someone called focus in keydown
- if (activeElement !== h.getWindow(element)
- .document.activeElement) {
- element = h.getWindow(element)
- .document.activeElement;
- }
-
- Syn.trigger('keypress', keypressOptions, element);
- }
- }
- if (defaultResult && defaultResult.nodeName) {
- element = defaultResult;
- }
-
- if (defaultResult !== null) {
- Syn.schedule(function () {
- if (Syn.support.oninput) {
- Syn.trigger('input', Syn.key.options(key, 'input'), element);
- }
- Syn.trigger('keyup', Syn.key.options(key, 'keyup'), element);
- callback(runDefaults, element);
- }, 1);
- } else {
- callback(runDefaults, element);
- }
-
- //do mouseup
- return element;
- // is there a keypress? .. if not , run default
- // yes -> did we prevent it?, if not run ...
- },
- /**
- * @function Syn.type type()
- * @parent keys
- * @signature `Syn.type(options, element, callback)`
- * Types sequence of [Syn.key key actions]. Each
- * character is typed, one at a type.
- * Multi-character keys like 'left' should be
- * enclosed in square brackents.
- *
- * The following types 'JavaScript MVC' then deletes the space.
- * @codestart
- * Syn.type('JavaScript MVC[left][left][left]\b','name')
- * @codeend
- *
- * Type is able to handle (and move with) tabs (\t).
- * The following simulates tabing and entering values in a form and
- * eventually submitting the form.
- * @codestart
- * Syn.type("Justin\tMeyer\t27\tjustinbmeyer@gmail.com\r")
- * @codeend
- * @param {String} options the text to type
- * @param {HTMLElement} [element] an element or an id of an element
- * @param {Function} [callback] a function to callback
- */
- _type: function (options, element, callback) {
- //break it up into parts ...
- //go through each type and run
- var parts = (options + "")
- .match(/(\[[^\]]+\])|([^\[])/g),
- self = this,
- runNextPart = function (runDefaults, el) {
- var part = parts.shift();
- if (!part) {
- callback(runDefaults, el);
- return;
- }
- el = el || element;
- if (part.length > 1) {
- part = part.substr(1, part.length - 2);
- }
- self._key(part, el, runNextPart);
- };
-
- runNextPart();
-
- }
- });
-
- return Syn;
-})(__m2, __m6, __m4);
-
-// ## src/drag/drag.js
-var __m7 = (function (Syn) {
-
- // check if elementFromPageExists
- (function dragSupport() {
-
- // document body has to exists for this test
- if (!document.body) {
- Syn.schedule(dragSupport, 1);
- return;
- }
- var div = document.createElement('div');
- document.body.appendChild(div);
- Syn.helpers.extend(div.style, {
- width: "100px",
- height: "10000px",
- backgroundColor: "blue",
- position: "absolute",
- top: "10px",
- left: "0px",
- zIndex: 19999
- });
- document.body.scrollTop = 11;
- if (!document.elementFromPoint) {
- return;
- }
- var el = document.elementFromPoint(3, 1);
- if (el === div) {
- Syn.support.elementFromClient = true;
- } else {
- Syn.support.elementFromPage = true;
- }
- document.body.removeChild(div);
- document.body.scrollTop = 0;
- })();
-
- //gets an element from a point
- var elementFromPoint = function (point, element) {
- var clientX = point.clientX,
- clientY = point.clientY,
- win = Syn.helpers.getWindow(element),
- el;
-
- if (Syn.support.elementFromPage) {
- var off = Syn.helpers.scrollOffset(win);
- clientX = clientX + off.left; //convert to pageX
- clientY = clientY + off.top; //convert to pageY
- }
- el = win.document.elementFromPoint ? win.document.elementFromPoint(clientX, clientY) : element;
- if (el === win.document.documentElement && (point.clientY < 0 || point.clientX < 0)) {
- return element;
- } else {
- return el;
- }
- },
- //creates an event at a certain point
- createEventAtPoint = function (event, point, element) {
- var el = elementFromPoint(point, element);
- Syn.trigger(event, point, el || element);
- return el;
- },
- // creates a mousemove event, but first triggering mouseout / mouseover if appropriate
- mouseMove = function (point, element, last) {
- var el = elementFromPoint(point, element);
- if (last !== el && el && last) {
- var options = Syn.helpers.extend({}, point);
- options.relatedTarget = el;
- Syn.trigger("mouseout", options, last);
- options.relatedTarget = last;
- Syn.trigger("mouseover", options, el);
- }
-
- Syn.trigger("mousemove", point, el || element);
- return el;
- },
- // start and end are in clientX, clientY
- startMove = function (start, end, duration, element, callback) {
- var startTime = new Date(),
- distX = end.clientX - start.clientX,
- distY = end.clientY - start.clientY,
- win = Syn.helpers.getWindow(element),
- current = elementFromPoint(start, element),
- cursor = win.document.createElement('div'),
- calls = 0,
- move;
- move = function onmove() {
- //get what fraction we are at
- var now = new Date(),
- scrollOffset = Syn.helpers.scrollOffset(win),
- fraction = (calls === 0 ? 0 : now - startTime) / duration,
- options = {
- clientX: distX * fraction + start.clientX,
- clientY: distY * fraction + start.clientY
- };
- calls++;
- if (fraction < 1) {
- Syn.helpers.extend(cursor.style, {
- left: (options.clientX + scrollOffset.left + 2) + "px",
- top: (options.clientY + scrollOffset.top + 2) + "px"
- });
- current = mouseMove(options, element, current);
- Syn.schedule(onmove, 15);
- } else {
- current = mouseMove(end, element, current);
- win.document.body.removeChild(cursor);
- callback();
- }
- };
- Syn.helpers.extend(cursor.style, {
- height: "5px",
- width: "5px",
- backgroundColor: "red",
- position: "absolute",
- zIndex: 19999,
- fontSize: "1px"
- });
- win.document.body.appendChild(cursor);
- move();
- },
- startDrag = function (start, end, duration, element, callback) {
- createEventAtPoint("mousedown", start, element);
- startMove(start, end, duration, element, function () {
- createEventAtPoint("mouseup", end, element);
- callback();
- });
- },
- center = function (el) {
- var j = Syn.jquery()(el),
- o = j.offset();
- return {
- pageX: o.left + (j.outerWidth() / 2),
- pageY: o.top + (j.outerHeight() / 2)
- };
- },
- convertOption = function (option, win, from) {
- var page = /(\d+)[x ](\d+)/,
- client = /(\d+)X(\d+)/,
- relative = /([+-]\d+)[xX ]([+-]\d+)/,
- parts;
- //check relative "+22x-44"
- if (typeof option === 'string' && relative.test(option) && from) {
- var cent = center(from);
- parts = option.match(relative);
- option = {
- pageX: cent.pageX + parseInt(parts[1]),
- pageY: cent.pageY + parseInt(parts[2])
- };
- }
- if (typeof option === "string" && page.test(option)) {
- parts = option.match(page);
- option = {
- pageX: parseInt(parts[1]),
- pageY: parseInt(parts[2])
- };
- }
- if (typeof option === 'string' && client.test(option)) {
- parts = option.match(client);
- option = {
- clientX: parseInt(parts[1]),
- clientY: parseInt(parts[2])
- };
- }
- if (typeof option === 'string') {
- option = Syn.jquery()(option, win.document)[0];
- }
- if (option.nodeName) {
- option = center(option);
- }
- if (option.pageX) {
- var off = Syn.helpers.scrollOffset(win);
- option = {
- clientX: option.pageX - off.left,
- clientY: option.pageY - off.top
- };
- }
- return option;
- },
- // if the client chords are not going to be visible ... scroll the page so they will be ...
- adjust = function (from, to, win) {
- if (from.clientY < 0) {
- var off = Syn.helpers.scrollOffset(win);
- var top = off.top + (from.clientY) - 100,
- diff = top - off.top;
-
- // first, lets see if we can scroll 100 px
- if (top > 0) {
-
- } else {
- top = 0;
- diff = -off.top;
- }
- from.clientY = from.clientY - diff;
- to.clientY = to.clientY - diff;
- Syn.helpers.scrollOffset(win, {
- top: top,
- left: off.left
- });
- }
- };
- /**
- * @add Syn prototype
- */
- Syn.helpers.extend(Syn.init.prototype, {
- /**
- * @function Syn.move move()
- * @parent mouse
- * @signature `Syn.move(options, from, callback)`
- * Moves the cursor from one point to another.
- *
- * ### Quick Example
- *
- * The following moves the cursor from (0,0) in
- * the window to (100,100) in 1 second.
- *
- * Syn.move(
- * {
- * from: {clientX: 0, clientY: 0},
- * to: {clientX: 100, clientY: 100},
- * duration: 1000
- * },
- * document.document)
- *
- * ## Options
- *
- * There are many ways to configure the endpoints of the move.
- *
- * ### PageX and PageY
- *
- * If you pass pageX or pageY, these will get converted
- * to client coordinates.
- *
- * Syn.move(
- * {
- * from: {pageX: 0, pageY: 0},
- * to: {pageX: 100, pageY: 100}
- * },
- * document.document)
- *
- * ### String Coordinates
- *
- * You can set the pageX and pageY as strings like:
- *
- * Syn.move(
- * {
- * from: "0x0",
- * to: "100x100"
- * },
- * document.document)
- *
- * ### Element Coordinates
- *
- * If jQuery is present, you can pass an element as the from or to option
- * and the coordinate will be set as the center of the element.
-
- * Syn.move(
- * {
- * from: $(".recipe")[0],
- * to: $("#trash")[0]
- * },
- * document.document)
- *
- * ### Query Strings
- *
- * If jQuery is present, you can pass a query string as the from or to option.
- *
- * Syn.move(
- * {
- * from: ".recipe",
- * to: "#trash"
- * },
- * document.document)
- *
- * ### No From
- *
- * If you don't provide a from, the element argument passed to Syn is used.
- *
- * Syn.move(
- * { to: "#trash" },
- * 'myrecipe')
- *
- * ### Relative
- *
- * You can move the drag relative to the center of the from element.
- *
- * Syn.move("+20 +30", "myrecipe");
- *
- * @param {Object} options options to configure the drag
- * @param {HTMLElement} from the element to move
- * @param {Function} callback a callback that happens after the drag motion has completed
- */
- _move: function (options, from, callback) {
- //need to convert if elements
- var win = Syn.helpers.getWindow(from),
- fro = convertOption(options.from || from, win, from),
- to = convertOption(options.to || options, win, from);
-
- if (options.adjust !== false) {
- adjust(fro, to, win);
- }
- startMove(fro, to, options.duration || 500, from, callback);
-
- },
- /**
- * @function Syn.drag drag()
- * @parent mouse
- * @signature `Syn.drag(options, from, callback)`
- * Creates a mousedown and drags from one point to another.
- * Check out [Syn.prototype.move move] for API details.
- *
- * @param {Object} options
- * @param {Object} from
- * @param {Object} callback
- */
- _drag: function (options, from, callback) {
- //need to convert if elements
- var win = Syn.helpers.getWindow(from),
- fro = convertOption(options.from || from, win, from),
- to = convertOption(options.to || options, win, from);
-
- if (options.adjust !== false) {
- adjust(fro, to, win);
- }
- startDrag(fro, to, options.duration || 500, from, callback);
-
- }
- });
- return Syn;
-})(__m2);
-
-// ## src/syn.js
-var __m1 = (function (Syn) {
- window.Syn = Syn;
-
- return Syn;
- })(__m2, __m3, __m4, __m5, __m7);
-
-}(window);
\ No newline at end of file
diff --git a/tests/e2e/test.html b/tests/e2e/test.html
deleted file mode 100644
index 3c869eb8..00000000
--- a/tests/e2e/test.html
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
- Basic Test Suite
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/tests/e2e/tests.js b/tests/e2e/tests.js
deleted file mode 100644
index 078ba122..00000000
--- a/tests/e2e/tests.js
+++ /dev/null
@@ -1,437 +0,0 @@
-//saucelabs reporting; see https://github.com/axemclion/grunt-saucelabs#test-result-details-with-qunit
-
-var log = []
-
-QUnit.done(function (test_results) {
- var tests = []
- for (var i = 0, len = log.length; i < len; i++) {
- var details = log[i]
- tests.push({
- name: details.name,
- result: details.result,
- expected: details.expected,
- actual: details.actual,
- source: details.source
- })
- }
- test_results.tests = tests
-
- window.global_test_results = test_results
-})
-QUnit.testStart(function (testDetails) {
- QUnit.log(function (details) {
- if (!details.result) {
- details.name = testDetails.name
- log.push(details)
- }
- })
-})
-
-//qunit doesn't support Function.prototype.bind...
-if (!Function.prototype.bind) {
- Function.prototype.bind = function (oThis) {
- if (typeof this !== "function") {
- // closest thing possible to the ECMAScript 5
- // internal IsCallable function
- throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable")
- }
-
- var aArgs = Array.prototype.slice.call(arguments, 1),
- fToBind = this,
- fNOP = function () {},
- fBound = function () {
- return fToBind.apply(this instanceof fNOP && oThis
- ? this
- : oThis,
- aArgs.concat(Array.prototype.slice.call(arguments)))
- }
-
- fNOP.prototype = this.prototype
- fBound.prototype = new fNOP()
-
- return fBound
- }
-}
-
-//tests
-var dummyEl = document.getElementById('dummy')
-
-test('Mithril accessible as window.m', function() {
- expect(1)
- ok(window.m)
-})
-
-test('m.trust w/ html entities', function() {
- expect(1)
- var view1 = m('div', "a", m.trust("&"), "b")
-
- m.render(dummyEl, view1)
- equal(dummyEl.innerHTML, '