Merge branch 'next'

This commit is contained in:
Isiah Meadows 2018-10-25 14:23:33 -04:00
commit 2635070734
79 changed files with 7993 additions and 3507 deletions

View file

@ -1,7 +1,7 @@
.vscode
/coverage
/docs/lib
/examples
.vscode/
coverage/
docs/lib/
examples/
/mithril.js
/mithril.min.js
/node_modules
node_modules/

2
.gitattributes vendored
View file

@ -1,3 +1,5 @@
* text=auto
/mithril.js binary
/mithril.min.js binary
/package-lock.json binary
/yarn.lock binary

1
.github/CODEOWNERS vendored Normal file
View file

@ -0,0 +1 @@
render/ @pygy

32
.github/ISSUE_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,32 @@
<!--- Provide a general summary of the issue in the Title above -->
## Expected Behavior
<!--- If you're describing a bug, tell us what should happen -->
<!--- If you're suggesting a change/improvement, tell us how it should work -->
## Current Behavior
<!--- If describing a bug, tell us what happens instead of the expected behavior -->
<!--- If suggesting a change/improvement, explain the difference from current behavior -->
## Possible Solution
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
<!--- or ideas how to implement the addition or change -->
## Steps to Reproduce (for bugs)
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
<!--- reproduce this bug. Include code to reproduce, if relevant -->
1.
2.
3.
4.
## Context
<!--- How has this issue affected you? What are you trying to accomplish? -->
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
## Your Environment
<!--- Include as many relevant details about the environment you experienced the bug in -->
* Version used:
* Browser Name and version:
* Operating System and version (desktop or mobile):
* Link to your project:

31
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,31 @@
<!--- Provide a general summary of your changes in the Title above -->
## Description
<!--- Describe your changes in detail -->
## Motivation and Context
<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->
## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. -->
## Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Documentation change
## Checklist:
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [ ] My code follows the code style of this project.
- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.
- [ ] I have read the **CONTRIBUTING** document.
- [ ] I have added tests to cover my changes.
- [ ] All new and existing tests passed.
- [ ] I have updated `docs/change-log.md`

View file

@ -6,3 +6,4 @@
.gitignore
.travis.yml
CONTRIBUTING.md
yarn.lock

View file

@ -13,15 +13,19 @@ cache:
# Custom install step so the travis-only stuff doesn't need to be in package.json
install:
- npm install
- npm install @alrra/travis-scripts@^3.0.1 gh-pages@^0.12.0
# This is to prevent lint-staged/prettier from running on the bundles
- npm rm husky
# Bundle before running tests so the bundle is always up-to-date
before_script: npm run build --silent
# Build bundles (so they're always up to date)
before_script:
- npm run build-browser
# Pass -save so it'll update the readme as well
- npm run build-min -- -save
# Run tests, lint, and then check for perf regressions
script:
- npm test --silent
- npm run perf --silent
- npm test
- npm run perf
# After a successful build commit changes back to repo
after_success:
@ -84,7 +88,6 @@ deploy:
on:
tags: true
repo: MithrilJS/mithril.js
branch: master
- provider: npm
skip_cleanup: true
@ -94,4 +97,3 @@ deploy:
on:
tags: true
repo: MithrilJS/mithril.js
branch: master

316
README.md
View file

@ -1,267 +1,61 @@
# Introduction
mithril.js [![NPM Version](https://img.shields.io/npm/v/mithril.svg)](https://www.npmjs.com/package/mithril) [![NPM License](https://img.shields.io/npm/l/mithril.svg)](https://www.npmjs.com/package/mithril) [![NPM Downloads](https://img.shields.io/npm/dm/mithril.svg)](https://www.npmjs.com/package/mithril)
==========
<p align="center">
<a href="https://travis-ci.org/MithrilJS/mithril.js">
<img src="https://img.shields.io/travis/MithrilJS/mithril.js/next.svg" alt="Build Status">
</a>
<a href="https://gitter.im/mithriljs/mithril.js">
<img src="https://img.shields.io/gitter/room/mithriljs/mithril.js.svg" alt="Gitter" />
</a>
</p>
- [What is Mithril?](#what-is-mithril)
- [Getting started](#getting-started)
- [Hello world](#hello-world)
- [DOM elements](#dom-elements)
- [Components](#components)
- [Routing](#routing)
- [XHR](#xhr)
- [Installation](#installation)
- [Documentation](#documentation)
- [Getting Help](#getting-help)
- [Contributing](#contributing)
## What is Mithril?
A modern client-side Javascript framework for building Single Page Applications. It's small (<!-- size -->8.87 KB<!-- /size --> gzipped), fast and provides routing and XHR utilities out of the box.
Mithril is used by companies like Vimeo and Nike, and open source platforms like Lichess 👍.
Browsers all the way back to IE9 are supported, no polyfills required 👌.
## Installation
### CDN
```html
<script src="https://unpkg.com/mithril"></script>
```
### npm
```bash
$ npm install mithril
```
The ["Getting started" guide](https://mithril.js.org/#getting-started) is a good place to start learning how to use mithril.
## Documentation
Documentation lives on [mithril.js.org](https://mithril.js.org).
You may be interested in the [API Docs](https://mithril.js.org/api.html), a [Simple Application](https://mithril.js.org/simple-application.html), or perhaps some [Examples](https://mithril.js.org/examples.html).
## Getting Help
Mithril has an active & welcoming community on [Gitter](https://gitter.im/mithriljs/mithril.js), or feel free to ask questions on [Stack Overflow](https://stackoverflow.com/questions/tagged/mithril.js) using the `mithril.js` tag.
## Contributing
There's a [Contributing FAQ](https://mithril.js.org/contributing.html) on the mithril site that hopefully helps, but if not definitely hop into the [Gitter Room](https://gitter.im/mithriljs/mithril.js) and ask away!
---
### What is Mithril?
Thanks for reading!
Mithril is a modern client-side Javascript framework for building Single Page Applications.
It's small (< 8kb gzip), fast and provides routing and XHR utilities out of the box.
<div style="display:flex;margin:0 0 30px;">
<div style="width:50%;">
<h5>Download size</h5>
<small>Mithril (8kb)</small>
<div style="animation:grow 0.08s;background:#1e5799;height:3px;margin:0 10px 10px 0;transform-origin:0;width:4%;"></div>
<small style="color:#aaa;">Vue + Vue-Router + Vuex + fetch (40kb)</small>
<div style="animation:grow 0.4s;background:#1e5799;height:3px;margin:0 10px 10px 0;transform-origin:0;width:20%"></div>
<small style="color:#aaa;">React + React-Router + Redux + fetch (64kb)</small>
<div style="animation:grow 0.64s;background:#1e5799;height:3px;margin:0 10px 10px 0;transform-origin:0;width:32%"></div>
<small style="color:#aaa;">Angular (135kb)</small>
<div style="animation:grow 1.35s;background:#1e5799;height:3px;margin:0 10px 10px 0;transform-origin:0;width:68%"></div>
</div>
<div style="width:50%;">
<h5>Performance</h5>
<small>Mithril (6.4ms)</small>
<div style="animation:grow 0.64s;background:#1e5799;height:3px;margin:0 10px 10px 0;transform-origin:0;width:24%;"></div>
<small style="color:#aaa;">Vue (9.8ms)</small>
<div style="animation:grow 0.98s;background:#1e5799;height:3px;margin:0 10px 10px 0;transform-origin:0;width:40%"></div>
<small style="color:#aaa;">React (12.1ms)</small>
<div style="animation:grow 1.21s;background:#1e5799;height:3px;margin:0 10px 10px 0;transform-origin:0;width:48%"></div>
<small style="color:#aaa;">Angular (11.5ms)</small>
<div style="animation:grow 1.15s;background:#1e5799;height:3px;margin:0 10px 10px 0;transform-origin:0;width:44%"></div>
</div>
</div>
Mithril is used by companies like Vimeo and Nike, and open source platforms like Lichess.
If you are an experienced developer and want to know how Mithril compares to other frameworks, see the [framework comparison](http://mithril.js.org/framework-comparison.html) page.
Mithril supports browsers all the way back to IE9, no polyfills required.
---
### Getting started
The easiest way to try out Mithril is to include it from a CDN, and follow this tutorial. It'll cover the majority of the API surface (including routing and XHR) but it'll only take 10 minutes.
Let's create an HTML file to follow along:
```markup
<body>
<script src="//unpkg.com/mithril/mithril.js"></script>
<script>
var root = document.body
// your code goes here!
</script>
</body>
```
---
### Hello world
Let's start as small as we can: render some text on screen. Copy the code below into your file (and by copy, I mean type it out - you'll learn better)
```javascript
var root = document.body
m.render(root, "Hello world")
```
Now, let's change the text to something else. Add this line of code under the previous one:
```javascript
m.render(root, "My first app")
```
As you can see, you use the same code to both create and update HTML. Mithril automatically figures out the most efficient way of updating the text, rather than blindly recreating it from scratch.
---
### DOM elements
Let's wrap our text in an `<h1>` tag.
```javascript
m.render(root, m("h1", "My first app"))
```
The `m()` function can be used to describe any HTML structure you want. So if you need to add a class to the `<h1>`:
```javascript
m("h1", {class: "title"}, "My first app")
```
If you want to have multiple elements:
```javascript
[
m("h1", {class: "title"}, "My first app"),
m("button", "A button"),
]
```
And so on:
```javascript
m("main", [
m("h1", {class: "title"}, "My first app"),
m("button", "A button"),
])
```
Note: If you prefer `<html>` syntax, [it's possible to use it via a Babel plugin](http://mithril.js.org/jsx.html).
```jsx
// HTML syntax via Babel's JSX plugin
<main>
<h1 class="title">My first app</h1>
<button>A button</button>
</main>
```
---
### Components
A Mithril component is just an object with a `view` function. Here's the code above as a component:
```javascript
var Hello = {
view: function() {
return m("main", [
m("h1", {class: "title"}, "My first app"),
m("button", "A button"),
])
}
}
```
To activate the component, we use `m.mount`.
```javascript
m.mount(root, Hello)
```
As you would expect, doing so creates this markup:
```markup
<main>
<h1 class="title">My first app</h1>
<button>A button</button>
</main>
```
The `m.mount` function is similar to `m.render`, but instead of rendering some HTML only once, it activates Mithril's auto-redrawing system. To understand what that means, let's add some events:
```javascript
var count = 0 // added a variable
var Hello = {
view: function() {
return m("main", [
m("h1", {class: "title"}, "My first app"),
// changed the next line
m("button", {onclick: function() {count++}}, count + " clicks"),
])
}
}
m.mount(root, Hello)
```
We defined an `onclick` event on the button, which increments a variable `count` (which was declared at the top). We are now also rendering the value of that variable in the button label.
You can now update the label of the button by clicking the button. Since we used `m.mount`, you don't need to manually call `m.render` to apply the changes in the `count` variable to the HTML; Mithril does it for you.
If you're wondering about performance, it turns out Mithril is very fast at rendering updates, because it only touches the parts of the DOM it absolutely needs to. So in our example above, when you click the button, the text in it is the only part of the DOM Mithril actually updates.
---
### Routing
Routing just means going from one screen to another in an application with several screens.
Let's add a splash page that appears before our click counter. First we create a component for it:
```javascript
var Splash = {
view: function() {
return m("a", {href: "#!/hello"}, "Enter!")
}
}
```
As you can see, this component simply renders a link to `#!/hello`. The `#!` part is known as a hashbang, and it's a common convention used in Single Page Applications to indicate that the stuff after it (the `/hello` part) is a route path.
Now that we going to have more than one screen, we use `m.route` instead of `m.mount`.
```javascript
m.route(root, "/splash", {
"/splash": Splash,
"/hello": Hello,
})
```
The `m.route` function still has the same auto-redrawing functionality that `m.mount` does, and it also enables URL awareness; in other words, it lets Mithril know what to do when it sees a `#!` in the URL.
The `"/splash"` right after `root` means that's the default route, i.e. if the hashbang in the URL doesn't point to one of the defined routes (`/splash` and `/hello`, in our case), then Mithril redirects to the default route. So if you open the page in a browser and your URL is `http://localhost`, then you get redirected to `http://localhost/#!/splash`.
Also, as you would expect, clicking on the link on the splash page takes you to the click counter screen we created earlier. Notice that now your URL will point to `http://localhost/#!/hello`. You can navigate back and forth to the splash page using the browser's back and next button.
---
### XHR
Basically, XHR is just a way to talk to a server.
Let's change our click counter to make it save data on a server. For the server, we'll use [REM](http://rem-rest-api.herokuapp.com), a mock REST API designed for toy apps like this tutorial.
First we create a function that calls `m.request`. The `url` specifies an endpoint that represents a resource, the `method` specifies the type of action we're taking (typically the `PUT` method [upserts](https://en.wiktionary.org/wiki/upsert)), `data` is the payload that we're sending to the endpoint and `withCredentials` means to enable cookies (a requirement for the REM API to work)
```javascript
var count = 0
var increment = function() {
m.request({
method: "PUT",
url: "//rem-rest-api.herokuapp.com/api/tutorial/1",
data: {count: count + 1},
withCredentials: true,
})
.then(function(data) {
count = parseInt(data.count)
})
}
```
Calling the increment function [upserts](https://en.wiktionary.org/wiki/upsert) an object `{count: 1}` to the `/api/tutorial/1` endpoint. This endpoint returns an object with the same `count` value that was sent to it. Notice that the `count` variable is only updated after the request completes, and it's updated with the response value from the server now.
Let's replace the event handler in the component to call the `increment` function instead of incrementing the `count` variable directly:
```javascript
var Hello = {
view: function() {
return m("main", [
m("h1", {class: "title"}, "My first app"),
m("button", {onclick: increment}, count + " clicks"),
])
}
}
```
Clicking the button should now update the count.
---
We covered how to create and update HTML, how to create components, routes for a Single Page Application, and interacted with a server via XHR.
This should be enough to get you started writing the frontend for a real application. Now that you are comfortable with the basics of the Mithril API, [be sure to check out the simple application tutorial](http://mithril.js.org/simple-application.html), which walks you through building a realistic application.
🎁

View file

@ -16,6 +16,6 @@ module.exports = function(redrawService) {
redrawService.render(root, Vnode(component))
}
redrawService.subscribe(root, run)
redrawService.redraw()
run()
}
}

View file

@ -4,26 +4,23 @@ var coreRenderer = require("../render/render")
function throttle(callback) {
//60fps translates to 16.6ms, round it down since setTimeout requires int
var time = 16
var delay = 16
var last = 0, pending = null
var timeout = typeof requestAnimationFrame === "function" ? requestAnimationFrame : setTimeout
return function() {
var now = Date.now()
if (last === 0 || now - last >= time) {
last = now
callback()
}
else if (pending === null) {
var elapsed = Date.now() - last
if (pending === null) {
pending = timeout(function() {
pending = null
callback()
last = Date.now()
}, time - (now - last))
}, delay - elapsed)
}
}
}
module.exports = function($window) {
module.exports = function($window, throttleMock) {
var renderService = coreRenderer($window)
renderService.setEventCallback(function(e) {
if (e.redraw === false) e.redraw = undefined
@ -31,18 +28,24 @@ module.exports = function($window) {
})
var callbacks = []
var rendering = false
function subscribe(key, callback) {
unsubscribe(key)
callbacks.push(key, throttle(callback))
callbacks.push(key, callback)
}
function unsubscribe(key) {
var index = callbacks.indexOf(key)
if (index > -1) callbacks.splice(index, 2)
}
function redraw() {
for (var i = 1; i < callbacks.length; i += 2) {
callbacks[i]()
}
function sync() {
if (rendering) throw new Error("Nested m.redraw.sync() call")
rendering = true
for (var i = 1; i < callbacks.length; i+=2) try {callbacks[i]()} catch (e) {if (typeof console !== "undefined") console.error(e)}
rendering = false
}
var redraw = (throttleMock || throttle)(sync)
redraw.sync = sync
return {subscribe: subscribe, unsubscribe: unsubscribe, redraw: redraw, render: renderService.render}
}

View file

@ -11,9 +11,14 @@ module.exports = function($window, redrawService) {
var render, component, attrs, currentPath, lastUpdate
var route = function(root, defaultRoute, routes) {
if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined")
var run = function() {
function run() {
if (render != null) redrawService.render(root, render(Vnode(component, attrs.key, attrs)))
}
var redraw = function() {
run()
redraw = redrawService.redraw
}
redrawService.subscribe(root, run)
var bail = function(path) {
if (path !== defaultRoute) routeService.setPath(defaultRoute, null, {replace: true})
else throw new Error("Could not resolve default route " + defaultRoute)
@ -24,7 +29,7 @@ module.exports = function($window, redrawService) {
component = comp != null && (typeof comp.view === "function" || typeof comp === "function")? comp : "div"
attrs = params, currentPath = path, lastUpdate = null
render = (routeResolver.render || identity).bind(routeResolver)
run()
redraw()
}
if (payload.view || typeof payload === "function") update({}, payload)
else {
@ -36,7 +41,6 @@ module.exports = function($window, redrawService) {
else update(payload, "div")
}
}, bail)
redrawService.subscribe(root, run)
}
route.set = function(path, data, options) {
if (lastUpdate != null) {
@ -48,7 +52,7 @@ module.exports = function($window, redrawService) {
}
route.get = function() {return currentPath}
route.prefix = function(prefix) {routeService.prefix = prefix}
route.link = function(vnode) {
var link = function(options, vnode) {
vnode.dom.setAttribute("href", routeService.prefix + vnode.attrs.href)
vnode.dom.onclick = function(e) {
if (e.ctrlKey || e.metaKey || e.shiftKey || e.which === 2) return
@ -56,9 +60,13 @@ module.exports = function($window, redrawService) {
e.redraw = false
var href = this.getAttribute("href")
if (href.indexOf(routeService.prefix) === 0) href = href.slice(routeService.prefix.length)
route.set(href, undefined, undefined)
route.set(href, undefined, options)
}
}
route.link = function(args) {
if (args.tag == null) return link.bind(link, args)
return link({}, args)
}
route.param = function(key) {
if(typeof attrs !== "undefined" && typeof key !== "undefined") return attrs[key]
return attrs

View file

@ -13,8 +13,8 @@
<script src="../../test-utils/pushStateMock.js"></script>
<script src="../../test-utils/xhrMock.js"></script>
<script src="../../test-utils/browserMock.js"></script>
<script src="../../test-utils/component.js"></script>
<script src="../../test-utils/components.js"></script>
<script src="../../test-utils/throttleMock.js"></script>
<script src="../../promise/promise.js"></script>
<script src="../../render/vnode.js"></script>
<script src="../../render/trust.js"></script>
@ -28,7 +28,6 @@
<script src="../../api/redraw.js"></script>
<script src="../../api/mount.js"></script>
<script src="../../api/router.js"></script>
<script src="./test-redraw.js"></script>
<script src="./test-mount.js"></script>
<script src="./test-router.js"></script>

View file

@ -3,25 +3,29 @@
var o = require("../../ospec/ospec")
var components = require("../../test-utils/components")
var domMock = require("../../test-utils/domMock")
var throttleMocker = require("../../test-utils/throttleMock")
var m = require("../../render/hyperscript")
var apiRedraw = require("../../api/redraw")
var apiMounter = require("../../api/mount")
o.spec("mount", function() {
var FRAME_BUDGET = Math.floor(1000 / 60)
var $window, root, redrawService, mount, render
var $window, root, redrawService, mount, render, throttleMock
o.beforeEach(function() {
$window = domMock()
throttleMock = throttleMocker()
root = $window.document.body
redrawService = apiRedraw($window)
redrawService = apiRedraw($window, throttleMock.throttle)
mount = apiMounter(redrawService)
render = redrawService.render
})
o.afterEach(function() {
o(throttleMock.queueLength()).equals(0)
})
o("throws on invalid component", function() {
var threw = false
try {
@ -46,7 +50,7 @@ o.spec("mount", function() {
o(threw).equals(true)
})
o("renders into `root`", function() {
o("renders into `root` synchronoulsy", function() {
mount(root, createComponent({
view : function() {
return m("div")
@ -68,7 +72,37 @@ o.spec("mount", function() {
o(root.childNodes.length).equals(0)
})
o("redraws on events", function(done) {
o("Mounting a second root doesn't cause the first one to redraw", function() {
var view = o.spy(function() {
return m("div")
})
render(root, [
m("#child0"),
m("#child1")
])
mount(root.childNodes[0], createComponent({
view : view
}))
o(root.firstChild.nodeName).equals("DIV")
o(view.callCount).equals(1)
mount(root.childNodes[1], createComponent({
view : function() {
return m("div")
}
}))
o(view.callCount).equals(1)
throttleMock.fire()
o(view.callCount).equals(1)
})
o("redraws on events", function() {
var onupdate = o.spy()
var oninit = o.spy()
var onclick = o.spy()
@ -96,17 +130,12 @@ o.spec("mount", function() {
o(onclick.args[0].type).equals("click")
o(onclick.args[0].target).equals(root.firstChild)
// Wrapped to give time for the rate-limited redraw to fire
setTimeout(function() {
o(onupdate.callCount).equals(1)
throttleMock.fire()
done()
}, FRAME_BUDGET)
o(onupdate.callCount).equals(1)
})
o("redraws several mount points on events", function(done, timeout) {
timeout(60)
o("redraws several mount points on events", function() {
var onupdate0 = o.spy()
var oninit0 = o.spy()
var onclick0 = o.spy()
@ -153,26 +182,26 @@ o.spec("mount", function() {
o(onclick0.callCount).equals(1)
o(onclick0.this).equals(root.childNodes[0].firstChild)
setTimeout(function() {
o(onupdate0.callCount).equals(1)
o(onupdate1.callCount).equals(1)
throttleMock.fire()
root.childNodes[1].firstChild.dispatchEvent(e)
o(onclick1.callCount).equals(1)
o(onclick1.this).equals(root.childNodes[1].firstChild)
o(onupdate0.callCount).equals(1)
o(onupdate1.callCount).equals(1)
setTimeout(function() {
o(onupdate0.callCount).equals(2)
o(onupdate1.callCount).equals(2)
root.childNodes[1].firstChild.dispatchEvent(e)
done()
}, FRAME_BUDGET)
}, FRAME_BUDGET)
o(onclick1.callCount).equals(1)
o(onclick1.this).equals(root.childNodes[1].firstChild)
throttleMock.fire()
o(onupdate0.callCount).equals(2)
o(onupdate1.callCount).equals(2)
})
o("event handlers can skip redraw", function(done) {
var onupdate = o.spy()
o("event handlers can skip redraw", function() {
var onupdate = o.spy(function(){
throw new Error("This shouldn't have been called")
})
var oninit = o.spy()
var e = $window.document.createEvent("MouseEvents")
@ -194,15 +223,12 @@ o.spec("mount", function() {
o(oninit.callCount).equals(1)
// Wrapped to ensure no redraw fired
setTimeout(function() {
o(onupdate.callCount).equals(0)
throttleMock.fire()
done()
}, FRAME_BUDGET)
o(onupdate.callCount).equals(0)
})
o("redraws when the render function is run", function(done) {
o("redraws when the render function is run", function() {
var onupdate = o.spy()
var oninit = o.spy()
@ -220,17 +246,12 @@ o.spec("mount", function() {
redrawService.redraw()
// Wrapped to give time for the rate-limited redraw to fire
setTimeout(function() {
o(onupdate.callCount).equals(1)
throttleMock.fire()
done()
}, FRAME_BUDGET)
o(onupdate.callCount).equals(1)
})
o("throttles", function(done, timeout) {
timeout(200)
o("throttles", function() {
var i = 0
mount(root, createComponent({view: function() {i++}}))
var before = i
@ -242,12 +263,11 @@ o.spec("mount", function() {
var after = i
setTimeout(function(){
o(before).equals(1) // mounts synchronously
o(after).equals(1) // throttles rest
o(i).equals(2)
done()
},40)
throttleMock.fire()
o(before).equals(1) // mounts synchronously
o(after).equals(1) // throttles rest
o(i).equals(2)
})
})
})

View file

@ -2,6 +2,7 @@
var o = require("../../ospec/ospec")
var domMock = require("../../test-utils/domMock")
var throttleMocker = require("../../test-utils/throttleMock")
var apiRedraw = require("../../api/redraw")
o.spec("redrawService", function() {
@ -17,7 +18,9 @@ o.spec("redrawService", function() {
redrawService.redraw()
})
o("should run a single renderer entry", function(done) {
o("honours throttleMock", function() {
var throttleMock = throttleMocker()
redrawService = apiRedraw(domMock(), throttleMock.throttle)
var spy = o.spy()
redrawService.subscribe(root, spy)
@ -26,15 +29,27 @@ o.spec("redrawService", function() {
redrawService.redraw()
o(spy.callCount).equals(0)
throttleMock.fire()
o(spy.callCount).equals(1)
})
o("should run a single renderer entry", function(done) {
var spy = o.spy()
redrawService.subscribe(root, spy)
o(spy.callCount).equals(0)
redrawService.redraw()
redrawService.redraw()
redrawService.redraw()
o(spy.callCount).equals(1)
o(spy.callCount).equals(0)
setTimeout(function() {
o(spy.callCount).equals(2)
o(spy.callCount).equals(1)
done()
}, 20)
@ -54,27 +69,29 @@ o.spec("redrawService", function() {
redrawService.redraw()
o(spy1.callCount).equals(1)
o(spy2.callCount).equals(1)
o(spy3.callCount).equals(1)
o(spy1.callCount).equals(0)
o(spy2.callCount).equals(0)
o(spy3.callCount).equals(0)
redrawService.redraw()
o(spy1.callCount).equals(1)
o(spy2.callCount).equals(1)
o(spy3.callCount).equals(1)
o(spy1.callCount).equals(0)
o(spy2.callCount).equals(0)
o(spy3.callCount).equals(0)
setTimeout(function() {
o(spy1.callCount).equals(2)
o(spy2.callCount).equals(2)
o(spy3.callCount).equals(2)
o(spy1.callCount).equals(1)
o(spy2.callCount).equals(1)
o(spy3.callCount).equals(1)
done()
}, 20)
})
o("should stop running after unsubscribe", function() {
var spy = o.spy()
o("should stop running after unsubscribe", function(done) {
var spy = o.spy(function() {
throw new Error("This shouldn't have been called")
})
redrawService.subscribe(root, spy)
redrawService.unsubscribe(root, spy)
@ -82,9 +99,33 @@ o.spec("redrawService", function() {
redrawService.redraw()
o(spy.callCount).equals(0)
setTimeout(function() {
o(spy.callCount).equals(0)
done()
}, 20)
})
o("does nothing on invalid unsubscribe", function() {
o("should stop running after unsubscribe, even if it occurs after redraw is requested", function(done) {
var spy = o.spy(function() {
throw new Error("This shouldn't have been called")
})
redrawService.subscribe(root, spy)
redrawService.redraw()
redrawService.unsubscribe(root, spy)
o(spy.callCount).equals(0)
setTimeout(function() {
o(spy.callCount).equals(0)
done()
}, 20)
})
o("does nothing on invalid unsubscribe", function(done) {
var spy = o.spy()
redrawService.subscribe(root, spy)
@ -92,6 +133,39 @@ o.spec("redrawService", function() {
redrawService.redraw()
o(spy.callCount).equals(1)
setTimeout(function() {
o(spy.callCount).equals(1)
done()
}, 20)
})
o("redraw.sync() redraws all roots synchronously", function() {
var el1 = $document.createElement("div")
var el2 = $document.createElement("div")
var el3 = $document.createElement("div")
var spy1 = o.spy()
var spy2 = o.spy()
var spy3 = o.spy()
redrawService.subscribe(el1, spy1)
redrawService.subscribe(el2, spy2)
redrawService.subscribe(el3, spy3)
o(spy1.callCount).equals(0)
o(spy2.callCount).equals(0)
o(spy3.callCount).equals(0)
redrawService.redraw.sync()
o(spy1.callCount).equals(1)
o(spy2.callCount).equals(1)
o(spy3.callCount).equals(1)
redrawService.redraw.sync()
o(spy1.callCount).equals(2)
o(spy2.callCount).equals(2)
o(spy3.callCount).equals(2)
})
})

View file

@ -3,6 +3,7 @@
var o = require("../../ospec/ospec")
var callAsync = require("../../test-utils/callAsync")
var browserMock = require("../../test-utils/browserMock")
var throttleMocker = require("../../test-utils/throttleMock")
var m = require("../../render/hyperscript")
var callAsync = require("../../test-utils/callAsync")
@ -14,19 +15,23 @@ o.spec("route", function() {
void [{protocol: "http:", hostname: "localhost"}, {protocol: "file:", hostname: "/"}].forEach(function(env) {
void ["#", "?", "", "#!", "?!", "/foo"].forEach(function(prefix) {
o.spec("using prefix `" + prefix + "` starting on " + env.protocol + "//" + env.hostname, function() {
var FRAME_BUDGET = Math.floor(1000 / 60)
var $window, root, redrawService, route
var $window, root, redrawService, route, throttleMock
o.beforeEach(function() {
$window = browserMock(env)
throttleMock = throttleMocker()
root = $window.document.body
redrawService = apiRedraw($window)
redrawService = apiRedraw($window, throttleMock.throttle)
route = apiRouter($window, redrawService)
route.prefix(prefix)
})
o.afterEach(function() {
o(throttleMock.queueLength()).equals(0)
})
o("throws on invalid `root` DOM node", function() {
var threw = false
try {
@ -50,7 +55,7 @@ o.spec("route", function() {
o(root.firstChild.nodeName).equals("DIV")
})
o("routed mount points can redraw synchronously (POJO component)", function() {
o("routed mount points only redraw asynchronously (POJO component)", function() {
var view = o.spy()
$window.location.href = prefix + "/"
@ -60,11 +65,14 @@ o.spec("route", function() {
redrawService.redraw()
o(view.callCount).equals(2)
o(view.callCount).equals(1)
throttleMock.fire()
o(view.callCount).equals(2)
})
o("routed mount points can redraw synchronously (constructible component)", function() {
o("routed mount points only redraw asynchronously (constructible component)", function() {
var view = o.spy()
var Cmp = function(){}
@ -77,11 +85,14 @@ o.spec("route", function() {
redrawService.redraw()
o(view.callCount).equals(2)
o(view.callCount).equals(1)
throttleMock.fire()
o(view.callCount).equals(2)
})
o("routed mount points can redraw synchronously (closure component)", function() {
o("routed mount points only redraw asynchronously (closure component)", function() {
var view = o.spy()
function Cmp() {return {view: view}}
@ -93,8 +104,11 @@ o.spec("route", function() {
redrawService.redraw()
o(view.callCount).equals(2)
o(view.callCount).equals(1)
throttleMock.fire()
o(view.callCount).equals(2)
})
o("default route doesn't break back button", function(done) {
@ -160,11 +174,12 @@ o.spec("route", function() {
o(oninit.callCount).equals(1)
redrawService.redraw()
throttleMock.fire()
o(onupdate.callCount).equals(1)
})
o("redraws on events", function(done) {
o("redraws on events", function() {
var onupdate = o.spy()
var oninit = o.spy()
var onclick = o.spy()
@ -194,12 +209,9 @@ o.spec("route", function() {
o(onclick.args[0].type).equals("click")
o(onclick.args[0].target).equals(root.firstChild)
// Wrapped to give time for the rate-limited redraw to fire
callAsync(function() {
o(onupdate.callCount).equals(1)
done()
})
throttleMock.fire()
o(onupdate.callCount).equals(1)
})
o("event handlers can skip redraw", function(done) {
@ -269,6 +281,36 @@ o.spec("route", function() {
o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "") + "test")
})
o("passes options on route.link", function() {
var opts = {}
var e = $window.document.createEvent("MouseEvents")
e.initEvent("click", true, true)
$window.location.href = prefix + "/"
route(root, "/", {
"/" : {
view: function() {
return m("a", {
href: "/test",
oncreate: route.link(opts)
})
}
},
"/test" : {
view : function() {
return m("div")
}
}
})
route.set = o.spy(route.set)
root.firstChild.dispatchEvent(e)
o(route.set.callCount).equals(1)
o(route.set.args[2]).equals(opts)
})
o("accepts RouteResolver with onmatch that returns Component", function(done) {
var matchCount = 0
var renderCount = 0
@ -502,7 +544,10 @@ o.spec("route", function() {
o(oninit.callCount).equals(1)
route.set("/def")
callAsync(function() {
throttleMock.fire()
o(oninit.callCount).equals(2)
done()
})
})
@ -538,23 +583,28 @@ o.spec("route", function() {
route(root, "/a", {
"/a" : {
render: function() {
return m("div")
return m("div", m("p"))
},
},
"/b" : {
render: function() {
return m("div")
return m("div", m("a"))
},
},
})
var dom = root.firstChild
var child = dom.firstChild
o(root.firstChild.nodeName).equals("DIV")
route.set("/b")
callAsync(function() {
throttleMock.fire()
o(root.firstChild).equals(dom)
o(root.firstChild.firstChild).notEquals(child)
done()
})
@ -588,6 +638,7 @@ o.spec("route", function() {
o(renderCount).equals(1)
redrawService.redraw()
throttleMock.fire()
o(matchCount).equals(1)
o(renderCount).equals(2)
@ -623,6 +674,7 @@ o.spec("route", function() {
o(renderCount).equals(1)
redrawService.redraw()
throttleMock.fire()
o(matchCount).equals(1)
o(renderCount).equals(2)
@ -818,10 +870,14 @@ o.spec("route", function() {
})
callAsync(function() {
throttleMock.fire()
route.set("/b")
callAsync(function() {
callAsync(function() {
callAsync(function() {
throttleMock.fire()
o(render.callCount).equals(0)
o(component.view.callCount).equals(2)
@ -942,6 +998,7 @@ o.spec("route", function() {
o(onmatch.callCount).equals(1)
redrawService.redraw()
throttleMock.fire()
o(view.callCount).equals(2)
o(onmatch.callCount).equals(1)
@ -1020,6 +1077,8 @@ o.spec("route", function() {
})
callAsync(function() {
throttleMock.fire()
o(onmatch.callCount).equals(1)
o(render.callCount).equals(1)
@ -1027,6 +1086,8 @@ o.spec("route", function() {
callAsync(function() {
callAsync(function() {
throttleMock.fire()
o(onmatch.callCount).equals(2)
o(render.callCount).equals(2)
@ -1077,9 +1138,15 @@ o.spec("route", function() {
route.set("/b")
callAsync(function() {
throttleMock.fire()
o(root.firstChild.nodeName).equals("B")
route.set("/a")
callAsync(function() {
throttleMock.fire()
o(root.firstChild.nodeName).equals("A")
done()
@ -1144,7 +1211,9 @@ o.spec("route", function() {
route.set("/b")
// setting the route is asynchronous
callAsync(function() {
throttleMock.fire()
o(spy.callCount).equals(1)
done()
@ -1184,9 +1253,7 @@ o.spec("route", function() {
})
})
o("throttles", function(done, timeout) {
timeout(200)
o("throttles", function() {
var i = 0
$window.location.href = prefix + "/"
route(root, "/", {
@ -1200,12 +1267,11 @@ o.spec("route", function() {
redrawService.redraw()
var after = i
setTimeout(function() {
o(before).equals(1) // routes synchronously
o(after).equals(2) // redraws synchronously
o(i).equals(3) // throttles rest
done()
}, FRAME_BUDGET * 2)
throttleMock.fire()
o(before).equals(1) // routes synchronously
o(after).equals(1) // redraws asynchronously
o(i).equals(2)
})
o("m.route.param is available outside of route handlers", function(done) {

View file

@ -17,113 +17,109 @@ function parse(file) {
var error
function run(input, output) {
try {
var modules = {}
var bindings = {}
var declaration = /^\s*(?:var|let|const|function)[\t ]+([\w_$]+)/gm
var include = /(?:((?:var|let|const|,|)[\t ]*)([\w_$\.\[\]"'`]+)(\s*=\s*))?require\(([^\)]+)\)(\s*[`\.\(\[])?/gm
var uuid = 0
var process = function(filepath, data) {
data.replace(declaration, function(match, binding) {bindings[binding] = 0})
var modules = {}
var bindings = {}
var declaration = /^\s*(?:var|let|const|function)[\t ]+([\w_$]+)/gm
var include = /(?:((?:var|let|const|,|)[\t ]*)([\w_$\.\[\]"'`]+)(\s*=\s*))?require\(([^\)]+)\)(\s*[`\.\(\[])?/gm
var uuid = 0
var process = function(filepath, data) {
data.replace(declaration, function(match, binding) {bindings[binding] = 0})
return data.replace(include, function(match, def, variable, eq, dep, rest) {
var filename = new Function("return " + dep).call(), pre = ""
return data.replace(include, function(match, def, variable, eq, dep, rest) {
var filename = new Function("return " + dep).call(), pre = ""
def = def || "", variable = variable || "", eq = eq || "", rest = rest || ""
if (def[0] === ",") def = "\nvar ", pre = "\n"
var dependency = resolve(filepath, filename)
var localUUID = uuid // global uuid can update from nested `process` call, ensure same id is used on declaration and consumption
var code = process(dependency, pre + (modules[dependency] == null ? exportCode(filename, dependency, def, variable, eq, rest, localUUID) : def + variable + eq + modules[dependency]))
modules[dependency] = rest ? "_" + localUUID : variable
uuid++
return code + rest
})
def = def || "", variable = variable || "", eq = eq || "", rest = rest || ""
if (def[0] === ",") def = "\nvar ", pre = "\n"
var dependency = resolve(filepath, filename)
var localUUID = uuid // global uuid can update from nested `process` call, ensure same id is used on declaration and consumption
var code = process(dependency, pre + (modules[dependency] == null ? exportCode(filename, dependency, def, variable, eq, rest, localUUID) : def + variable + eq + modules[dependency]))
modules[dependency] = rest ? "_" + localUUID : variable
uuid++
return code + rest
})
}
var resolve = function(filepath, filename) {
if (filename[0] !== ".") {
// resolve as npm dependency
var packagePath = "./node_modules/" + filename + "/package.json"
var meta = isFile(packagePath) ? parse(packagePath) : {}
var main = "./node_modules/" + filename + "/" + (meta.main || filename + ".js")
return path.resolve(isFile(main) ? main : "./node_modules/" + filename + "/index.js")
}
var resolve = function(filepath, filename) {
if (filename[0] !== ".") {
// resolve as npm dependency
var packagePath = "./node_modules/" + filename + "/package.json"
var meta = isFile(packagePath) ? parse(packagePath) : {}
var main = "./node_modules/" + filename + "/" + (meta.main || filename + ".js")
return path.resolve(isFile(main) ? main : "./node_modules/" + filename + "/index.js")
}
else {
// resolve as local dependency
return path.resolve(path.dirname(filepath), filename + ".js")
}
}
var exportCode = function(filename, filepath, def, variable, eq, rest, uuid) {
var code = read(filepath)
// if there's a syntax error, report w/ proper stack trace
try {new Function(code)} catch (e) {
proc.exec("node " + filepath, function(e) {
if (e !== null && e.message !== error) {
error = e.message
console.log("\x1b[31m" + e.message + "\x1b[0m")
}
})
}
// disambiguate collisions
var ignored = {}
code.replace(include, function(match, def, variable, eq, dep) {
var filename = new Function("return " + dep).call()
var binding = modules[resolve(filepath, filename)]
if (binding != null) ignored[binding] = true
})
if (code.match(new RegExp("module\\.exports\\s*=\\s*" + variable + "\s*$", "m"))) ignored[variable] = true
for (var binding in bindings) {
if (!ignored[binding]) {
var before = code
code = code.replace(new RegExp("(\\b)" + binding + "\\b", "g"), binding + bindings[binding])
if (before !== code) bindings[binding]++
}
}
// fix strings that got mangled by collision disambiguation
var string = /(["'])((?:\\\1|.)*?)(\1)/g
var candidates = Object.keys(bindings).map(function(binding) {return binding + (bindings[binding] - 1)}).join("|")
code = code.replace(string, function(match, open, data, close) {
var variables = new RegExp(Object.keys(bindings).map(function(binding) {return binding + (bindings[binding] - 1)}).join("|"), "g")
var fixed = data.replace(variables, function(match) {
return match.replace(/\d+$/, "")
})
return open + fixed + close
})
//fix props
var props = new RegExp("(\\.\\s*)(" + candidates + ")|([\\{,]\\s*)(" + candidates + ")(\\s*:)", "gm")
code = code.replace(props, function(match, dot, a, pre, b, post) {
if (dot) return dot + a.replace(/\d+$/, "")
else return pre + b.replace(/\d+$/, "") + post
})
return code
.replace(/("|')use strict\1;?/gm, "") // remove extraneous "use strict"
.replace(/module\.exports\s*=\s*/gm, rest ? "var _" + uuid + eq : def + (rest ? "_" : "") + variable + eq) // export
+ (rest ? "\n" + def + variable + eq + "_" + uuid : "") // if `rest` is truthy, it means the expression is fluent or higher-order (e.g. require(path).foo or require(path)(foo)
}
var versionTag = "bleeding-edge"
var packageFile = __dirname + "/../package.json"
var code = process(path.resolve(input), read(input))
.replace(/^\s*((?:var|let|const|)[\t ]*)([\w_$\.]+)(\s*=\s*)(\2)(?=[\s]+(\w)|;|$)/gm, "") // remove assignments to self
.replace(/;+(\r|\n|$)/g, ";$1") // remove redundant semicolons
.replace(/(\r|\n)+/g, "\n").replace(/(\r|\n)$/, "") // remove multiline breaks
.replace(versionTag, isFile(packageFile) ? parse(packageFile).version : versionTag) // set version
code = ";(function() {\n" + code + "\n}());"
if (!isFile(output) || code !== read(output)) {
//try {new Function(code); console.log("build completed at " + new Date())} catch (e) {}
error = null
fs.writeFileSync(output, code, "utf8")
else {
// resolve as local dependency
return path.resolve(path.dirname(filepath), filename + ".js")
}
}
catch (e) {
console.error(e.message)
var exportCode = function(filename, filepath, def, variable, eq, rest, uuid) {
var code = read(filepath)
// if there's a syntax error, report w/ proper stack trace
try {new Function(code)} catch (e) {
proc.exec("node " + filepath, function(e) {
if (e !== null && e.message !== error) {
error = e.message
console.log("\x1b[31m" + e.message + "\x1b[0m")
}
})
}
// disambiguate collisions
var ignored = {}
code.replace(include, function(match, def, variable, eq, dep) {
var filename = new Function("return " + dep).call()
var binding = modules[resolve(filepath, filename)]
if (binding != null) ignored[binding] = true
})
if (code.match(new RegExp("module\\.exports\\s*=\\s*" + variable + "\s*$", "m"))) ignored[variable] = true
for (var binding in bindings) {
if (!ignored[binding]) {
var before = code
code = code.replace(new RegExp("(\\b)" + binding + "\\b", "g"), binding + bindings[binding])
if (before !== code) bindings[binding]++
}
}
// fix strings that got mangled by collision disambiguation
var string = /(["'])((?:\\\1|.)*?)(\1)/g
var candidates = Object.keys(bindings).map(function(binding) {return binding + (bindings[binding] - 1)}).join("|")
code = code.replace(string, function(match, open, data, close) {
var variables = new RegExp(Object.keys(bindings).map(function(binding) {return binding + (bindings[binding] - 1)}).join("|"), "g")
var fixed = data.replace(variables, function(match) {
return match.replace(/\d+$/, "")
})
return open + fixed + close
})
//fix props
var props = new RegExp("((?:[^:]\\/\\/.*)?\\.\\s*)(" + candidates + ")|([\\{,]\\s*)(" + candidates + ")(\\s*:)", "gm")
code = code.replace(props, function(match, dot, a, pre, b, post) {
if (dot && dot.indexOf("//") === 1) return match // Don't do anything because dot was matched in a comment
else if (dot) return dot + a.replace(/\d+$/, "")
else return pre + b.replace(/\d+$/, "") + post
})
return code
.replace(/("|')use strict\1;?/gm, "") // remove extraneous "use strict"
.replace(/module\.exports\s*=\s*/gm, rest ? "var _" + uuid + eq : def + (rest ? "_" : "") + variable + eq) // export
+ (rest ? "\n" + def + variable + eq + "_" + uuid : "") // if `rest` is truthy, it means the expression is fluent or higher-order (e.g. require(path).foo or require(path)(foo)
}
var versionTag = "bleeding-edge"
var packageFile = __dirname + "/../package.json"
var code = process(path.resolve(input), read(input))
.replace(/^\s*((?:var|let|const|)[\t ]*)([\w_$\.]+)(\s*=\s*)(\2)(?=[\s]+(\w)|;|$)/gm, "") // remove assignments to self
.replace(/;+(\r|\n|$)/g, ";$1") // remove redundant semicolons
.replace(/(\r|\n)+/g, "\n").replace(/(\r|\n)$/, "") // remove multiline breaks
.replace(versionTag, isFile(packageFile) ? parse(packageFile).version : versionTag) // set version
code = ";(function() {\n" + code + "\n}());"
if (!isFile(output) || code !== read(output)) {
//try {new Function(code); console.log("build completed at " + new Date())} catch (e) {}
error = null
fs.writeFileSync(output, code, "utf8")
}
}

View file

@ -5,7 +5,7 @@ var fs = require("fs");
var bundle = require("./bundle")
var minify = require("./minify")
var aliases = {o: "output", m: "minify", w: "watch", a: "aggressive"}
var aliases = {o: "output", m: "minify", w: "watch", a: "aggressive", s: "save"}
var params = {}
var args = process.argv.slice(2), command = null
for (var i = 0; i < args.length; i++) {
@ -27,8 +27,6 @@ function add(value) {
bundle(params.input, params.output, {watch: params.watch})
if (params.minify) {
minify(params.output, params.output, {watch: params.watch, advanced: params.aggressive}, function(stats) {
var readme, kb;
function format(n) {
return n.toString().replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,")
}
@ -36,14 +34,16 @@ if (params.minify) {
console.log("Original size: " + format(stats.originalGzipSize) + " bytes gzipped (" + format(stats.originalSize) + " bytes uncompressed)")
console.log("Compiled size: " + format(stats.compressedGzipSize) + " bytes gzipped (" + format(stats.compressedSize) + " bytes uncompressed)")
readme = fs.readFileSync("./README.md", "utf8")
kb = stats.compressedGzipSize / 1024
if (params.save) {
var readme = fs.readFileSync("./README.md", "utf8")
var kb = stats.compressedGzipSize / 1000
fs.writeFileSync("./README.md",
readme.replace(
/(<!-- size -->)(.+?)(<!-- \/size -->)/,
"$1" + (kb % 1 ? kb.toFixed(2) : kb) + " KB$3"
fs.writeFileSync("./README.md",
readme.replace(
/(<!-- size -->)(.+?)(<!-- \/size -->)/,
"$1" + (kb % 1 ? kb.toFixed(2) : kb) + " KB$3"
)
)
)
}
})
}

View file

@ -5,7 +5,7 @@ var bundle = require("../bundle")
var fs = require("fs")
var ns = "bundler/tests/"
var ns = "./"
function read(filepath) {
try {return fs.readFileSync(ns + filepath, "utf8")} catch (e) {/* ignore */}
}

View file

@ -17,7 +17,7 @@ Mithril does not provide any animation APIs per se, since these other options ar
### Animation on element creation
Animating an element via CSS when the element created couldn't be simpler. Just add an animation to a CSS class normally:
Animating an element via CSS when the element is created couldn't be simpler. Just add an animation to a CSS class normally:
```css
.fancy {animation:fade-in 0.5s;}
@ -41,7 +41,7 @@ m.mount(document.body, FancyComponent)
### Animation on element removal
The problem with animating before removing an element is that we must wait until the animation is complete before we can actually remove the element. Fortunately, Mithril offers a [`onbeforeremove`](lifecycle-methods.md#onbeforeremove) hook that allows us to defer the removal of an element.
The problem with animating before removing an element is that we must wait until the animation is complete before we can actually remove the element. Fortunately, Mithril offers the [`onbeforeremove`](lifecycle-methods.md#onbeforeremove) hook that allows us to defer the removal of an element.
Let's create an `exit` animation that fades `opacity` from 1 to 0.
@ -75,7 +75,7 @@ var FancyComponent = {
onbeforeremove: function(vnode) {
vnode.dom.classList.add("exit")
return new Promise(function(resolve) {
setTimeout(resolve, 500)
vnode.dom.addEventListener("animationend", resolve)
})
},
view: function() {
@ -86,7 +86,7 @@ var FancyComponent = {
`vnode.dom` points to the root DOM element of the component (`<div class="fancy">`). We use the classList API here to add an `exit` class to `<div class="fancy">`.
Then we return a [Promise](promise.md) that resolves after half a second. When we return a promise from `onbeforeremove`, Mithril waits until the promise is resolved and only then it removes the element. In this case, it waits half a second, giving the exit animation the exact time it needs to complete.
Then we return a [Promise](promise.md) that resolves when the `animationend` event fires. When we return a promise from `onbeforeremove`, Mithril waits until the promise is resolved and only then it removes the element. In this case, it waits for the exit animation to finish.
We can verify that both the enter and exit animations work by mounting the `Toggler` component:

View file

@ -1,5 +1,6 @@
# Change log
- [v2.0.0-rc](#v200rc)
- [v1.1.6](#v116)
- [v1.1.5](#v115)
- [v1.1.4](#v114)
@ -10,6 +11,86 @@
- [v1.0.1](#v101)
- [Migrating from v0.2.x](#migrating-from-v02x)
- [Older docs](http://mithril.js.org/archive/v0.2.5/index.html)
- [ospec change-log](../ospec/change-log.md)
---
### v2.0.0-rc
#### Breaking changes
- API: Component vnode `children` are not normalized into vnodes on ingestion; normalization only happens if and when they are ingested by the view ([#2155](https://github.com/MithrilJS/mithril.js/pull/2155/) (thanks to [@magikstm](https://github.com/magikstm) for related optimization [#2064](https://github.com/MithrilJS/mithril.js/pull/2064)))
- API: `m.redraw()` is always asynchronous ([#1592](https://github.com/MithrilJS/mithril.js/pull/1592))
- API: `m.mount()` will only render its own root when called, it will not trigger a `redraw()` ([#1592](https://github.com/MithrilJS/mithril.js/pull/1592))
- API: Assigning to `vnode.state` (as in `vnode.state = ...`) is no longer supported. Instead, an error is thrown if `vnode.state` changes upon the invocation of a lifecycle hook.
- API: `m.request` will no longer reject the Promise on server errors (eg. status >= 400) if the caller supplies an `extract` callback. This gives applications more control over handling server responses.
- hyperscript: when attributes have a `null` or `undefined` value, they are treated as if they were absent. [#1773](https://github.com/MithrilJS/mithril.js/issues/1773) ([#2174](https://github.com/MithrilJS/mithril.js/pull/2174))
- hyperscript: when an attribute is defined on both the first and second argument (as a CSS selector and an `attrs` field, respectively), the latter takes precedence, except for `class` attributes that are still added together. [#2172](https://github.com/MithrilJS/mithril.js/issues/2172) ([#2174](https://github.com/MithrilJS/mithril.js/pull/2174))
- stream: when a stream conditionally returns HALT, dependant stream will also end ([#2200](https://github.com/MithrilJS/mithril.js/pull/2200))
- render: remove some redundancy within the component initialization code ([#2213](https://github.com/MithrilJS/mithril.js/pull/2213))
- render: Align custom elements to work like normal elements, minus all the HTML-specific magic. ([#2221](https://github.com/MithrilJS/mithril.js/pull/2221))
- render: simplify component removal ([#2214](https://github.com/MithrilJS/mithril.js/pull/2214))
#### News
- API: Introduction of `m.redraw.sync()` ([#1592](https://github.com/MithrilJS/mithril.js/pull/1592))
- API: Event handlers may also be objects with `handleEvent` methods ([#1949](https://github.com/MithrilJS/mithril.js/pull/1949), [#2222](https://github.com/MithrilJS/mithril.js/pull/2222)).
- API: `m.route.link` accepts an optional `options` object ([#1930](https://github.com/MithrilJS/mithril.js/pull/1930))
- API: `m.request` better error message on JSON parse error - ([#2195](https://github.com/MithrilJS/mithril.js/pull/2195), [@codeclown](https://github.com/codeclown))
- API: `m.request` supports `timeout` as attr - ([#1966](https://github.com/MithrilJS/mithril.js/pull/1966))
- API: `m.request` supports `responseType` as attr - ([#2193](https://github.com/MithrilJS/mithril.js/pull/2193))
- Mocks: add limited support for the DOMParser API ([#2097](https://github.com/MithrilJS/mithril.js/pull/2097))
- API: add support for raw SVG in `m.trust()` string ([#2097](https://github.com/MithrilJS/mithril.js/pull/2097))
- render/core: remove the DOM nodes recycling pool ([#2122](https://github.com/MithrilJS/mithril.js/pull/2122))
- render/core: revamp the core diff engine, and introduce a longest-increasing-subsequence-based logic to minimize DOM operations when re-ordering keyed nodes.
#### Bug fixes
- API: `m.route.set()` causes all mount points to be redrawn ([#1592](https://github.com/MithrilJS/mithril.js/pull/1592))
- render/attrs: Using style objects in hyperscript calls will now properly diff style properties from one render to another as opposed to re-writing all element style properties every render.
- render/attrs All vnodes attributes are properly removed when absent or set to `null` or `undefined` [#1804](https://github.com/MithrilJS/mithril.js/issues/1804) [#2082](https://github.com/MithrilJS/mithril.js/issues/2082) ([#1865](https://github.com/MithrilJS/mithril.js/pull/1865), [#2130](https://github.com/MithrilJS/mithril.js/pull/2130))
- render/core: Render state correctly on select change event [#1916](https://github.com/MithrilJS/mithril.js/issues/1916) ([#1918](https://github.com/MithrilJS/mithril.js/pull/1918) [@robinchew](https://github.com/robinchew), [#2052](https://github.com/MithrilJS/mithril.js/pull/2052))
- render/core: fix various updateNodes/removeNodes issues when the pool and fragments are involved [#1990](https://github.com/MithrilJS/mithril.js/issues/1990), [#1991](https://github.com/MithrilJS/mithril.js/issues/1991), [#2003](https://github.com/MithrilJS/mithril.js/issues/2003), [#2021](https://github.com/MithrilJS/mithril.js/pull/2021)
- render/core: fix crashes when the keyed vnodes with the same `key` had different `tag` values [#2128](https://github.com/MithrilJS/mithril.js/issues/2128) [@JacksonJN](https://github.com/JacksonJN) ([#2130](https://github.com/MithrilJS/mithril.js/pull/2130))
- render/core: fix cached nodes behavior in some keyed diff scenarios [#2132](https://github.com/MithrilJS/mithril.js/issues/2132) ([#2130](https://github.com/MithrilJS/mithril.js/pull/2130))
- render/events: `addEventListener` and `removeEventListener` are always used to manage event subscriptions, preventing external interference.
- render/events: Event listeners allocate less memory, swap at low cost, and are properly diffed now when rendered via `m.mount()`/`m.redraw()`.
- render/events: `Object.prototype` properties can no longer interfere with event listener calls.
- render/events: Event handlers, when set to literally `undefined` (or any non-function), are now correctly removed.
- render/hooks: fixed an ommission that caused `oninit` to be called unnecessarily in some cases [#1992](https://github.com/MithrilJS/mithril.js/issues/1992)
- docs: tweaks: ([#2104](https://github.com/MithrilJS/mithril.js/pull/2104) [@mikeyb](https://github.com/mikeyb), [#2205](https://github.com/MithrilJS/mithril.js/pull/2205), [@cavemansspa](https://github.com/cavemansspa))
- render/core: avoid touching `Object.prototype.__proto__` setter with `key: "__proto__"` in certain situations ([#2251](https://github.com/MithrilJS/mithril.js/pull/2251))
---
### v1.1.7
- Stream references no longer magically coerce to their underlying values ([#2150](https://github.com/MithrilJS/mithril.js/pull/2150), breaking change: `mithril-stream@2.0.0`)
- Promise polyfill implementation separated from polyfilling logic.
- `PromisePolyfill` is now available on the exported/global `m`.
---
### v1.1.6
- core: render() function can no longer prevent from changing `document.activeElement` in lifecycle hooks ([#1988](https://github.com/MithrilJS/mithril.js/pull/1988), [@purplecode](https://github.com/purplecode))
- core: don't call `onremove` on the children of components that return null from the view [#1921](https://github.com/MithrilJS/mithril.js/issues/1921) [@octavore](https://github.com/octavore) ([#1922](https://github.com/MithrilJS/mithril.js/pull/1922))
- hypertext: correct handling of shared attributes object passed to `m()`. Will copy attributes when it's necessary [#1941](https://github.com/MithrilJS/mithril.js/issues/1941) [@s-ilya](https://github.com/s-ilya) ([#1942](https://github.com/MithrilJS/mithril.js/pull/1942))
---
### v1.1.5
- API: If a user sets the Content-Type header within a request's options, that value will be the entire header value rather than being appended to the default value ([#1924](https://github.com/MithrilJS/mithril.js/pull/1924))
---
### v1.1.4
#### Bug fixes:
- Fix IE bug where active element is null causing render function to throw error ([#1943](https://github.com/MithrilJS/mithril.js/pull/1943), [@JacksonJN](https://github.com/JacksonJN))
---
@ -75,10 +156,7 @@
- router: Don't overwrite the options object when redirecting from `onmatch with m.route.set()` [#1857](https://github.com/MithrilJS/mithril.js/issues/1857) ([#1889](https://github.com/MithrilJS/mithril.js/pull/1889))
- stream: Move the "use strict" directive inside the IIFE [#1831](https://github.com/MithrilJS/mithril.js/issues/1831) ([#1893](https://github.com/MithrilJS/mithril.js/pull/1893))
#### Ospec improvements
- Shell command: Ignore hidden directories and files ([#1855](https://github.com/MithrilJS/mithril.js/pull/1855) [@pdfernhout)](https://github.com/pdfernhout))
- Library: Add the possibility to name new test suites ([#1529](https://github.com/MithrilJS/mithril.js/pull/1529))
---
#### Docs / Repo maintenance

View file

@ -3,6 +3,7 @@
Here are some examples of Mithril in action
- [Animation](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/animation/mosaic.html)
- [Community Added Examples](https://how-to-mithril.js.org)
- [DBMonster](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/dbmonster/mithril/index.html)
- [Markdown Editor](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/editor/index.html)
- SVG: [Clock](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/svg/clock.html), [Ring](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/svg/ring.html), [Tiger](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/svg/tiger.html)

View file

@ -5,6 +5,7 @@
- [How it works](#how-it-works)
- [Flexibility](#flexibility)
- [CSS selectors](#css-selectors)
- [Attributes passed as the second argument](attributes-passed-as-the-second-argument)
- [DOM attributes](#dom-attributes)
- [Style attribute](#style-attribute)
- [Events](#events)
@ -144,7 +145,23 @@ m("a.link[href=/]", {
// <a href="/" class="link selected">Home</a>
```
If there are class names in both first and second arguments of `m()`, they are merged together as you would expect.
### Attributes passed as the second argument
You can pass attributes, properties, events and lifecycle hooks in the second, optional argument (see the next sections for details).
```JS
m("button", {
class: "my-button",
onclick: function() {/* ... */},
oncreate: function() {/* ... */}
})
```
If the value of such an attribute is `null` or `undefined`, it is treated as if the attribute was absent.
If there are class names in both first and second arguments of `m()`, they are merged together as you would expect. If the value of the class in the second argument is `null`or `undefined`, it is ignored.
If another attribute is present in both the first and the second argument, the second one takes precedence even if it is is `null` or `undefined`.
---
@ -161,6 +178,76 @@ m("input[readonly]")
m("input[readOnly]")
```
This even includes custom elements. For example, you can use [A-Frame](https://aframe.io/docs/0.8.0/introduction/) within Mithril, no problem!
```javascript
m("a-scene", [
m("a-box", {
position: "-1 0.5 -3",
rotation: "0 45 0",
color: "#4CC3D9",
}),
m("a-sphere", {
position: "0 1.25 -5",
radius: "1.25",
color: "#EF2D5E",
}),
m("a-cylinder", {
position: "1 0.75 -3",
radius: "0.5",
height: "1.5",
color: "#FFC65D",
}),
m("a-plane", {
position: "0 0 -4",
rotation: "-90 0 0",
width: "4",
height: "4",
color: "#7BC8A4",
}),
m("a-sky", {
color: "#ECECEC",
}),
])
```
And yes, this translates to both attributes and properties, and it works just like they would in the DOM. Using [Brick's `brick-deck`](http://brick.mozilla.io/docs/brick-deck) as an example, they have a `selected-index` attribute with a corresponding `selectedIndex` getter/setter property.
```javascript
m("brick-deck[selected-index=0]", [/* ... */]) // lowercase
m("brick-deck[selectedIndex=0]", [/* ... */]) // uppercase
// I know these look odd, but `brick-deck`'s `selectedIndex` property is a
// string, not a number.
m("brick-deck", {"selected-index": "0"}, [/* ... */])
m("brick-deck", {"selectedIndex": "0"}, [/* ... */])
```
For custom elements, it doesn't auto-stringify properties, in case they are objects, numbers, or some other non-string value. So assuming you had some custom element `my-special-element` that has an `elem.whitelist` array getter/setter property, you could do this, and it'd work as you'd expect:
```javascript
m("my-special-element", {
whitelist: [
"https://example.com",
"http://neverssl.com",
"https://google.com",
],
})
```
If you have classes or IDs for those elements, the shorthands still work as you would expect. To pull another A-Frame example:
```javascript
// These two are equivalent
m("a-entity#player")
m("a-entity", {id: "player"})
```
Do note that all the properties with magic semantics, like lifecycle attributes, `onevent` handlers, `key`s, `class`, and `style`, those are still treated the same way they are for normal HTML elements.
---
### Style attribute
@ -175,7 +262,7 @@ m("div[style=background:red]")
Using a string as a `style` would overwrite all inline styles in the element if it is redrawn, and not only CSS rules whose values have changed.
Mithril does not attempt to add units to number values.
Mithril does not attempt to add units to number values. It simply stringifies them.
---

View file

@ -56,7 +56,7 @@ Let's create an HTML file to follow along:
```markup
<body>
<script src="//unpkg.com/mithril/mithril.js"></script>
<script src="https://unpkg.com/mithril/mithril.js"></script>
<script>
var root = document.body

View file

@ -101,12 +101,14 @@ function ensureLinkIsValid(file, data) {
}
function initMocks() {
global.window = require("../test-utils/browserMock")() // eslint-disable-line global-require
/* eslint-disable global-require */
global.window = require("../test-utils/browserMock")()
global.document = window.document
global.m = require("../index") // eslint-disable-line global-require
global.o = require("../ospec/ospec") // eslint-disable-line global-require
global.stream = require("../stream") // eslint-disable-line global-require
global.m = require("../index")
global.o = require("../ospec/ospec")
global.stream = require("../stream")
global.alert = function() {}
/* eslint-enable global-require */
//routes consumed by request.md
global.window.$defineRoutes({

View file

@ -26,6 +26,8 @@ A [ES6 Promise](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/G
A Promise is a mechanism for working with asynchronous computations.
Mithril provides a polyfill when the environment does not support Promises. The polyfill can also be referenced specifically via `m.PromisePolyfill`.
---
### Signature

View file

@ -2,6 +2,8 @@
- [Description](#description)
- [Signature](#signature)
- [Static members](#static-members)
-[m.redraw.sync()](#mredrawsync)
- [How it works](#how-it-works)
---
@ -14,8 +16,6 @@ You DON'T need to call it if data is modified within the execution context of an
You DO need to call it in `setTimeout`/`setInterval`/`requestAnimationFrame` callbacks, or callbacks from 3rd party libraries.
Typically, `m.redraw` triggers an asynchronous redraws, but it may trigger synchronously if Mithril detects it's possible to improve performance by doing so (i.e. if no redraw was requested within the last animation frame). You should write code assuming that it always redraws asynchronously.
---
### Signature
@ -26,6 +26,16 @@ Argument | Type | Required | Description
----------- | -------------------- | -------- | ---
**returns** | | | Returns nothing
#### Static members
##### m.redraw.sync
`m.redraw.sync()`
Argument | Type | Required | Description
----------- | -------------------- | -------- | ---
**returns** | | | Returns nothing
---
### How it works
@ -34,4 +44,12 @@ When callbacks outside of Mithril run, you need to notify Mithril's rendering en
To trigger a redraw, call `m.redraw()`. Note that `m.redraw` only works if you used `m.mount` or `m.route`. If you rendered via `m.render`, you should use `m.render` to redraw.
You should not call m.redraw from a [lifecycle method](lifecycle-methods.md). Doing so will result in undefined behavior.
`m.redraw()` always triggers an asynchronous redraws, whereas `m.redraw.sync()` triggers a synchronous one. `m.redraw()` is tied to `window.requestAnimationFrame()` (we provide a fallback for IE9). It will thus typically fire at most 60 times per second. It may fire faster if your monitor has a higher refresh rate.
`m.redraw.sync()` is mostly intended to make videos play work in iOS. That only works in response to user-triggered events. It comes with several caveat:
- You should not call `m.redraw.sync()` from a [lifecycle method](lifecycle-methods.md) or the `view()` method of a component. Doing so will result in undefined behavior (it throws an error when possible).
- `m.redraw.sync()` called from an event handler can cause the DOM to be modified while an event is bubbling. Depending on the structure of the old and new DOM trees, the event can finish the bubbling phase in the new tree and trigger unwanted handlers.
- It is not throttled. One call to `m.redraw.sync()` causes immediately one `m.render()` call per root registered with [`m.mount()`](mount.md) or [`m.route()`](route.md).
`m.redraw()` doesn't have any of those issues, you can call it from wherever you like.

View file

@ -50,6 +50,7 @@ Argument | Type | Required | Descr
`options.password` | `String` | No | A password for HTTP authorization. Defaults to `undefined`. This option is provided for `XMLHttpRequest` compatibility, but you should avoid using it because it sends the password in plain text over the network.
`options.withCredentials` | `Boolean` | No | Whether to send cookies to 3rd party domains. Defaults to `false`
`options.timeout` | `Number` | No | The amount of milliseconds a request can take before automatically being [terminated](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/timeout). Defaults to `undefined`.
`options.responseType` | `String` | No | The expected [type](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType) of the response. Defaults to `undefined`.
`options.config` | `xhr = Function(xhr)` | No | Exposes the underlying XMLHttpRequest object for low-level configuration. Defaults to the [identity function](https://en.wikipedia.org/wiki/Identity_function).
`options.headers` | `Object` | No | Headers to append to the request before sending it (applied right before `options.config`).
`options.type` | `any = Function(any)` | No | A constructor to be applied to each object in the response. Defaults to the [identity function](https://en.wikipedia.org/wiki/Identity_function).

View file

@ -240,7 +240,7 @@ Now we can style the `UserList` component:
The CSS above is written using a convention of keeping all styles for a rule in a single line, in alphabetical order. This convention is designed to take maximum advantage of screen real estate, and makes it easier to scan the CSS selectors (since they are always on the left side) and their logical grouping, and it enforces predictable and uniform placement of CSS rules for each selector.
Obviously you can use whatever spacing/indentation convention you prefer. The example above is just an illustration of a not-so-widespread convention that has strong rationales behind it, but deviate from the more widespread cosmetic-oriented spacing conventions.
Obviously you can use whatever spacing/indentation convention you prefer. The example above is just an illustration of a not-so-widespread convention that has strong rationales behind it, but deviates from the more widespread cosmetic-oriented spacing conventions.
Reloading the browser window now should display some styled elements.

View file

@ -120,11 +120,13 @@ Argument | Type | Required | Description
Creates a new stream with the results of calling the function on every value in the stream with an accumulator and the incoming value.
Note that you can prevent dependent streams from being updated by returning the special value `stream.HALT` inside the accumulator function.
`stream = Stream.scan(fn, accumulator, stream)`
Argument | Type | Required | Description
------------- | -------------------------------- | -------- | ---
`fn` | `(accumulator, value) -> result` | Yes | A function that takes an accumulator and value parameter and returns a new accumulator value
`fn` | `(accumulator, value) -> result \| HALT` | Yes | A function that takes an accumulator and value parameter and returns a new accumulator value
`accumulator` | `any` | Yes | The starting value for the accumulator
`stream` | `Stream` | Yes | Stream containing the values
**returns** | `Stream` | | Returns a new stream containing the result
@ -502,13 +504,6 @@ var serialized = JSON.stringify(value)
console.log(serialized) // logs 123
```
Streams also implement a `valueOf` method that returns the value of the stream.
```javascript
var value = stream(123)
console.log("test " + value) // logs "test 123"
```
---
### Streams do not trigger rendering

View file

@ -17,5 +17,6 @@ m.parseQueryString = require("./querystring/parse")
m.buildQueryString = require("./querystring/build")
m.version = "bleeding-edge"
m.vnode = require("./render/vnode")
m.PromisePolyfill = require("./promise/polyfill")
module.exports = m

File diff suppressed because it is too large Load diff

92
mithril.min.js vendored
View file

@ -1,44 +1,48 @@
(function(){function B(b,d,f,g,e,n){return{tag:b,key:d,attrs:f,children:g,text:e,dom:n,domSize:void 0,state:void 0,_state:void 0,events:void 0,instance:void 0,skip:!1}}function N(b){for(var d in b)if(G.call(b,d))return!1;return!0}function D(b){var d=arguments[1],f=2;if(null==b||"string"!==typeof b&&"function"!==typeof b&&"function"!==typeof b.view)throw Error("The selector must be either a string or a component.");if("string"===typeof b){var g;if(!(g=O[b])){var e="div";for(var n=[],h={};g=Q.exec(b);){var q=
g[1],m=g[2];""===q&&""!==m?e=m:"#"===q?h.id=m:"."===q?n.push(m):"["===g[3][0]&&((q=g[6])&&(q=q.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")),"class"===g[4]?n.push(q):h[g[4]]=""===q?q:q||!0)}0<n.length&&(h.className=n.join(" "));g=O[b]={tag:e,attrs:h}}}if(null==d)d={};else if("object"!==typeof d||null!=d.tag||Array.isArray(d))d={},f=1;if(arguments.length===f+1)e=arguments[f],Array.isArray(e)||(e=[e]);else for(e=[];f<arguments.length;)e.push(arguments[f++]);f=B.normalizeChildren(e);if("string"===
typeof b){e=!1;var k,t;n=d.className||d["class"];if(!N(g.attrs)&&!N(d)){h={};for(var a in d)G.call(d,a)&&(h[a]=d[a]);d=h}for(a in g.attrs)G.call(g.attrs,a)&&(d[a]=g.attrs[a]);void 0!==n&&(void 0!==d["class"]&&(d["class"]=void 0,d.className=n),null!=g.attrs.className&&(d.className=g.attrs.className+" "+n));for(a in d)if(G.call(d,a)&&"key"!==a){e=!0;break}Array.isArray(f)&&1===f.length&&null!=f[0]&&"#"===f[0].tag?t=f[0].children:k=f;return B(g.tag,d.key,e?d:void 0,k,t)}return B(b,d.key,d,f)}function R(b){var d=
0,f=null,g="function"===typeof requestAnimationFrame?requestAnimationFrame:setTimeout;return function(){var e=Date.now();0===d||16<=e-d?(d=e,b()):null===f&&(f=g(function(){f=null;b();d=Date.now()},16-(e-d)))}}B.normalize=function(b){return Array.isArray(b)?B("[",void 0,void 0,B.normalizeChildren(b),void 0,void 0):null!=b&&"object"!==typeof b?B("#",void 0,void 0,!1===b?"":b,void 0,void 0):b};B.normalizeChildren=function(b){for(var d=0;d<b.length;d++)b[d]=B.normalize(b[d]);return b};var Q=/(?:(^|#|\.)([^#\.\[\]]+))|(\[(.+?)(?:\s*=\s*("|'|)((?:\\["'\]]|.)*?)\5)?\])/g,
O={},G={}.hasOwnProperty;D.trust=function(b){null==b&&(b="");return B("<",void 0,void 0,b,void 0,void 0)};D.fragment=function(b,d){return B("[",b.key,b,B.normalizeChildren(d),void 0,void 0)};var x=function(b){function d(b,a){return function E(d){var h;try{if(!a||null==d||"object"!==typeof d&&"function"!==typeof d||"function"!==typeof(h=d.then))k(function(){a||0!==b.length||console.error("Possible unhandled promise rejection:",d);for(var f=0;f<b.length;f++)b[f](d);e.length=0;n.length=0;m.state=a;m.retry=
function(){E(d)}});else{if(d===g)throw new TypeError("Promise can't be resolved w/ itself");f(h.bind(d))}}catch(S){q(S)}}}function f(b){function a(a){return function(b){0<d++||a(b)}}var d=0,f=a(q);try{b(a(h),f)}catch(E){f(E)}}if(!(this instanceof x))throw Error("Promise must be called with `new`");if("function"!==typeof b)throw new TypeError("executor must be a function");var g=this,e=[],n=[],h=d(e,!0),q=d(n,!1),m=g._instance={resolvers:e,rejectors:n},k="function"===typeof setImmediate?setImmediate:
setTimeout;f(b)};x.prototype.then=function(b,d){function f(b,d,f,h){d.push(function(a){if("function"!==typeof b)f(a);else try{e(b(a))}catch(w){n&&n(w)}});"function"===typeof g.retry&&h===g.state&&g.retry()}var g=this._instance,e,n,h=new x(function(b,d){e=b;n=d});f(b,g.resolvers,e,!0);f(d,g.rejectors,n,!1);return h};x.prototype["catch"]=function(b){return this.then(null,b)};x.resolve=function(b){return b instanceof x?b:new x(function(d){d(b)})};x.reject=function(b){return new x(function(d,f){f(b)})};
x.all=function(b){return new x(function(d,f){var g=b.length,e=0,n=[];if(0===b.length)d([]);else for(var h=0;h<b.length;h++)(function(h){function m(b){e++;n[h]=b;e===g&&d(n)}null==b[h]||"object"!==typeof b[h]&&"function"!==typeof b[h]||"function"!==typeof b[h].then?m(b[h]):b[h].then(m,f)})(h)})};x.race=function(b){return new x(function(d,f){for(var g=0;g<b.length;g++)b[g].then(d,f)})};"undefined"!==typeof window?("undefined"===typeof window.Promise&&(window.Promise=x),x=window.Promise):"undefined"!==
typeof global&&("undefined"===typeof global.Promise&&(global.Promise=x),x=global.Promise);var F=function(b){function d(b,g){if(Array.isArray(g))for(var e=0;e<g.length;e++)d(b+"["+e+"]",g[e]);else if("[object Object]"===Object.prototype.toString.call(g))for(e in g)d(b+"["+e+"]",g[e]);else f.push(encodeURIComponent(b)+(null!=g&&""!==g?"="+encodeURIComponent(g):""))}if("[object Object]"!==Object.prototype.toString.call(b))return"";var f=[],g;for(g in b)d(g,b[g]);return f.join("&")},T=/^file:\/\//i,L=
function(b,d){function f(){function a(){0===--b&&"function"===typeof t&&t()}var b=0;return function u(d){var f=d.then;d.then=function(){b++;var e=f.apply(d,arguments);e.then(a,function(d){a();if(0===b)throw d;});return u(e)};return d}}function g(a,b){if("string"===typeof a){var d=a;a=b||{};null==a.url&&(a.url=d)}return a}function e(a,b){if(null==b)return a;for(var d=a.match(/:[^\/]+/gi)||[],f=0;f<d.length;f++){var e=d[f].slice(1);null!=b[e]&&(a=a.replace(d[f],b[e]))}return a}function n(a,b){var d=
F(b);if(""!==d){var f=0>a.indexOf("?")?"?":"&";a+=f+d}return a}function h(a){try{return""!==a?JSON.parse(a):null}catch(w){throw Error(a);}}function q(a){return a.responseText}function m(a,b){if("function"===typeof a)if(Array.isArray(b))for(var d=0;d<b.length;d++)b[d]=new a(b[d]);else return new a(b);return b}var k=0,t;return{request:function(a,k){var t=f();a=g(a,k);var w=new d(function(d,f){null==a.method&&(a.method="GET");a.method=a.method.toUpperCase();var g="GET"===a.method||"TRACE"===a.method?
!1:"boolean"===typeof a.useBody?a.useBody:!0;"function"!==typeof a.serialize&&(a.serialize="undefined"!==typeof FormData&&a.data instanceof FormData?function(a){return a}:JSON.stringify);"function"!==typeof a.deserialize&&(a.deserialize=h);"function"!==typeof a.extract&&(a.extract=q);a.url=e(a.url,a.data);g?a.data=a.serialize(a.data):a.url=n(a.url,a.data);var k=new b.XMLHttpRequest,t=!1,w=k.abort;k.abort=function(){t=!0;w.call(k)};k.open(a.method,a.url,"boolean"===typeof a.async?a.async:!0,"string"===
typeof a.user?a.user:void 0,"string"===typeof a.password?a.password:void 0);a.serialize!==JSON.stringify||!g||a.headers&&a.headers.hasOwnProperty("Content-Type")||k.setRequestHeader("Content-Type","application/json; charset=utf-8");a.deserialize!==h||a.headers&&a.headers.hasOwnProperty("Accept")||k.setRequestHeader("Accept","application/json, text/*");a.withCredentials&&(k.withCredentials=a.withCredentials);for(var u in a.headers)({}).hasOwnProperty.call(a.headers,u)&&k.setRequestHeader(u,a.headers[u]);
"function"===typeof a.config&&(k=a.config(k,a)||k);k.onreadystatechange=function(){if(!t&&4===k.readyState)try{var b=a.extract!==q?a.extract(k,a):a.deserialize(a.extract(k,a));if(200<=k.status&&300>k.status||304===k.status||T.test(a.url))d(m(a.type,b));else{var l=Error(k.responseText),c;for(c in b)l[c]=b[c];f(l)}}catch(p){f(p)}};g&&null!=a.data?k.send(a.data):k.send()});return!0===a.background?w:t(w)},jsonp:function(a,h){var t=f();a=g(a,h);var q=new d(function(d,f){var g=a.callbackName||"_mithril_"+
Math.round(1E16*Math.random())+"_"+k++,h=b.document.createElement("script");b[g]=function(f){h.parentNode.removeChild(h);d(m(a.type,f));delete b[g]};h.onerror=function(){h.parentNode.removeChild(h);f(Error("JSONP request failed"));delete b[g]};null==a.data&&(a.data={});a.url=e(a.url,a.data);a.data[a.callbackKey||"callback"]=g;h.src=n(a.url,a.data);b.document.documentElement.appendChild(h)});return!0===a.background?q:t(q)},setCompletionCallback:function(a){t=a}}}(window,x),P=function(b){function d(l,
c,p,a,b,d,g){for(;p<a;p++){var v=c[p];null!=v&&f(l,v,b,g,d)}}function f(l,c,p,a,b){var v=c.tag;if("string"===typeof v)switch(c.state={},null!=c.attrs&&D(c.attrs,c,p),v){case "#":return c.dom=A.createTextNode(c.children),k(l,c.dom,b),c.dom;case "<":return g(l,c,b);case "[":var h=A.createDocumentFragment();null!=c.children&&(v=c.children,d(h,v,0,v.length,p,null,a));c.dom=h.firstChild;c.domSize=h.childNodes.length;k(l,h,b);return h;default:var m=c.tag,r=(v=c.attrs)&&v.is;m=(a=c.attrs&&c.attrs.xmlns||
G[c.tag]||a)?r?A.createElementNS(a,m,{is:r}):A.createElementNS(a,m):r?A.createElement(m,{is:r}):A.createElement(m);c.dom=m;if(null!=v)for(h in r=a,v)E(c,h,null,v[h],r);k(l,m,b);null!=c.attrs&&null!=c.attrs.contenteditable?t(c):(null!=c.text&&(""!==c.text?m.textContent=c.text:c.children=[B("#",void 0,void 0,c.text,void 0,void 0)]),null!=c.children&&(l=c.children,d(m,l,0,l.length,p,null,a),l=c.attrs,"select"===c.tag&&null!=l&&("value"in l&&E(c,"value",null,l.value,void 0),"selectedIndex"in l&&E(c,"selectedIndex",
null,l.selectedIndex,void 0))));return m}else return e(c,p),null!=c.instance?(p=f(l,c.instance,p,a,b),c.dom=c.instance.dom,c.domSize=null!=c.dom?c.instance.domSize:0,k(l,p,b),c=p):(c.domSize=0,c=K),c}function g(l,c,a){var p={caption:"table",thead:"table",tbody:"table",tfoot:"table",tr:"tbody",th:"tr",td:"tr",colgroup:"table",col:"colgroup"}[(c.children.match(/^\s*?<(\w+)/im)||[])[1]]||"div";p=A.createElement(p);p.innerHTML=c.children;c.dom=p.firstChild;c.domSize=p.childNodes.length;c=A.createDocumentFragment();
for(var b;b=p.firstChild;)c.appendChild(b);k(l,c,a);return c}function e(l,c){if("function"===typeof l.tag.view){l.state=Object.create(l.tag);var a=l.state.view;if(null!=a.$$reentrantLock$$)return K;a.$$reentrantLock$$=!0}else{l.state=void 0;a=l.tag;if(null!=a.$$reentrantLock$$)return K;a.$$reentrantLock$$=!0;l.state=null!=l.tag.prototype&&"function"===typeof l.tag.prototype.view?new l.tag(l):l.tag(l)}l._state=l.state;null!=l.attrs&&D(l.attrs,l,c);D(l._state,l,c);l.instance=B.normalize(l._state.view.call(l.state,
l));if(l.instance===l)throw Error("A view cannot return the vnode it received as argument");a.$$reentrantLock$$=null}function n(l,c,p,b,g,e,n){if(c!==p&&(null!=c||null!=p))if(null==c)d(l,p,0,p.length,g,e,n);else if(null==p)a(c,0,c.length,p);else{if(c.length===p.length){for(var v=!1,r=0;r<p.length;r++)if(null!=p[r]&&null!=c[r]){v=null==p[r].key&&null==c[r].key;break}if(v){for(r=0;r<c.length;r++)c[r]!==p[r]&&(null==c[r]&&null!=p[r]?f(l,p[r],g,n,m(c,r+1,e)):null==p[r]?a(c,r,r+1,p):h(l,c[r],p[r],g,m(c,
r+1,e),b,n));return}}if(!b)a:{if(null!=c.pool&&Math.abs(c.pool.length-p.length)<=Math.abs(c.length-p.length)&&(b=p[0]&&p[0].children&&p[0].children.length||0,Math.abs((c.pool[0]&&c.pool[0].children&&c.pool[0].children.length||0)-b)<=Math.abs((c[0]&&c[0].children&&c[0].children.length||0)-b))){b=!0;break a}b=!1}if(b){var t=c.pool;c=c.concat(c.pool)}r=v=0;for(var w=c.length-1,y=p.length-1,H;w>=v&&y>=r;){var u=c[v],z=p[r];if(u!==z||b)if(null==u)v++;else if(null==z)r++;else if(u.key===z.key){var C=null!=
t&&v>=c.length-t.length||null==t&&b;v++;r++;h(l,u,z,g,m(c,v,e),C,n);b&&u.tag===z.tag&&k(l,q(u),e)}else if(u=c[w],u!==z||b)if(null==u)w--;else if(null==z)r++;else if(u.key===z.key)C=null!=t&&w>=c.length-t.length||null==t&&b,h(l,u,z,g,m(c,w+1,e),C,n),(b||r<y)&&k(l,q(u),m(c,v,e)),w--,r++;else break;else w--,r++;else v++,r++}for(;w>=v&&y>=r;){u=c[w];z=p[y];if(u!==z||b)if(null==u)w--;else{if(null!=z)if(u.key===z.key)C=null!=t&&w>=c.length-t.length||null==t&&b,h(l,u,z,g,m(c,w+1,e),C,n),b&&u.tag===z.tag&&
k(l,q(u),e),null!=u.dom&&(e=u.dom),w--;else{if(!H){H=c;u=w;C={};var A;for(A=0;A<u;A++){var x=H[A];null!=x&&(x=x.key,null!=x&&(C[x]=A))}H=C}null!=z&&(u=H[z.key],null!=u?(C=c[u],h(l,C,z,g,m(c,w+1,e),b,n),k(l,q(C),e),c[u].skip=!0,null!=C.dom&&(e=C.dom)):e=f(l,z,g,n,e))}y--}else w--,y--;if(y<r)break}d(l,p,r,y+1,g,e,n);a(c,v,w+1,p)}}function h(l,c,a,b,d,m,k){var p=c.tag;if(p===a.tag){a.state=c.state;a._state=c._state;a.events=c.events;var v;if(v=!m){var C,z;null!=a.attrs&&"function"===typeof a.attrs.onbeforeupdate&&
(C=a.attrs.onbeforeupdate.call(a.state,a,c));"string"!==typeof a.tag&&"function"===typeof a._state.onbeforeupdate&&(z=a._state.onbeforeupdate.call(a.state,a,c));void 0===C&&void 0===z||C||z?v=!1:(a.dom=c.dom,a.domSize=c.domSize,a.instance=c.instance,v=!0)}if(!v)if("string"===typeof p)switch(null!=a.attrs&&(m?(a.state={},D(a.attrs,a,b)):J(a.attrs,a,b)),p){case "#":c.children.toString()!==a.children.toString()&&(c.dom.nodeValue=a.children);a.dom=c.dom;break;case "<":c.children!==a.children?(q(c),g(l,
a,d)):(a.dom=c.dom,a.domSize=c.domSize);break;case "[":n(l,c.children,a.children,m,b,d,k);c=0;b=a.children;a.dom=null;if(null!=b){for(m=0;m<b.length;m++){var y=b[m];null!=y&&null!=y.dom&&(null==a.dom&&(a.dom=y.dom),c+=y.domSize||1)}1!==c&&(a.domSize=c)}break;default:l=a.dom=c.dom;k=a.attrs&&a.attrs.xmlns||G[a.tag]||k;"textarea"===a.tag&&(null==a.attrs&&(a.attrs={}),null!=a.text&&(a.attrs.value=a.text,a.text=void 0));d=c.attrs;p=a.attrs;v=k;if(null!=p)for(y in p)E(a,y,d&&d[y],p[y],v);if(null!=d)for(y in d)null!=
p&&y in p||("className"===y&&(y="class"),"o"!==y[0]||"n"!==y[1]||u(y)?"key"!==y&&a.dom.removeAttribute(y):x(a,y,void 0));null!=a.attrs&&null!=a.attrs.contenteditable?t(a):null!=c.text&&null!=a.text&&""!==a.text?c.text.toString()!==a.text.toString()&&(c.dom.firstChild.nodeValue=a.text):(null!=c.text&&(c.children=[B("#",void 0,void 0,c.text,void 0,c.dom.firstChild)]),null!=a.text&&(a.children=[B("#",void 0,void 0,a.text,void 0,void 0)]),n(l,c.children,a.children,m,b,null,k))}else{if(m)e(a,b);else{a.instance=
B.normalize(a._state.view.call(a.state,a));if(a.instance===a)throw Error("A view cannot return the vnode it received as argument");null!=a.attrs&&J(a.attrs,a,b);J(a._state,a,b)}null!=a.instance?(null==c.instance?f(l,a.instance,b,k,d):h(l,c.instance,a.instance,b,d,m,k),a.dom=a.instance.dom,a.domSize=a.instance.domSize):null!=c.instance?(w(c.instance,null),a.dom=void 0,a.domSize=0):(a.dom=c.dom,a.domSize=c.domSize)}}else w(c,null),f(l,a,b,k,d)}function q(a){var c=a.domSize;if(null!=c||null==a.dom){var b=
A.createDocumentFragment();if(0<c){for(a=a.dom;--c;)b.appendChild(a.nextSibling);b.insertBefore(a,b.firstChild)}return b}return a.dom}function m(a,c,b){for(;c<a.length;c++)if(null!=a[c]&&null!=a[c].dom)return a[c].dom;return b}function k(a,c,b){b&&b.parentNode?a.insertBefore(c,b):a.appendChild(c)}function t(a){var c=a.children;if(null!=c&&1===c.length&&"<"===c[0].tag)c=c[0].children,a.dom.innerHTML!==c&&(a.dom.innerHTML=c);else if(null!=a.text||null!=c&&0!==c.length)throw Error("Child node of a contenteditable must be trusted");
}function a(a,c,b,d){for(;c<b;c++){var l=a[c];null!=l&&(l.skip?l.skip=!1:w(l,d))}}function w(a,c){function b(){if(++d===l&&(C(a),a.dom)){var b=a.domSize||1;if(1<b)for(var e=a.dom;--b;){var g=e.nextSibling,f=g.parentNode;null!=f&&f.removeChild(g)}b=a.dom;e=b.parentNode;null!=e&&e.removeChild(b);if(b=null!=c&&null==a.domSize)b=a.attrs,b=!(null!=b&&(b.oncreate||b.onupdate||b.onbeforeremove||b.onremove));b&&"string"===typeof a.tag&&(c.pool?c.pool.push(a):c.pool=[a])}}var l=1,d=0;if(a.attrs&&"function"===
typeof a.attrs.onbeforeremove){var e=a.attrs.onbeforeremove.call(a.state,a);null!=e&&"function"===typeof e.then&&(l++,e.then(b,b))}"string"!==typeof a.tag&&"function"===typeof a._state.onbeforeremove&&(e=a._state.onbeforeremove.call(a.state,a),null!=e&&"function"===typeof e.then&&(l++,e.then(b,b)));b()}function C(a){a.attrs&&"function"===typeof a.attrs.onremove&&a.attrs.onremove.call(a.state,a);if("string"!==typeof a.tag)"function"===typeof a._state.onremove&&a._state.onremove.call(a.state,a),null!=
a.instance&&C(a.instance);else if(a=a.children,Array.isArray(a))for(var c=0;c<a.length;c++){var b=a[c];null!=b&&C(b)}}function E(a,b,d,e,g){var c=a.dom;if("key"!==b&&"is"!==b&&(d!==e||"value"===b||"checked"===b||"selectedIndex"===b||"selected"===b&&a.dom===A.activeElement||"object"===typeof e)&&"undefined"!==typeof e&&!u(b)){var l=b.indexOf(":");if(-1<l&&"xlink"===b.substr(0,l))c.setAttributeNS("http://www.w3.org/1999/xlink",b.slice(l+1),e);else if("o"===b[0]&&"n"===b[1]&&"function"===typeof e)x(a,
b,e);else if("style"===b)if(a=d,a===e&&(c.style.cssText="",a=null),null==e)c.style.cssText="";else if("string"===typeof e)c.style.cssText=e;else{"string"===typeof a&&(c.style.cssText="");for(var f in e)c.style[f]=e[f];if(null!=a&&"string"!==typeof a)for(f in a)f in e||(c.style[f]="")}else if(b in c&&"href"!==b&&"list"!==b&&"form"!==b&&"width"!==b&&"height"!==b&&void 0===g&&!(a.attrs.is||-1<a.tag.indexOf("-"))){if("value"===b){f=""+e;if(("input"===a.tag||"textarea"===a.tag)&&a.dom.value===f&&a.dom===
A.activeElement)return;if("select"===a.tag)if(null===e){if(-1===a.dom.selectedIndex&&a.dom===A.activeElement)return}else if(null!==d&&a.dom.value===f&&a.dom===A.activeElement)return;if("option"===a.tag&&null!=d&&a.dom.value===f)return}"input"===a.tag&&"type"===b?c.setAttribute(b,e):c[b]=e}else"boolean"===typeof e?e?c.setAttribute(b,""):c.removeAttribute(b):c.setAttribute("className"===b?"class":b,e)}}function u(a){return"oninit"===a||"oncreate"===a||"onupdate"===a||"onremove"===a||"onbeforeremove"===
a||"onbeforeupdate"===a}function x(a,b,d){var c=a.dom,e="function"!==typeof F?d:function(a){var b=d.call(c,a);F.call(c,a);return b};if(b in c)c[b]="function"===typeof d?e:null;else{var f=b.slice(2);void 0===a.events&&(a.events={});a.events[b]!==e&&(null!=a.events[b]&&c.removeEventListener(f,a.events[b],!1),"function"===typeof d&&(a.events[b]=e,c.addEventListener(f,a.events[b],!1)))}}function D(a,b,d){"function"===typeof a.oninit&&a.oninit.call(b.state,b);"function"===typeof a.oncreate&&d.push(a.oncreate.bind(b.state,
b))}function J(a,b,d){"function"===typeof a.onupdate&&d.push(a.onupdate.bind(b.state,b))}var A=b.document,K=A.createDocumentFragment(),G={svg:"http://www.w3.org/2000/svg",math:"http://www.w3.org/1998/Math/MathML"},F;return{render:function(a,b){if(!a)throw Error("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.");var c=[],d=A.activeElement,e=a.namespaceURI;null==a.vnodes&&(a.textContent="");Array.isArray(b)||(b=[b]);n(a,a.vnodes,B.normalizeChildren(b),!1,c,null,"http://www.w3.org/1999/xhtml"===
e?void 0:e);a.vnodes=b;null!=d&&A.activeElement!==d&&d.focus();for(d=0;d<c.length;d++)c[d]()},setEventCallback:function(a){return F=a}}},I=function(b){function d(b){b=g.indexOf(b);-1<b&&g.splice(b,2)}function f(){for(var b=1;b<g.length;b+=2)g[b]()}b=P(b);b.setEventCallback(function(b){!1===b.redraw?b.redraw=void 0:f()});var g=[];return{subscribe:function(b,f){d(b);g.push(b,R(f))},unsubscribe:d,redraw:f,render:b.render}}(window);L.setCompletionCallback(I.redraw);D.mount=function(b){return function(d,
f){if(null===f)b.render(d,[]),b.unsubscribe(d);else{if(null==f.view&&"function"!==typeof f)throw Error("m.mount(element, component) expects a component, not a vnode");b.subscribe(d,function(){b.render(d,B(f))});b.redraw()}}}(I);var U=x,M=function(b){if(""===b||null==b)return{};"?"===b.charAt(0)&&(b=b.slice(1));b=b.split("&");for(var d={},f={},g=0;g<b.length;g++){var e=b[g].split("="),n=decodeURIComponent(e[0]);e=2===e.length?decodeURIComponent(e[1]):"";"true"===e?e=!0:"false"===e&&(e=!1);var h=n.split(/\]\[?|\[/),
q=d;-1<n.indexOf("[")&&h.pop();for(var m=0;m<h.length;m++){n=h[m];var k=h[m+1];k=""==k||!isNaN(parseInt(k,10));var t=m===h.length-1;""===n&&(n=h.slice(0,m).join(),null==f[n]&&(f[n]=0),n=f[n]++);null==q[n]&&(q[n]=t?e:k?[]:{});q=q[n]}}return d},V=function(b){function d(d){var e=b.location[d].replace(/(?:%[a-f89][a-f0-9])+/gim,decodeURIComponent);"pathname"===d&&"/"!==e[0]&&(e="/"+e);return e}function f(b){return function(){null==h&&(h=n(function(){h=null;b()}))}}function g(b,d,e){var a=b.indexOf("?"),
f=b.indexOf("#"),g=-1<a?a:-1<f?f:b.length;if(-1<a){a=M(b.slice(a+1,-1<f?f:b.length));for(var h in a)d[h]=a[h]}if(-1<f)for(h in d=M(b.slice(f+1)),d)e[h]=d[h];return b.slice(0,g)}var e="function"===typeof b.history.pushState,n="function"===typeof setImmediate?setImmediate:setTimeout,h,q={prefix:"#!",getPath:function(){switch(q.prefix.charAt(0)){case "#":return d("hash").slice(q.prefix.length);case "?":return d("search").slice(q.prefix.length)+d("hash");default:return d("pathname").slice(q.prefix.length)+
d("search")+d("hash")}},setPath:function(d,f,h){var a={},k={};d=g(d,a,k);if(null!=f){for(var m in f)a[m]=f[m];d=d.replace(/:([^\/]+)/g,function(b,d){delete a[d];return f[d]})}(m=F(a))&&(d+="?"+m);(k=F(k))&&(d+="#"+k);e?(k=h?h.state:null,m=h?h.title:null,b.onpopstate(),h&&h.replace?b.history.replaceState(k,m,q.prefix+d):b.history.pushState(k,m,q.prefix+d)):b.location.href=q.prefix+d},defineRoutes:function(d,h,n){function a(){var a=q.getPath(),e={},f=g(a,e,e),k=b.history.state;if(null!=k)for(var m in k)e[m]=
k[m];for(var t in d)if(k=new RegExp("^"+t.replace(/:[^\/]+?\.{3}/g,"(.*?)").replace(/:[^\/]+/g,"([^\\/]+)")+"/?$"),k.test(f)){f.replace(k,function(){for(var b=t.match(/:[^\/]+/g)||[],f=[].slice.call(arguments,1,-2),g=0;g<b.length;g++)e[b[g].replace(/:|\./g,"")]=decodeURIComponent(f[g]);h(d[t],e,a,t)});return}n(a,e)}e?b.onpopstate=f(a):"#"===q.prefix.charAt(0)&&(b.onhashchange=a);a()}};return q};D.route=function(b,d){var f=V(b),g=function(b){return b},e,n,h,q,m,k=function(b,a,k){if(null==b)throw Error("Ensure the DOM element that was passed to `m.route` is not undefined");
var t=function(){null!=e&&d.render(b,e(B(n,h.key,h)))},w=function(b){if(b!==a)f.setPath(a,null,{replace:!0});else throw Error("Could not resolve default route "+a);};f.defineRoutes(k,function(a,b,d){var f=m=function(a,k){f===m&&(n=null==k||"function"!==typeof k.view&&"function"!==typeof k?"div":k,h=b,q=d,m=null,e=(a.render||g).bind(a),t())};a.view||"function"===typeof a?f({},a):a.onmatch?U.resolve(a.onmatch(b,d)).then(function(b){f(a,b)},w):f(a,"div")},w);d.subscribe(b,t)};k.set=function(b,a,d){null!=
m&&(d=d||{},d.replace=!0);m=null;f.setPath(b,a,d)};k.get=function(){return q};k.prefix=function(b){f.prefix=b};k.link=function(b){b.dom.setAttribute("href",f.prefix+b.attrs.href);b.dom.onclick=function(a){a.ctrlKey||a.metaKey||a.shiftKey||2===a.which||(a.preventDefault(),a.redraw=!1,a=this.getAttribute("href"),0===a.indexOf(f.prefix)&&(a=a.slice(f.prefix.length)),k.set(a,void 0,void 0))}};k.param=function(b){return"undefined"!==typeof h&&"undefined"!==typeof b?h[b]:h};return k}(window,I);D.withAttr=
function(b,d,f){return function(g){d.call(f||this,b in g.currentTarget?g.currentTarget[b]:g.currentTarget.getAttribute(b))}};var W=P(window);D.render=W.render;D.redraw=I.redraw;D.request=L.request;D.jsonp=L.jsonp;D.parseQueryString=M;D.buildQueryString=F;D.version="1.1.6";D.vnode=B;"undefined"!==typeof module?module.exports=D:window.m=D})();
(function(){function x(a,d,e,g,q,k){return{tag:a,key:d,attrs:e,children:g,text:q,dom:k,domSize:void 0,state:void 0,events:void 0,instance:void 0}}function Q(a){for(var d in a)if(G.call(a,d))return!1;return!0}function w(a){if(null==a||"string"!==typeof a&&"function"!==typeof a&&"function"!==typeof a.view)throw Error("The selector must be either a string or a component.");var d=arguments[1],e=2;if(null==d)d={};else if("object"!==typeof d||null!=d.tag||Array.isArray(d))d={},e=1;if(arguments.length===
e+1){var g=arguments[e];Array.isArray(g)||(g=[g])}else for(g=[];e<arguments.length;)g.push(arguments[e++]);if("string"===typeof a){if(!(e=R[a])){for(var q="div",k=[],h={};e=W.exec(a);){var m=e[1],n=e[2];""===m&&""!==n?q=n:"#"===m?h.id=n:"."===m?k.push(n):"["===e[3][0]&&((m=e[6])&&(m=m.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")),"class"===e[4]?k.push(m):h[e[4]]=""===m?m:m||!0)}0<k.length&&(h.className=k.join(" "));e=R[a]={tag:q,attrs:h}}g=x.normalizeChildren(g);q=!1;var l,C;k=G.call(d,"class")?
"class":"className";h=d[k];if(!Q(e.attrs)&&!Q(d)){m={};for(var c in d)G.call(d,c)&&(m[c]=d[c]);d=m}for(c in e.attrs)G.call(e.attrs,c)&&"className"!==c&&!G.call(d,c)&&(d[c]=e.attrs[c]);if(null!=h||null!=e.attrs.className)d.className=null!=h?null!=e.attrs.className?e.attrs.className+" "+h:h:null!=e.attrs.className?e.attrs.className:null;"class"===k&&(d["class"]=null);for(c in d)if(G.call(d,c)&&"key"!==c){q=!0;break}Array.isArray(g)&&1===g.length&&null!=g[0]&&"#"===g[0].tag?C=g[0].children:l=g;return x(e.tag,
d.key,q?d:null,l,C)}return x(a,d.key,d,g)}function X(a){var d=0,e=null,g="function"===typeof requestAnimationFrame?requestAnimationFrame:setTimeout;return function(){var q=Date.now()-d;null===e&&(e=g(function(){e=null;a();d=Date.now()},16-q))}}x.normalize=function(a){return Array.isArray(a)?x("[",void 0,void 0,x.normalizeChildren(a),void 0,void 0):null!=a&&"object"!==typeof a?x("#",void 0,void 0,!1===a?"":a,void 0,void 0):a};x.normalizeChildren=function(a){for(var d=[],e=0;e<a.length;e++)d[e]=x.normalize(a[e]);
return d};var W=/(?:(^|#|\.)([^#\.\[\]]+))|(\[(.+?)(?:\s*=\s*("|'|)((?:\\["'\]]|.)*?)\5)?\])/g,R={},G={}.hasOwnProperty;w.trust=function(a){null==a&&(a="");return x("<",void 0,void 0,a,void 0,void 0)};w.fragment=function(a,d){return x("[",a.key,a,x.normalizeChildren(d),void 0,void 0)};var p=function(a){function d(a,c){return function K(d){var F;try{if(!c||null==d||"object"!==typeof d&&"function"!==typeof d||"function"!==typeof(F=d.then))l(function(){c||0!==a.length||console.error("Possible unhandled promise rejection:",
d);for(var e=0;e<a.length;e++)a[e](d);q.length=0;k.length=0;n.state=c;n.retry=function(){K(d)}});else{if(d===g)throw new TypeError("Promise can't be resolved w/ itself");e(F.bind(d))}}catch(Y){m(Y)}}}function e(a){function c(c){return function(a){0<d++||c(a)}}var d=0,e=c(m);try{a(c(h),e)}catch(K){e(K)}}if(!(this instanceof p))throw Error("Promise must be called with `new`");if("function"!==typeof a)throw new TypeError("executor must be a function");var g=this,q=[],k=[],h=d(q,!0),m=d(k,!1),n=g._instance=
{resolvers:q,rejectors:k},l="function"===typeof setImmediate?setImmediate:setTimeout;e(a)};p.prototype.then=function(a,d){function e(a,d,e,h){d.push(function(c){if("function"!==typeof a)e(c);else try{q(a(c))}catch(y){k&&k(y)}});"function"===typeof g.retry&&h===g.state&&g.retry()}var g=this._instance,q,k,h=new p(function(a,d){q=a;k=d});e(a,g.resolvers,q,!0);e(d,g.rejectors,k,!1);return h};p.prototype["catch"]=function(a){return this.then(null,a)};p.prototype["finally"]=function(a){return this.then(function(d){return p.resolve(a()).then(function(){return d})},
function(d){return p.resolve(a()).then(function(){return p.reject(d)})})};p.resolve=function(a){return a instanceof p?a:new p(function(d){d(a)})};p.reject=function(a){return new p(function(d,e){e(a)})};p.all=function(a){return new p(function(d,e){var g=a.length,q=0,k=[];if(0===a.length)d([]);else for(var h=0;h<a.length;h++)(function(m){function n(a){q++;k[m]=a;q===g&&d(k)}null==a[m]||"object"!==typeof a[m]&&"function"!==typeof a[m]||"function"!==typeof a[m].then?n(a[m]):a[m].then(n,e)})(h)})};p.race=
function(a){return new p(function(d,e){for(var g=0;g<a.length;g++)a[g].then(d,e)})};"undefined"!==typeof window?("undefined"===typeof window.Promise?window.Promise=p:window.Promise.prototype["finally"]||(window.Promise.prototype["finally"]=p.prototype["finally"]),p=window.Promise):"undefined"!==typeof global&&("undefined"===typeof global.Promise?global.Promise=p:global.Promise.prototype["finally"]||(global.Promise.prototype["finally"]=p.prototype["finally"]),p=global.Promise);var H=function(a){function d(a,
g){if(Array.isArray(g))for(var h=0;h<g.length;h++)d(a+"["+h+"]",g[h]);else if("[object Object]"===Object.prototype.toString.call(g))for(h in g)d(a+"["+h+"]",g[h]);else e.push(encodeURIComponent(a)+(null!=g&&""!==g?"="+encodeURIComponent(g):""))}if("[object Object]"!==Object.prototype.toString.call(a))return"";var e=[],g;for(g in a)d(g,a[g]);return e.join("&")},Z=/^file:\/\//i,O=function(a,d){function e(){function c(){0===--a&&"function"===typeof C&&C()}var a=0;return function D(d){var e=d.then;d.then=
function(){a++;var g=e.apply(d,arguments);g.then(c,function(d){c();if(0===a)throw d;});return D(g)};return d}}function g(c,a){if("string"===typeof c){var d=c;c=a||{};null==c.url&&(c.url=d)}return c}function q(c,a){if(null==a)return c;for(var d=c.match(/:[^\/]+/gi)||[],e=0;e<d.length;e++){var g=d[e].slice(1);null!=a[g]&&(c=c.replace(d[e],a[g]))}return c}function k(c,a){var d=H(a);if(""!==d){var e=0>c.indexOf("?")?"?":"&";c+=e+d}return c}function h(c){try{return""!==c?JSON.parse(c):null}catch(y){throw Error("Invalid JSON: "+
c);}}function m(c){return c.responseText}function n(c,a){if("function"===typeof c)if(Array.isArray(a))for(var d=0;d<a.length;d++)a[d]=new c(a[d]);else return new c(a);return a}var l=0,C;return{request:function(c,l){var F=e();c=g(c,l);var y=new d(function(d,e){null==c.method&&(c.method="GET");c.method=c.method.toUpperCase();var g="GET"===c.method||"TRACE"===c.method?!1:"boolean"===typeof c.useBody?c.useBody:!0;"function"!==typeof c.serialize&&(c.serialize="undefined"!==typeof FormData&&c.data instanceof
FormData?function(c){return c}:JSON.stringify);"function"!==typeof c.deserialize&&(c.deserialize=h);"function"!==typeof c.extract&&(c.extract=m);c.url=q(c.url,c.data);g?c.data=c.serialize(c.data):c.url=k(c.url,c.data);var l=new a.XMLHttpRequest,F=!1,y=l.abort;l.abort=function(){F=!0;y.call(l)};l.open(c.method,c.url,"boolean"===typeof c.async?c.async:!0,"string"===typeof c.user?c.user:void 0,"string"===typeof c.password?c.password:void 0);c.serialize!==JSON.stringify||!g||c.headers&&c.headers.hasOwnProperty("Content-Type")||
l.setRequestHeader("Content-Type","application/json; charset=utf-8");c.deserialize!==h||c.headers&&c.headers.hasOwnProperty("Accept")||l.setRequestHeader("Accept","application/json, text/*");c.withCredentials&&(l.withCredentials=c.withCredentials);c.timeout&&(l.timeout=c.timeout);c.responseType&&(l.responseType=c.responseType);for(var C in c.headers)({}).hasOwnProperty.call(c.headers,C)&&l.setRequestHeader(C,c.headers[C]);"function"===typeof c.config&&(l=c.config(l,c)||l);l.onreadystatechange=function(){if(!F&&
4===l.readyState)try{var a=c.extract!==m?c.extract(l,c):c.deserialize(c.extract(l,c));if(c.extract!==m||200<=l.status&&300>l.status||304===l.status||Z.test(c.url))d(n(c.type,a));else{var g=Error(l.responseText);g.code=l.status;g.response=a;e(g)}}catch(aa){e(aa)}};g&&null!=c.data?l.send(c.data):l.send()});return!0===c.background?y:F(y)},jsonp:function(c,m){var h=e();c=g(c,m);var y=new d(function(d,e){var g=c.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+l++,m=a.document.createElement("script");
a[g]=function(e){m.parentNode.removeChild(m);d(n(c.type,e));delete a[g]};m.onerror=function(){m.parentNode.removeChild(m);e(Error("JSONP request failed"));delete a[g]};null==c.data&&(c.data={});c.url=q(c.url,c.data);c.data[c.callbackKey||"callback"]=g;m.src=k(c.url,c.data);a.document.documentElement.appendChild(m)});return!0===c.background?y:h(y)},setCompletionCallback:function(c){C=c}}}(window,p),V=function(a){function d(t,b){if(t.state!==b)throw Error("`vnode.state` must not be modified");}function e(t){var b=
t.state;try{return this.apply(b,arguments)}finally{d(t,b)}}function g(t,b,f,c,a,d,e){for(;f<c;f++){var g=b[f];null!=g&&q(t,g,a,e,d)}}function q(t,b,f,a,d){var n=b.tag;if("string"===typeof n)switch(b.state={},null!=b.attrs&&N(b.attrs,b,f),n){case "#":b.dom=E.createTextNode(b.children);C(t,b.dom,d);break;case "<":k(t,b,a,d);break;case "[":n=E.createDocumentFragment();if(null!=b.children){var l=b.children;g(n,l,0,l.length,f,null,a)}b.dom=n.firstChild;b.domSize=n.childNodes.length;C(t,n,d);break;default:var m=
b.tag,u=(n=b.attrs)&&n.is;m=(a=b.attrs&&b.attrs.xmlns||G[b.tag]||a)?u?E.createElementNS(a,m,{is:u}):E.createElementNS(a,m):u?E.createElement(m,{is:u}):E.createElement(m);b.dom=m;if(null!=n)for(l in u=a,n)D(b,l,null,n[l],u);C(t,m,d);if(null!=n&&null!=n.contenteditable)c(b);else if(null!=b.text&&(""!==b.text?m.textContent=b.text:b.children=[x("#",void 0,void 0,b.text,void 0,void 0)]),null!=b.children&&(t=b.children,g(m,t,0,t.length,f,null,a),"select"===b.tag&&null!=n)){if("value"in n)if(null===n.value)-1!==
b.dom.selectedIndex&&(b.dom.value=null);else if(f=""+n.value,b.dom.value!==f||-1===b.dom.selectedIndex)b.dom.value=f;"selectedIndex"in n&&D(b,"selectedIndex",null,n.selectedIndex,void 0)}}else{a:{if("function"===typeof b.tag.view){b.state=Object.create(b.tag);n=b.state.view;if(null!=n.$$reentrantLock$$)break a;n.$$reentrantLock$$=!0}else{b.state=void 0;n=b.tag;if(null!=n.$$reentrantLock$$)break a;n.$$reentrantLock$$=!0;b.state=null!=b.tag.prototype&&"function"===typeof b.tag.prototype.view?new b.tag(b):
b.tag(b)}null!=b.attrs&&N(b.attrs,b,f);N(b.state,b,f);b.instance=x.normalize(e.call(b.state.view,b));if(b.instance===b)throw Error("A view cannot return the vnode it received as argument");n.$$reentrantLock$$=null}null!=b.instance?(q(t,b.instance,f,a,d),b.dom=b.instance.dom,b.domSize=null!=b.dom?b.instance.domSize:0):b.domSize=0}}function k(t,b,f,c){var a=b.children.match(/^\s*?<(\w+)/im)||[];a=E.createElement(I[a[1]]||"div");"http://www.w3.org/2000/svg"===f?(a.innerHTML='<svg xmlns="http://www.w3.org/2000/svg">'+
b.children+"</svg>",a=a.firstChild):a.innerHTML=b.children;b.dom=a.firstChild;b.domSize=a.childNodes.length;for(b=E.createDocumentFragment();f=a.firstChild;)b.appendChild(f);C(t,b,c)}function h(t,b,f,c,a,d){if(b!==f&&(null!=b||null!=f))if(null==b||0===b.length)g(t,f,0,f.length,c,a,d);else if(null==f||0===f.length)y(b,0,b.length);else{for(var e=0,u=0,h=null,k=null;u<b.length;u++)if(null!=b[u]){h=null!=b[u].key;break}for(;e<f.length;e++)if(null!=f[e]){k=null!=f[e].key;break}if(null!==k||null!=h)if(h!==
k)y(b,u,b.length),g(t,f,e,f.length,c,a,d);else if(k){k=b.length-1;h=f.length-1;for(var p,r,v,A,z,B;k>=u&&h>=e;)if(A=b[k],z=f[h],null==A)k--;else if(null==z)h--;else if(A.key===z.key)A!==z&&m(t,A,z,c,a,d),null!=z.dom&&(a=z.dom),k--,h--;else break;for(;k>=u&&h>=e;)if(r=b[u],v=f[e],null==r)u++;else if(null==v)e++;else if(r.key===v.key)u++,e++,r!==v&&m(t,r,v,c,l(b,u,a),d);else break;for(;k>=u&&h>=e;){if(null==r)u++;else if(null==v)e++;else if(null==A)k--;else if(null==z)h--;else if(e===h)break;else{if(r.key!==
z.key||A.key!==v.key)break;B=l(b,u,a);C(t,n(A),B);A!==v&&m(t,A,v,c,B,d);++e<=--h&&C(t,n(r),a);r!==z&&m(t,r,z,c,a,d);null!=z.dom&&(a=z.dom);u++;k--}A=b[k];z=f[h];r=b[u];v=f[e]}for(;k>=u&&h>=e;){if(null==A)k--;else if(null==z)h--;else if(A.key===z.key)A!==z&&m(t,A,z,c,a,d),null!=z.dom&&(a=z.dom),k--,h--;else break;A=b[k];z=f[h]}if(e>h)y(b,u,k+1);else if(u>k)g(t,f,e,h+1,c,a,d);else{v=a;A=h-e+1;r=Array(A);var x=2147483647,w=0;for(B=0;B<A;B++)r[B]=-1;for(B=h;B>=e;B--){if(null==p){p=b;A=u;z=k+1;for(var D=
Object.create(null);A<z;A++){var J=p[A];null!=J&&(J=J.key,null!=J&&(D[J]=A))}p=D}z=f[B];null!=z&&(D=p[z.key],null!=D&&(x=D<x?D:-1,r[B-e]=D,A=b[D],b[D]=null,A!==z&&m(t,A,z,c,a,d),null!=z.dom&&(a=z.dom),w++))}a=v;w!==k-u+1&&y(b,u,k+1);if(0===w)g(t,f,e,h+1,c,a,d);else if(-1===x){u=r.slice();b=[];b.push(0);k=0;for(B=r.length;k<B;++k)if(-1!==r[k])if(v=b[b.length-1],r[v]<r[k])u[k]=v,b.push(k);else{v=0;for(x=b.length-1;v<x;)w=(v+x)/2|0,r[b[w]]<r[k]?v=w+1:x=w;r[k]<r[b[v]]&&(0<v&&(u[k]=b[v-1]),b[v]=k)}v=b.length;
for(x=b[v-1];0<v--;)b[v]=x,x=u[x];u=b.length-1;for(B=h;B>=e;B--)v=f[B],-1===r[B-e]?q(t,v,c,d,a):b[u]===B-e?u--:C(t,n(v),a),null!=v.dom&&(a=f[B].dom)}else for(B=h;B>=e;B--)v=f[B],-1===r[B-e]&&q(t,v,c,d,a),null!=v.dom&&(a=f[B].dom)}}else{h=b.length<f.length?b.length:f.length;for(e=e<u?e:u;e<h;e++)r=b[e],v=f[e],r===v||null==r&&null==v||(null==r?q(t,v,c,d,l(b,e+1,a)):null==v?F(r):m(t,r,v,c,l(b,e+1,a),d));b.length>h&&y(b,e,b.length);f.length>h&&g(t,f,e,f.length,c,a,d)}}}function m(a,b,f,d,g,l){var t=b.tag;
if(t===f.tag){f.state=b.state;f.events=b.events;var u;var y;null!=f.attrs&&"function"===typeof f.attrs.onbeforeupdate&&(u=e.call(f.attrs.onbeforeupdate,f,b));"string"!==typeof f.tag&&"function"===typeof f.state.onbeforeupdate&&(y=e.call(f.state.onbeforeupdate,f,b));void 0===u&&void 0===y||u||y?u=!1:(f.dom=b.dom,f.domSize=b.domSize,f.instance=b.instance,u=!0);if(!u)if("string"===typeof t)switch(null!=f.attrs&&L(f.attrs,f,d),t){case "#":b.children.toString()!==f.children.toString()&&(b.dom.nodeValue=
f.children);f.dom=b.dom;break;case "<":b.children!==f.children?(n(b),k(a,f,l,g)):(f.dom=b.dom,f.domSize=b.domSize);break;case "[":h(a,b.children,f.children,d,g,l);b=0;d=f.children;f.dom=null;if(null!=d){for(var p=0;p<d.length;p++)a=d[p],null!=a&&null!=a.dom&&(null==f.dom&&(f.dom=a.dom),b+=a.domSize||1);1!==b&&(f.domSize=b)}break;default:a=f.dom=b.dom;l=f.attrs&&f.attrs.xmlns||G[f.tag]||l;"textarea"===f.tag&&(null==f.attrs&&(f.attrs={}),null!=f.text&&(f.attrs.value=f.text,f.text=void 0));g=b.attrs;
t=f.attrs;u=l;if(null!=t)for(p in t)D(f,p,g&&g[p],t[p],u);var C;if(null!=g)for(p in g)if(null!=(C=g[p])&&(null==t||null==t[p])){y=f;var r=p,v=u;"key"===r||"is"===r||null==C||w(r)||("o"!==r[0]||"n"!==r[1]||w(r)?"style"===r?T(y.dom,C,null):!S(y,r,v)||"className"===r||"option"===y.tag&&"value"===r||"input"===y.tag&&"type"===r?(v=r.indexOf(":"),-1!==v&&(r=r.slice(v+1)),!1!==C&&y.dom.removeAttribute("className"===r?"class":r)):y.dom[r]=null:U(y,r,void 0))}null!=f.attrs&&null!=f.attrs.contenteditable?c(f):
null!=b.text&&null!=f.text&&""!==f.text?b.text.toString()!==f.text.toString()&&(b.dom.firstChild.nodeValue=f.text):(null!=b.text&&(b.children=[x("#",void 0,void 0,b.text,void 0,b.dom.firstChild)]),null!=f.text&&(f.children=[x("#",void 0,void 0,f.text,void 0,void 0)]),h(a,b.children,f.children,d,null,l))}else{f.instance=x.normalize(e.call(f.state.view,f));if(f.instance===f)throw Error("A view cannot return the vnode it received as argument");null!=f.attrs&&L(f.attrs,f,d);L(f.state,f,d);null!=f.instance?
(null==b.instance?q(a,f.instance,d,l,g):m(a,b.instance,f.instance,d,g,l),f.dom=f.instance.dom,f.domSize=f.instance.domSize):null!=b.instance?(F(b.instance),f.dom=void 0,f.domSize=0):(f.dom=b.dom,f.domSize=b.domSize)}}else F(b),q(a,f,d,l,g)}function n(a){var b=a.domSize;if(null!=b||null==a.dom){var c=E.createDocumentFragment();if(0<b){for(a=a.dom;--b;)c.appendChild(a.nextSibling);c.insertBefore(a,c.firstChild)}return c}return a.dom}function l(a,b,c){for(;b<a.length;b++)if(null!=a[b]&&null!=a[b].dom)return a[b].dom;
return c}function C(a,b,c){null!=c?a.insertBefore(b,c):a.appendChild(b)}function c(a){var b=a.children;if(null!=b&&1===b.length&&"<"===b[0].tag)b=b[0].children,a.dom.innerHTML!==b&&(a.dom.innerHTML=b);else if(null!=a.text||null!=b&&0!==b.length)throw Error("Child node of a contenteditable must be trusted");}function y(a,b,c){for(;b<c;b++){var f=a[b];null!=f&&F(f)}}function F(a){function b(){if(++t===c&&(d(a,g),p(a),a.dom)){for(var b=a.dom.parentNode,f=a.domSize||1;--f;)b.removeChild(a.dom.nextSibling);
b.removeChild(a.dom)}}var c=1,t=0,g=a.state;if(a.attrs&&"function"===typeof a.attrs.onbeforeremove){var n=e.call(a.attrs.onbeforeremove,a);null!=n&&"function"===typeof n.then&&(c++,n.then(b,b))}"string"!==typeof a.tag&&"function"===typeof a.state.onbeforeremove&&(n=e.call(a.state.onbeforeremove,a),null!=n&&"function"===typeof n.then&&(c++,n.then(b,b)));b()}function p(a){a.attrs&&"function"===typeof a.attrs.onremove&&e.call(a.attrs.onremove,a);if("string"!==typeof a.tag)"function"===typeof a.state.onremove&&
e.call(a.state.onremove,a),null!=a.instance&&p(a.instance);else if(a=a.children,Array.isArray(a))for(var b=0;b<a.length;b++){var c=a[b];null!=c&&p(c)}}function D(a,b,c,d,e){if("key"!==b&&"is"!==b&&null!=d&&!w(b)&&(c!==d||"value"===b||"checked"===b||"selectedIndex"===b||"selected"===b&&a.dom===E.activeElement||"option"===a.tag&&a.dom.parentNode===E.activeElement||"object"===typeof d)){if("o"===b[0]&&"n"===b[1])return U(a,b,d);"xlink:"===b.slice(0,6)?a.dom.setAttributeNS("http://www.w3.org/1999/xlink",
b.slice(6),d):"style"===b?T(a.dom,c,d):S(a,b,e)?"value"===b&&(("input"===a.tag||"textarea"===a.tag)&&a.dom.value===""+d&&a.dom===E.activeElement||"select"===a.tag&&null!==c&&a.dom.value===""+d||"option"===a.tag&&null!==c&&a.dom.value===""+d)||("input"===a.tag&&"type"===b?a.dom.setAttribute(b,d):a.dom[b]=d):"boolean"===typeof d?d?a.dom.setAttribute(b,""):a.dom.removeAttribute(b):a.dom.setAttribute("className"===b?"class":b,d)}}function w(a){return"oninit"===a||"oncreate"===a||"onupdate"===a||"onremove"===
a||"onbeforeremove"===a||"onbeforeupdate"===a}function S(a,b,c){return void 0===c&&(-1<a.tag.indexOf("-")||null!=a.attrs&&a.attrs.is||"href"!==b&&"list"!==b&&"form"!==b&&"width"!==b&&"height"!==b)&&b in a.dom}function T(a,b,c){if(null!=b&&null!=c&&"object"===typeof b&&"object"===typeof c&&c!==b){for(var d in c)c[d]!==b[d]&&(a.style[d]=c[d]);for(d in b)d in c||(a.style[d]="")}else if(b===c&&(a.style.cssText="",b=null),null==c)a.style.cssText="";else if("string"===typeof c)a.style.cssText=c;else for(d in"string"===
typeof b&&(a.style.cssText=""),c)a.style[d]=c[d]}function M(){}function U(a,b,c){null!=a.events?a.events[b]!==c&&(null==c||"function"!==typeof c&&"object"!==typeof c?(null!=a.events[b]&&a.dom.removeEventListener(b.slice(2),a.events,!1),a.events[b]=void 0):(null==a.events[b]&&a.dom.addEventListener(b.slice(2),a.events,!1),a.events[b]=c)):null==c||"function"!==typeof c&&"object"!==typeof c||(a.events=new M,a.dom.addEventListener(b.slice(2),a.events,!1),a.events[b]=c)}function N(a,b,c){"function"===
typeof a.oninit&&e.call(a.oninit,b);"function"===typeof a.oncreate&&c.push(e.bind(a.oncreate,b))}function L(a,b,c){"function"===typeof a.onupdate&&c.push(e.bind(a.onupdate,b))}var E=a.document,G={svg:"http://www.w3.org/2000/svg",math:"http://www.w3.org/1998/Math/MathML"},H,I={caption:"table",thead:"table",tbody:"table",tfoot:"table",tr:"tbody",th:"tr",td:"tr",colgroup:"table",col:"colgroup"};M.prototype=Object.create(null);M.prototype.handleEvent=function(a){var b=this["on"+a.type],c;"function"===
typeof b?c=b.call(a.target,a):"function"===typeof b.handleEvent&&b.handleEvent(a);"function"===typeof H&&H.call(a.target,a);!1===c&&(a.preventDefault(),a.stopPropagation())};return{render:function(a,b){if(!a)throw Error("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.");var c=[],d=E.activeElement,e=a.namespaceURI;null==a.vnodes&&(a.textContent="");Array.isArray(b)||(b=[b]);h(a,a.vnodes,x.normalizeChildren(b),c,null,"http://www.w3.org/1999/xhtml"===e?void 0:e);a.vnodes=
b;null!=d&&E.activeElement!==d&&"function"===typeof d.focus&&d.focus();for(d=0;d<c.length;d++)c[d]()},setEventCallback:function(a){return H=a}}},I=function(a,d){function e(a){a=k.indexOf(a);-1<a&&k.splice(a,2)}function g(){if(h)throw Error("Nested m.redraw.sync() call");h=!0;for(var a=1;a<k.length;a+=2)try{k[a]()}catch(l){"undefined"!==typeof console&&console.error(l)}h=!1}var q=V(a);q.setEventCallback(function(a){!1===a.redraw?a.redraw=void 0:m()});var k=[],h=!1,m=(d||X)(g);m.sync=g;return{subscribe:function(a,
d){e(a);k.push(a,d)},unsubscribe:e,redraw:m,render:q.render}}(window);O.setCompletionCallback(I.redraw);w.mount=function(a){return function(d,e){if(null===e)a.render(d,[]),a.unsubscribe(d);else{if(null==e.view&&"function"!==typeof e)throw Error("m.mount(element, component) expects a component, not a vnode");var g=function(){a.render(d,x(e))};a.subscribe(d,g);g()}}}(I);var ba=p,P=function(a){if(""===a||null==a)return{};"?"===a.charAt(0)&&(a=a.slice(1));a=a.split("&");for(var d={},e={},g=0;g<a.length;g++){var q=
a[g].split("="),k=decodeURIComponent(q[0]);q=2===q.length?decodeURIComponent(q[1]):"";"true"===q?q=!0:"false"===q&&(q=!1);var h=k.split(/\]\[?|\[/),m=d;-1<k.indexOf("[")&&h.pop();for(var n=0;n<h.length;n++){k=h[n];var l=h[n+1];l=""==l||!isNaN(parseInt(l,10));var p=n===h.length-1;""===k&&(k=h.slice(0,n).join(),null==e[k]&&(e[k]=0),k=e[k]++);null==m[k]&&(m[k]=p?q:l?[]:{});m=m[k]}}return d},ca=function(a){function d(d){var e=a.location[d].replace(/(?:%[a-f89][a-f0-9])+/gim,decodeURIComponent);"pathname"===
d&&"/"!==e[0]&&(e="/"+e);return e}function e(a){return function(){null==h&&(h=k(function(){h=null;a()}))}}function g(a,d,e){var c=a.indexOf("?"),g=a.indexOf("#"),h=-1<c?c:-1<g?g:a.length;if(-1<c){c=P(a.slice(c+1,-1<g?g:a.length));for(var k in c)d[k]=c[k]}if(-1<g)for(k in d=P(a.slice(g+1)),d)e[k]=d[k];return a.slice(0,h)}var q="function"===typeof a.history.pushState,k="function"===typeof setImmediate?setImmediate:setTimeout,h,m={prefix:"#!",getPath:function(){switch(m.prefix.charAt(0)){case "#":return d("hash").slice(m.prefix.length);
case "?":return d("search").slice(m.prefix.length)+d("hash");default:return d("pathname").slice(m.prefix.length)+d("search")+d("hash")}},setPath:function(d,e,k){var c={},h={};d=g(d,c,h);if(null!=e){for(var l in e)c[l]=e[l];d=d.replace(/:([^\/]+)/g,function(a,d){delete c[d];return e[d]})}(l=H(c))&&(d+="?"+l);(h=H(h))&&(d+="#"+h);q?(h=k?k.state:null,l=k?k.title:null,a.onpopstate(),k&&k.replace?a.history.replaceState(h,l,m.prefix+d):a.history.pushState(h,l,m.prefix+d)):a.location.href=m.prefix+d},defineRoutes:function(d,
k,h){function c(){var c=m.getPath(),e={},l=g(c,e,e),n=a.history.state;if(null!=n)for(var q in n)e[q]=n[q];for(var p in d)if(n=new RegExp("^"+p.replace(/:[^\/]+?\.{3}/g,"(.*?)").replace(/:[^\/]+/g,"([^\\/]+)")+"/?$"),n.test(l)){l.replace(n,function(){for(var a=p.match(/:[^\/]+/g)||[],g=[].slice.call(arguments,1,-2),h=0;h<a.length;h++)e[a[h].replace(/:|\./g,"")]=decodeURIComponent(g[h]);k(d[p],e,c,p)});return}h(c,e)}q?a.onpopstate=e(c):"#"===m.prefix.charAt(0)&&(a.onhashchange=c);c()}};return m};w.route=
function(a,d){var e=ca(a),g=function(a){return a},p,k,h,m,n,l=function(a,l,q){function c(){null!=p&&d.render(a,p(x(k,h.key,h)))}if(null==a)throw Error("Ensure the DOM element that was passed to `m.route` is not undefined");var y=function(){c();y=d.redraw};d.subscribe(a,c);var w=function(a){if(a!==l)e.setPath(l,null,{replace:!0});else throw Error("Could not resolve default route "+l);};e.defineRoutes(q,function(a,c,d){var e=n=function(a,l){e===n&&(k=null==l||"function"!==typeof l.view&&"function"!==
typeof l?"div":l,h=c,m=d,n=null,p=(a.render||g).bind(a),y())};a.view||"function"===typeof a?e({},a):a.onmatch?ba.resolve(a.onmatch(c,d)).then(function(c){e(a,c)},w):e(a,"div")},w)};l.set=function(a,d,g){null!=n&&(g=g||{},g.replace=!0);n=null;e.setPath(a,d,g)};l.get=function(){return m};l.prefix=function(a){e.prefix=a};var w=function(a,d){d.dom.setAttribute("href",e.prefix+d.attrs.href);d.dom.onclick=function(c){c.ctrlKey||c.metaKey||c.shiftKey||2===c.which||(c.preventDefault(),c.redraw=!1,c=this.getAttribute("href"),
0===c.indexOf(e.prefix)&&(c=c.slice(e.prefix.length)),l.set(c,void 0,a))}};l.link=function(a){return null==a.tag?w.bind(w,a):w({},a)};l.param=function(a){return"undefined"!==typeof h&&"undefined"!==typeof a?h[a]:h};return l}(window,I);w.withAttr=function(a,d,e){return function(g){d.call(e||this,a in g.currentTarget?g.currentTarget[a]:g.currentTarget.getAttribute(a))}};var da=V(window);w.render=da.render;w.redraw=I.redraw;w.request=O.request;w.jsonp=O.jsonp;w.parseQueryString=P;w.buildQueryString=
H;w.version="1.1.3";w.vnode=x;w.PromisePolyfill=p;"undefined"!==typeof module?module.exports=w:window.m=w})();

View file

@ -1,13 +1,13 @@
ospec [![NPM Version](https://img.shields.io/npm/v/ospec.svg)](https://www.npmjs.com/package/ospec) [![NPM License](https://img.shields.io/npm/l/ospec.svg)](https://www.npmjs.com/package/ospec)
=====
[About](#about) | [Usage](#usage) | [API](#api) | [Goals](#goals)
[About](#about) | [Usage](#usage) | [CLI](#command-line-interface) | [API](#api) | [Goals](#goals)
Noiseless testing framework
## About
- ~330 LOC including the CLI runner
- ~360 LOC including the CLI runner
- terser and faster test code than with mocha, jasmine or tape
- test code reads like bullet points
- assertion code follows [SVO](https://en.wikipedia.org/wiki/Subjectverbobject) structure in present tense for terseness and readability
@ -111,6 +111,7 @@ o.spec("call()", function() {
o(spy.callCount).equals(1)
o(spy.args[0]).equals(1)
o(spy.calls[0]).deepEquals([1])
})
})
```
@ -164,30 +165,54 @@ o("promise test", async function() {
})
```
By default, asynchronous tests time out after 20ms. This can be changed on a per-test basis using the `timeout` argument:
#### Timeout delays
By default, asynchronous tests time out after 200ms. You can change that default for the current test suite and
its children by using the `o.specTimeout(delay)` function.
```javascript
o.spec("a spec that must timeout quickly", function(done, timeout) {
// wait 20ms before bailing out of the tests of this suite and
// its descendants
o.specTimeout(20)
o("some test", function(done) {
setTimeout(done, 10) // this will pass
})
o.spec("a child suite where the delay also applies", function () {
o("some test", function(done) {
setTimeout(done, 30) // this will time out.
})
})
})
o.spec("a spec that uses the default delay", function() {
// ...
})
```
This can also be changed on a per-test basis using the `o.timeout(delay)` function from within a test:
```javascript
o("setTimeout calls callback", function(done, timeout) {
timeout(50) //wait 50ms before bailing out of the test
o.timeout(500) //wait 500ms before bailing out of the test
setTimeout(done, 30)
setTimeout(done, 300)
})
```
Note that the `timeout` function call must be the first statement in its test. This currently does not work for promise tests. You can combine both methods to do this:
Note that the `o.timeout` function call must be the first statement in its test. It also works with Promise-returning tests:
```javascript
o("promise test", function(done, timeout) {
timeout(1000)
someOtherAsyncFunctionThatTakes900ms().then(done)
o("promise test", function() {
o.timeout(1000)
return someOtherAsyncFunctionThatTakes900ms()
})
```
```javascript
o("promise test", async function(done, timeout) {
timeout(1000)
o("promise test", async function() {
o.timeout(1000)
await someOtherAsyncFunctionThatTakes900ms()
done()
})
```
@ -247,20 +272,31 @@ o.spec("math", function() {
})
```
### Running only one test
### Running only some tests
A test can be temporarily made to run exclusively by calling `o.only()` instead of `o`. This is useful when troubleshooting regressions, to zero-in on a failing test, and to avoid saturating console log w/ irrelevant debug information.
One or more tests can be temporarily made to run exclusively by calling `o.only()` instead of `o`. This is useful when troubleshooting regressions, to zero-in on a failing test, and to avoid saturating console log w/ irrelevant debug information.
```javascript
o.spec("math", function() {
// will not run
o("addition", function() {
o(1 + 1).equals(2)
})
//only this test will be run, regardless of how many groups there are
// this test will be run, regardless of how many groups there are
o.only("subtraction", function() {
o(1 - 1).notEquals(2)
})
// will not run
o("multiplication", function() {
o(2 * 2).equals(4)
})
// this test will be run, regardless of how many groups there are
o.only("division", function() {
o(6 / 2).notEquals(2)
})
})
```
@ -288,28 +324,64 @@ _o("a test", function() {
_o.run()
```
### Running the test suite from the command-line
## Command Line Interface
ospec will automatically evaluate all `*.js` files in any folder named `/tests`.
`o.run()` is automatically called by the cli - no need to call it in your test code.
#### Create an npm script in your package:
Create a script in your package.json:
```
"scripts": {
...
"test": "ospec",
...
}
```
...and run it from the command line:
```
$ npm test
$ npm test
```
#### Direct use from the command line
**NOTE:** `o.run()` is automatically called by the cli - no need to call it in your test code.
Ospec doesn't work when installed globally. Using global scripts is generally a bad idea since you can end up with different, incompatible versions of the same package installed locally and globally.
### CLI Options
Running ospec without arguments is equivalent to running `ospec '**/tests/**/*.js'`. In english, this tells ospec to evaluate all `*.js` files in any sub-folder named `tests/` (the `node_modules` folder is always excluded).
If you wish to change this behavior, just provide one or more glob match patterns:
```
ospec '**/spec/**/*.js' '**/*.spec.js'
```
You can also provide ignore patterns (note: always add `--ignore` AFTER match patterns):
```
ospec --ignore 'folder1/**' 'folder2/**'
```
Finally, you may choose to load files or modules before any tests run (**note:** always add `--require` AFTER match patterns):
```
ospec --require esm
```
Here's an example of mixing them all together:
```
ospec '**/*.test.js' --ignore 'folder1/**' --require esm ./my-file.js
```
### Run ospec directly from the command line:
ospec comes with an executable named `ospec`. NPM auto-installs local binaries to `./node_modules/.bin/`. You can run ospec by running `./node_modules/.bin/ospec` from your project root, but there are more convenient methods to do so that we will soon describe.
ospec doesn't work when installed globally (`npm install -g`). Using global scripts is generally a bad idea since you can end up with different, incompatible versions of the same package installed locally and globally.
Here are different ways of running ospec from the command line. This knowledge applies to not just ospec, but any locally installed npm binary.
#### npx
If you're using a recent version of npm (v5+), you can use run `npx ospec` from your project folder.
#### npm-run
If you're using a recent version of npm (v5+), you can use run `npx ospec` from your project folder.
@ -325,6 +397,16 @@ Then, from a project that has ospec installed as a (dev) dependency:
npm-run ospec
```
#### PATH
If you understand how your system's PATH works (e.g. for [OSX](https://coolestguidesontheplanet.com/add-shell-path-osx/)), then you can add the following to your PATH...
```
export PATH=./node_modules/.bin:$PATH
```
...and you'll be able to run `ospec` without npx, npm, etc. This one-time setup will also work with other binaries across all your node projects, as long as you run binaries from the root of your projects.
---
## API
@ -491,15 +573,23 @@ o.run(function(results) {
---
### Boolean result.pass
### Boolean|Null result.pass
True if the test passed. **No other keys will exist on the result if this value is true.**
- `true` if the assertion passed.
- `false` if the assertion failed.
- `null` if the assertion was incomplete (`o("partial assertion) // and that's it`).
---
### Error result.error
The `Error` object explaining the reason behind a failure.
The `Error` object explaining the reason behind a failure. If the assertion failed, the stack will point to the actuall error. If the assertion did pass or was incomplete, this field is identical to `result.testError`.
---
### Error result.testError
An `Error` object whose stack points to the test definition that wraps the assertion. Useful as a fallback because in some async cases the main may not point to test code.
---
@ -527,7 +617,7 @@ o.spec("message", function() {
### String result.context
A `>`-separated string showing the structure of the test specification.
In case of failure, a `>`-separated string showing the structure of the test specification.
In the below example, `result.context` would be `testing > rocks`.
```javascript

70
ospec/bin/ospec Normal file → Executable file
View file

@ -1,48 +1,42 @@
#!/usr/bin/env node
"use strict"
var fs = require("fs")
var path = require("path")
var o = require("../ospec")
var path = require("path")
var glob = require("glob")
function traverseDirectory(pathname, callback) {
pathname = pathname.replace(/\\/g, "/")
return new Promise(function(resolve, reject) {
fs.lstat(pathname, function(err, stat) {
if (err) reject(err)
if (stat && stat.isDirectory()) {
fs.readdir(pathname, function(err, pathnames) {
if (err) reject(err)
var promises = []
for (var i = 0; i < pathnames.length; i++) {
if (pathnames[i] === "node_modules") continue
if (pathnames[i][0] === ".") continue
pathnames[i] = path.join(pathname, pathnames[i])
promises.push(traverseDirectory(pathnames[i], callback))
}
callback(pathname, stat, pathnames)
resolve(Promise.all(promises))
})
}
else {
callback(pathname, stat)
resolve(pathname)
}
})
function parseArgs(argv) {
argv = ["--globs"].concat(argv.slice(2))
var args = {}
var name
argv.forEach(function(arg) {
if (/^--/.test(arg)) {
name = arg.substr(2)
args[name] = args[name] || []
} else {
args[name].push(arg)
}
})
return args
}
traverseDirectory(".", function(pathname) {
if (pathname.match(/(?:^|\/)tests\/.*\.js$/)) {
require(path.normalize(process.cwd()) + "/" + pathname) // eslint-disable-line global-require
}
})
.then(o.run)
.catch(function(e) {
console.log(e.stack)
var args = parseArgs(process.argv)
var globList = args.globs && args.globs.length ? args.globs : ["**/tests/**/*.js"]
var ignore = ["**/node_modules/**"].concat(args.ignore||[])
var cwd = process.cwd()
args.require && args.require.forEach(function(module) {
module && require(require.resolve(module, { basedir: cwd }))
})
process.on("unhandledRejection", function(e) {
console.log("Uncaught (in promise) " + e.stack)
})
var pending = globList.length
globList.forEach(function(globPattern) {
glob(globPattern, {ignore: ignore})
.on("match", function(fileName) { require(path.join(cwd, fileName)) }) // eslint-disable-line global-require
.on("error", function(e) { console.error(e) })
.on("end", function() { if (--pending === 0) o.run()})
});
process.on("unhandledRejection", function(e) { console.error("Uncaught (in promise) " + e.stack) })

77
ospec/change-log.md Normal file
View file

@ -0,0 +1,77 @@
# Change Log for ospec
## Upcoming...
_2018-xx-yy_
<!-- Add new lines here. Version number will be decided later -->
- Add `spy.calls` array property to get the `this` and `arguments` values for any arbitrary call.
## 3.0.1
_2018-06-30_
### Bug fix
- Move `glob` from `devDependencies` to `dependencies`, fix the test runner ([#2186](https://github.com/MithrilJS/mithril.js/pull/2186) [@porsager](https://github.com/porsager)
## 3.0.0
_2018-06-20_
### Breaking
- Better input checking to prevent misuses of the library. Misues of the library will now throw errors, rather than report failures. This may uncover bugs in your test suites. Since it is potentially a disruptive update this change triggers a semver major bump. ([#2167](https://github.com/MithrilJS/mithril.js/pull/2167))
- Change the reserved character for hooks and test suite meta-information from `"__"` to `"\x01"`. Tests whose name start with `"\0x01"` will be rejected ([#2167](https://github.com/MithrilJS/mithril.js/pull/2167))
### Features
- Give async timeout a stack trace that points to the problematic test ([#2154](https://github.com/MithrilJS/mithril.js/pull/2154) [@gilbert](github.com/gilbert), [#2167](https://github.com/MithrilJS/mithril.js/pull/2167))
- deprecate the `timeout` parameter in async tests in favour of `o.timeout()` for setting the timeout delay. The `timeout` parameter still works for v3, and will be removed in v4 ([#2167](https://github.com/MithrilJS/mithril.js/pull/2167))
- add `o.defaultTimeout()` for setting the the timeout delay for the current spec and its children ([#2167](https://github.com/MithrilJS/mithril.js/pull/2167))
- adds the possibility to select more than one test with o.only ([#2171](https://github.com/MithrilJS/mithril.js/pull/2171))
### Bug fixes
- Detect duplicate calls to `done()` properly [#2162](https://github.com/MithrilJS/mithril.js/issues/2162) ([#2167](https://github.com/MithrilJS/mithril.js/pull/2167))
- Don't try to report internal errors as assertion failures, throw them instead ([#2167](https://github.com/MithrilJS/mithril.js/pull/2167))
- Don't ignore, silently, tests whose name start with the test suite meta-information sequence (was `"__"` up to this version) ([#2167](https://github.com/MithrilJS/mithril.js/pull/2167))
- Fix the `done()` call detection logic [#2158](https://github.com/MithrilJS/mithril.js/issues/2158) and assorted fixes (accept non-English names, tolerate comments) ([#2167](https://github.com/MithrilJS/mithril.js/pull/2167))
- Catch exceptions thrown in synchronous tests and report them as assertion failures ([#2171](https://github.com/MithrilJS/mithril.js/pull/2171))
- Fix a stack overflow when using `o.only()` with a large test suite ([#2171](https://github.com/MithrilJS/mithril.js/pull/2171))
## 2.1.0
_2018-05-25_
### Features
- Pinpoint the `o.only()` call site ([#2157](https://github.com/MithrilJS/mithril.js/pull/2157))
- Improved wording, spacing and color-coding of report messages and errors ([#2147](https://github.com/MithrilJS/mithril.js/pull/2147), [@maranomynet](https://github.com/maranomynet))
### Bug fixes
- Convert the exectuable back to plain ES5 [#2160](https://github.com/MithrilJS/mithril.js/issues/2160) ([#2161](https://github.com/MithrilJS/mithril.js/pull/2161))
## 2.0.0
_2018-05-09_
- Added `--require` feature to the ospec executable ([#2144](https://github.com/MithrilJS/mithril.js/pull/2144), [@gilbert](https://github.com/gilbert))
- In Node.js, ospec only uses colors when the output is sent to a terminal ([#2143](https://github.com/MithrilJS/mithril.js/pull/2143))
- the CLI runner now accepts globs as arguments ([#2141](https://github.com/MithrilJS/mithril.js/pull/2141), [@maranomynet](https://github.com/maranomynet))
- Added support for custom reporters ([#2020](https://github.com/MithrilJS/mithril.js/pull/2020), [@zyrolasting](https://github.com/zyrolasting))
- Make ospec more [Flems](https://flems.io)-friendly ([#2034](https://github.com/MithrilJS/mithril.js/pull/2034))
- Works either as a global or in CommonJS environments
- the o.run() report is always printed asynchronously (it could be synchronous before if none of the tests were async).
- Properly point to the assertion location of async errors [#2036](https://github.com/MithrilJS/mithril.js/issues/2036)
- expose the default reporter as `o.report(results)`
- Don't try to access the stack traces in IE9
## 1.4.1
_2018-05-03_
- Identical to v1.4.0, but with UNIX-style line endings so that BASH is happy.
## 1.4.0
_2017-12-01_
- Added support for async functions and promises in tests ([#1928](https://github.com/MithrilJS/mithril.js/pull/1928), [@StephanHoyer](https://github.com/StephanHoyer))
- Error handling for async tests with `done` callbacks supports error as first argument ([#1928](https://github.com/MithrilJS/mithril.js/pull/1928))
- Error messages which include newline characters do not swallow the stack trace [#1495](https://github.com/MithrilJS/mithril.js/issues/1495) ([#1984](https://github.com/MithrilJS/mithril.js/pull/1984), [@RodericDay](https://github.com/RodericDay))
## 1.3 and earlier
- Log using util.inspect to show object content instead of "[object Object]" ([#1661](https://github.com/MithrilJS/mithril.js/issues/1661), [@porsager](https://github.com/porsager))
- Shell command: Ignore hidden directories and files ([#1855](https://github.com/MithrilJS/mithril.js/pull/1855) [@pdfernhout)](https://github.com/pdfernhout))
- Library: Add the possibility to name new test suites ([#1529](https://github.com/MithrilJS/mithril.js/pull/1529))

View file

@ -1,11 +1,12 @@
/* eslint-disable global-require, no-bitwise, no-process-exit */
"use strict"
;(function(m) {
if (typeof module !== "undefined") module["exports"] = m()
else window.o = m()
})(function init(name) {
var spec = {}, subjects = [], results, only = null, ctx = spec, start, stack = 0, nextTickish, hasProcess = typeof process === "object", hasOwn = ({}).hasOwnProperty
var spec = {}, subjects = [], results, only = [], ctx = spec, start, stack = 0, nextTickish, hasProcess = typeof process === "object", hasOwn = ({}).hasOwnProperty
var ospecFileName = getStackName(ensureStackTrace(new Error), /[\/\\](.*?):\d+:\d+/), timeoutStackName
var globalTimeout = noTimeoutRightNow
var currentTestError = null
if (name != null) spec[name] = ctx = {}
try {throw new Error} catch (e) {
@ -13,19 +14,25 @@ else window.o = m()
}
function o(subject, predicate) {
if (predicate === undefined) {
if (results == null) throw new Error("Assertions should not occur outside test definitions")
if (!isRunning()) throw new Error("Assertions should not occur outside test definitions")
return new Assert(subject)
}
else if (results == null) {
ctx[unique(subject)] = predicate
} else {
throw new Error("Test definition shouldn't be nested. To group tests use `o.spec()`")
if (isRunning()) throw new Error("Test definitions and hooks shouldn't be nested. To group tests use `o.spec()`")
subject = String(subject)
if (subject.charCodeAt(0) === 1) throw new Error("test names starting with '\\x01' are reserved for internal use")
ctx[unique(subject)] = new Task(predicate, ensureStackTrace(new Error))
}
}
o.before = hook("__before")
o.after = hook("__after")
o.beforeEach = hook("__beforeEach")
o.afterEach = hook("__afterEach")
o.before = hook("\x01before")
o.after = hook("\x01after")
o.beforeEach = hook("\x01beforeEach")
o.afterEach = hook("\x01afterEach")
o.specTimeout = function (t) {
if (isRunning()) throw new Error("o.specTimeout() can only be called before o.run()")
if (hasOwn.call(ctx, "\x01specTimeout")) throw new Error("A default timeout has already been defined in this context")
if (typeof t !== "number") throw new Error("o.specTimeout() expects a number as argument")
ctx["\x01specTimeout"] = t
}
o.new = init
o.spec = function(subject, predicate) {
var parent = ctx
@ -34,13 +41,18 @@ else window.o = m()
ctx = parent
}
o.only = function(subject, predicate, silent) {
if (!silent) console.log(highlight("/!\\ WARNING /!\\ o.only() mode"))
o(subject, only = predicate)
if (!silent) console.log(
highlight("/!\\ WARNING /!\\ o.only() mode") + "\n" + o.cleanStackTrace(ensureStackTrace(new Error)) + "\n",
cStyle("red"), ""
)
only.push(predicate)
o(subject, predicate)
}
o.spy = function(fn) {
var spy = function() {
spy.this = this
spy.args = [].slice.call(arguments)
spy.calls.push({this: this, args: spy.args})
spy.callCount++
if (fn) return fn.apply(this, arguments)
@ -51,6 +63,7 @@ else window.o = m()
name: {value: fn.name}
})
spy.args = []
spy.calls = []
spy.callCount = 0
return spy
}
@ -67,104 +80,125 @@ else window.o = m()
}
if (ospecFileName == null) return stack.join("\n")
// skip ospec-related entries on the stack
while (stack[i].indexOf(ospecFileName) !== -1) i++
// now we're in user code
while (stack[i] != null && stack[i].indexOf(ospecFileName) !== -1) i++
// now we're in user code (or past the stack end)
return stack[i]
}
o.timeout = function(n) {
globalTimeout(n)
}
o.run = function(reporter) {
results = []
start = new Date
test(spec, [], [], function() {
test(spec, [], [], new Task(function() {
setTimeout(function () {
timeoutStackName = getStackName({stack: o.cleanStackTrace(ensureStackTrace(new Error))}, /([\w \.]+?:\d+:\d+)/)
if (typeof reporter === "function") reporter(results)
else {
var errCount = o.report(results)
if (hasProcess && errCount !== 0) process.exit(1)
if (hasProcess && errCount !== 0) process.exit(1) // eslint-disable-line no-process-exit
}
})
})
}, null), 200 /*default timeout delay*/)
function test(spec, pre, post, finalize) {
pre = [].concat(pre, spec["__beforeEach"] || [])
post = [].concat(spec["__afterEach"] || [], post)
series([].concat(spec["__before"] || [], Object.keys(spec).map(function(key) {
return function(done, timeout) {
timeout(Infinity)
if (key.slice(0, 2) === "__") return done()
if (only !== null && spec[key] !== only && typeof only === typeof spec[key]) return done()
subjects.push(key)
var type = typeof spec[key]
if (type === "object") test(spec[key], pre, post, pop)
if (type === "function") series([].concat(pre, spec[key], post, pop))
function pop() {
subjects.pop()
done()
}
function test(spec, pre, post, finalize, defaultDelay) {
if (hasOwn.call(spec, "\x01specTimeout")) defaultDelay = spec["\x01specTimeout"]
pre = [].concat(pre, spec["\x01beforeEach"] || [])
post = [].concat(spec["\x01afterEach"] || [], post)
series([].concat(spec["\x01before"] || [], Object.keys(spec).reduce(function(tasks, key) {
if (key.charCodeAt(0) !== 1 && (only.length === 0 || only.indexOf(spec[key].fn) !== -1 || !(spec[key] instanceof Task))) {
tasks.push(new Task(function(done) {
o.timeout(Infinity)
subjects.push(key)
var pop = new Task(function pop() {subjects.pop(), done()}, null)
if (spec[key] instanceof Task) series([].concat(pre, spec[key], post, pop), defaultDelay)
else test(spec[key], pre, post, pop, defaultDelay)
}, null))
}
}), spec["__after"] || [], finalize))
return tasks
}, []), spec["\x01after"] || [], finalize), defaultDelay)
}
function series(fns) {
function series(tasks, defaultDelay) {
var cursor = 0
next()
function next() {
if (cursor === fns.length) return
if (cursor === tasks.length) return
var task = tasks[cursor++]
var fn = task.fn
currentTestError = task.err
var timeout = 0, delay = defaultDelay, s = new Date
var current = cursor
var arg
globalTimeout = setDelay
var fn = fns[cursor++]
var timeout = 0, delay = 200, s = new Date
var isDone = false
// public API, may only be called once from use code (or after returned Promise resolution)
function done(err) {
if (err) {
if (err.message) record(err.message, err)
else record(err)
subjects.pop()
next()
}
if (timeout !== undefined) {
timeout = clearTimeout(timeout)
if (delay !== Infinity) record(null)
if (!isDone) next()
else throw new Error("`" + arg + "()` should only be called once")
isDone = true
}
else console.log("# elapsed: " + Math.round(new Date - s) + "ms, expected under " + delay + "ms")
if (!isDone) isDone = true
else throw new Error("`" + arg + "()` should only be called once")
if (timeout === undefined) console.warn("# elapsed: " + Math.round(new Date - s) + "ms, expected under " + delay + "ms\n" + o.cleanStackTrace(task.err))
finalizeAsync(err)
}
// for internal use only
function finalizeAsync(err) {
if (err == null) {
if (task.err != null) succeed(new Assert)
} else {
if (err instanceof Error) fail(new Assert, err.message, err)
else fail(new Assert, String(err), null)
}
if (timeout !== undefined) timeout = clearTimeout(timeout)
if (current === cursor) next()
}
function startTimer() {
timeout = setTimeout(function() {
timeout = undefined
record("async test timed out")
next()
finalizeAsync("async test timed out after " + delay + "ms")
}, Math.min(delay, 2147483647))
}
function setDelay (t) {
if (typeof t !== "number") throw new Error("timeout() and o.timeout() expect a number as argument")
delay = t
}
if (fn.length > 0) {
var body = fn.toString()
var arg = (body.match(/\(([\w$]+)/) || body.match(/([\w$]+)\s*=>/) || []).pop()
if (body.indexOf(arg) === body.lastIndexOf(arg)) throw new Error("`" + arg + "()` should be called at least once")
arg = (body.match(/^(.+?)(?:\s|\/\*[\s\S]*?\*\/|\/\/.*?\n)*=>/) || body.match(/\((?:\s|\/\*[\s\S]*?\*\/|\/\/.*?\n)*(.+?)(?:\s|\/\*[\s\S]*?\*\/|\/\/.*?\n)*[,\)]/) || []).pop()
if (body.indexOf(arg) === body.lastIndexOf(arg)) {
var e = new Error
e.stack = "`" + arg + "()` should be called at least once\n" + o.cleanStackTrace(task.err)
throw e
}
try {
fn(done, function(t) {delay = t})
fn(done, setDelay)
}
catch (e) {
done(e)
if (task.err != null) finalizeAsync(e)
// The errors of internal tasks (which don't have an Err) are ospec bugs and must be rethrown.
else throw e
}
if (timeout === 0) {
startTimer()
}
}
else {
var p = fn()
if (p && p.then) {
startTimer()
p.then(function() { done() }, done)
} else {
nextTickish(next)
} else {
try{
var p = fn()
if (p && p.then) {
startTimer()
p.then(function() { done() }, done)
} else {
nextTickish(next)
}
} catch (e) {
if (task.err != null) finalizeAsync(e)
// The errors of internal tasks (which don't have an Err) are ospec bugs and must be rethrown.
else throw e
}
}
globalTimeout = noTimeoutRightNow
}
}
}
@ -178,7 +212,7 @@ else window.o = m()
function hook(name) {
return function(predicate) {
if (ctx[name]) throw new Error("This hook should be defined outside of a loop or inside a nested test group:\n" + predicate)
ctx[name] = predicate
ctx[name] = new Task(predicate, ensureStackTrace(new Error))
}
}
@ -195,7 +229,7 @@ else window.o = m()
}
function deepEqual(a, b) {
if (a === b) return true
if (a === null ^ b === null || a === undefined ^ b === undefined) return false
if (a === null ^ b === null || a === undefined ^ b === undefined) return false // eslint-disable-line no-bitwise
if (typeof a === "object" && typeof b === "object") {
var aIsArgs = isArguments(a), bIsArgs = isArguments(b)
if (a.constructor === Object && b.constructor === Object && !aIsArgs && !bIsArgs) {
@ -227,54 +261,99 @@ else window.o = m()
return false
}
function Assert(value) {this.value = value}
function isRunning() {return results != null}
function Assert(value) {
this.value = value
this.i = results.length
results.push({pass: null, context: "", message: "Incomplete assertion in the test definition starting at...", error: currentTestError, testError: currentTestError})
}
function Task(fn, err) {
this.fn = fn
this.err = err
}
function define(name, verb, compare) {
Assert.prototype[name] = function assert(value) {
if (compare(this.value, value)) record(null)
else record(serialize(this.value) + "\n" + verb + "\n" + serialize(value))
if (compare(this.value, value)) succeed(this)
else fail(this, serialize(this.value) + "\n " + verb + "\n" + serialize(value))
var self = this
return function(message) {
var result = results[results.length - 1]
result.message = message + "\n\n" + result.message
if (!self.pass) self.message = message + "\n\n" + self.message
}
}
}
function record(message, error) {
var result = {pass: message === null}
if (result.pass === false) {
if (error == null) {
error = new Error
if (error.stack === undefined) new function() {try {throw error} catch (e) {error = e}}
}
result.context = subjects.join(" > ")
result.message = message
result.error = error
}
results.push(result)
function succeed(assertion) {
results[assertion.i].pass = true
}
function fail(assertion, message, error) {
results[assertion.i].pass = false
results[assertion.i].context = subjects.join(" > ")
results[assertion.i].message = message
results[assertion.i].error = error != null ? error : ensureStackTrace(new Error)
}
function serialize(value) {
if (hasProcess) return require("util").inspect(value)
if (hasProcess) return require("util").inspect(value) // eslint-disable-line global-require
if (value === null || (typeof value === "object" && !(value instanceof Array)) || typeof value === "number") return String(value)
else if (typeof value === "function") return value.name || "<anonymous function>"
try {return JSON.stringify(value)} catch (e) {return String(value)}
}
function highlight(message) {
return hasProcess ? "\x1b[31m" + message + "\x1b[0m" : "%c" + message + "%c "
function noTimeoutRightNow() {
throw new Error("o.timeout must be called snchronously from within a test definition or a hook")
}
var colorCodes = {
red: "31m",
red2: "31;1m",
green: "32;1m"
}
function highlight(message, color) {
var code = colorCodes[color] || colorCodes.red;
return hasProcess ? (process.stdout.isTTY ? "\x1b[" + code + message + "\x1b[0m" : message) : "%c" + message + "%c "
}
function cStyle(color, bold) {
return hasProcess||!color ? "" : "color:"+color+(bold ? ";font-weight:bold" : "")
}
function ensureStackTrace(error) {
// mandatory to get a stack in IE 10 and 11 (and maybe other envs?)
if (error.stack === undefined) try { throw error } catch(e) {return e}
else return error
}
function getStackName(e, exp) {
return e.stack && exp.test(e.stack) ? e.stack.match(exp)[1] : null
}
o.report = function (results) {
var errCount = 0
for (var i = 0, r; r = results[i]; i++) {
if (r.pass == null) {
r.testError.stack = r.message + "\n" + o.cleanStackTrace(r.testError)
r.testError.message = r.message
throw r.testError
}
if (!r.pass) {
var stackTrace = o.cleanStackTrace(r.error)
console.error(r.context + ":\n" + highlight(r.message) + (stackTrace ? "\n\n" + stackTrace + "\n\n" : ""), hasProcess ? "" : "color:red", hasProcess ? "" : "color:black")
var couldHaveABetterStackTrace = !stackTrace || timeoutStackName != null && stackTrace.indexOf(timeoutStackName) !== -1
if (couldHaveABetterStackTrace) stackTrace = r.testError != null ? o.cleanStackTrace(r.testError) : r.error.stack || ""
console.error(
(hasProcess ? "\n" : "") +
highlight(r.context + ":", "red2") + "\n" +
highlight(r.message, "red") +
(stackTrace ? "\n" + stackTrace + "\n" : ""),
cStyle("black", true), "", // reset to default
cStyle("red"), cStyle("black")
)
errCount++
}
}
var pl = results.length === 1 ? "" : "s"
var resultSummary = (errCount === 0) ?
highlight((pl ? "All " : "The ") + results.length + " assertion" + pl + " passed", "green"):
highlight(errCount + " out of " + results.length + " assertion" + pl + " failed", "red2")
var runningTime = " in " + Math.round(Date.now() - start) + "ms"
console.log(
(name ? name + ": " : "") +
results.length + " assertions completed in " + Math.round(new Date - start) + "ms, " +
"of which " + results.filter(function(result){return result.error}).length + " failed"
(hasProcess ? "\n" : "") +
(name ? name + ": " : "") + resultSummary + runningTime,
cStyle((errCount === 0 ? "green" : "red"), true), ""
)
return errCount
}

View file

@ -1,6 +1,6 @@
{
"name": "ospec",
"version": "1.4.0",
"version": "3.0.1",
"description": "Noiseless testing framework",
"main": "ospec.js",
"directories": {
@ -12,5 +12,8 @@
"bin": {
"ospec": "./bin/ospec"
},
"repository": "MithrilJS/mithril.js"
"repository": "MithrilJS/mithril.js",
"dependencies": {
"glob": "^7.1.2"
}
}

View file

@ -3,20 +3,137 @@
var callAsync = require("../../test-utils/callAsync")
var o = require("../ospec")
new function(o) {
o = o.new()
o.spec("ospec", function() {
o("skipped", function() {
// this throws an async error that can't be caught in browsers
if (typeof process !== "undefined") {
o("incomplete assertion", function(done) {
var stackMatcher = /([\w\.\\\/\-]+):(\d+):/
// /!\ this test relies on the `new Error` expression being six lines
// above the `oo("test", function(){...})` call.
var matches = (new Error).stack.match(stackMatcher)
if (matches != null) {
var name = matches[1]
var num = Number(matches[2])
}
var oo = o.new()
oo("test", function() {
oo("incomplete")
})
oo.run(function(results) {
o(results.length).equals(1)
o(results[0].message).equals("Incomplete assertion in the test definition starting at...")
o(results[0].pass).equals(null)
var stack = o.cleanStackTrace(results[0].testError)
var matches2 = stack && stack.match(stackMatcher)
if (matches != null && matches2 != null) {
o(matches[1]).equals(name)
o(Number(matches2[2])).equals(num + 6)
}
done()
})
})
}
o("o.only", function(done) {
var oo = o.new()
oo.spec("won't run", function() {
oo("nope, skipped", function() {
o(true).equals(false)
})
o.only(".only()", function() {
o(2).equals(2)
})
oo.spec("ospec", function() {
oo("skipped as well", function() {
oo(true).equals(false)
})
oo.only(".only()", function() {
oo(2).equals(2)
}, true)
oo.only("another .only()", function(done) {
done("that fails")
}, true)
})
o.run()
}(o)
oo.run(function(results){
o(results.length).equals(2)
o(results[0].pass).equals(true)
o(results[1].pass).equals(false)
done()
})
})
// Predicate test passing on clone results
o.spec("reporting", function() {
var oo
o.beforeEach(function(){
oo = o.new()
oo.spec("clone", function() {
oo("fail", function() {
oo(true).equals(false)
})
oo("pass", function() {
oo(true).equals(true)
})
})
})
o("reports per instance", function(done, timeout) {
timeout(100) // Waiting on clone
oo.run(function(results) {
o(typeof results).equals("object")
o("length" in results).equals(true)
o(results.length).equals(2)("Two results")
o("error" in results[0] && "pass" in results[0]).equals(true)("error and pass keys present in failing result")
o(results[0].pass).equals(false)("Test meant to fail has failed")
o(results[1].pass).equals(true)("Test meant to pass has passed")
done()
})
})
o("o.report() returns the number of failures", function () {
var log = console.log, error = console.error
console.log = o.spy()
console.error = o.spy()
function makeError(msg) {try{throw msg ? new Error(msg) : new Error} catch(e){return e}}
try {
var errCount = o.report([{pass: true}, {pass: true}])
o(errCount).equals(0)
o(console.log.callCount).equals(1)
o(console.error.callCount).equals(0)
errCount = o.report([
{pass: false, error: makeError("hey"), message: "hey"}
])
o(errCount).equals(1)
o(console.log.callCount).equals(2)
o(console.error.callCount).equals(1)
errCount = o.report([
{pass: false, error: makeError("hey"), message: "hey"},
{pass: true},
{pass: false, error: makeError("ho"), message: "ho"}
])
o(errCount).equals(2)
o(console.log.callCount).equals(3)
o(console.error.callCount).equals(3)
} catch (e) {
o(1).equals(0)("Error while testing the reporter")
}
console.log = log
console.error = error
})
})
new function(o) {
var clone = o.new()
@ -92,6 +209,7 @@ new function(o) {
o.spec("ospec", function() {
o.spec("sync", function() {
var a = 0, b = 0, illegalAssertionThrows = false
var reservedTestNameTrows = false
o.before(function() {a = 1})
o.after(function() {a = 0})
@ -100,6 +218,7 @@ o.spec("ospec", function() {
o.afterEach(function() {b = 0})
try {o("illegal assertion")} catch (e) {illegalAssertionThrows = true}
try {o("\x01reserved test name", function(){})} catch (e) {reservedTestNameTrows = true}
o("assertions", function() {
var nestedTestDeclarationThrows = false
@ -107,6 +226,7 @@ o.spec("ospec", function() {
o(illegalAssertionThrows).equals(true)
o(nestedTestDeclarationThrows).equals(true)
o(reservedTestNameTrows).equals(true)
var spy = o.spy()
spy(a)
@ -157,6 +277,8 @@ o.spec("ospec", function() {
o(spy.callCount).equals(1)
o(spy.args.length).equals(1)
o(spy.args[0]).equals(1)
o(spy.calls.length).equals(1)
o(spy.calls[0]).deepEquals({this: undefined, args: [1]})
})
o("spy wrapping", function() {
var spy = o.spy(function view(vnode){
@ -174,47 +296,368 @@ o.spec("ospec", function() {
o(spy.callCount).equals(1)
o(spy.args.length).equals(1)
o(spy.args[0]).deepEquals({children: children})
o(spy.calls.length).equals(1)
o(spy.calls[0]).deepEquals({this: state, args: [{children: children}]})
o(state).deepEquals({drawn: true})
o(output).deepEquals({tag: "div", children: children})
})
})
o.spec("async callback", function() {
var a = 0, b = 0
o.after(function() {
o(a).equals(0)
o(b).equals(0)
})
o.spec("", function(){
o.before(function(done) {
callAsync(function() {
a = 1
done()
})
})
o.after(function(done) {
callAsync(function() {
a = 0
done()
})
})
o.beforeEach(function(done) {
o(b).equals(0)
callAsync(function() {
b = 1
done()
})
})
o.afterEach(function(done) {
callAsync(function() {
b = 0
done()
})
})
o("hooks work as intended the first time", function(done) {
callAsync(function() {
var spy = o.spy()
spy(a)
o(a).equals(1)
o(b).equals(1)
done()
})
})
o("hooks work as intended the second time", function(done) {
callAsync(function() {
var spy = o.spy()
spy(a)
o(a).equals(1)
o(b).equals(1)
done()
})
})
})
})
o.spec("throwing in test context is recoreded as a failure", function() {
var oo
o.beforeEach(function(){oo = o.new()})
o.afterEach(function() {
oo.run(function(results) {
o(results.length).equals(1)
o(results[0].pass).equals(false)
})
})
o("sync test", function() {
oo("throw in sync test", function() {throw new Error})
})
o("async test", function() {
oo("throw in async test", function(done) {
throw new Error
done() // eslint-disable-line no-unreachable
})
})
})
o.spec("timeout", function () {
o("when using done()", function(done) {
var oo = o.new()
var err
// the success of this test is dependent on having the
// oo() call three linew below this one
try {throw new Error} catch(e) {err = e}
if (err.stack) {
var line = Number(err.stack.match(/:(\d+):/)[1])
oo("", function(oodone, timeout) {
// oodone() keep this line for now
timeout(1)
})
oo.run((function(results) {
o(results.length).equals(1)
o(results[0].pass).equals(false)
// todo test cleaned up results[0].error stack trace for the presence
// of the timeout stack entry
o(results[0].testError instanceof Error).equals(true)
o(o.cleanStackTrace(results[0].testError).indexOf("test-ospec.js:" + (line + 3) + ":")).notEquals(-1)
done()
}))
} else {
done()
}
})
o("when using a thenable", function(done) {
var oo = o.new()
var err
// the success of this test is dependent on having the
// oo() call three linew below this one
try {throw new Error} catch(e) {err = e}
if (err.stack) {
var line = Number(err.stack.match(/:(\d+):/)[1])
oo("", function() {
oo.timeout(1)
return {then: function(){}}
})
oo.run((function(results) {
o(results.length).equals(1)
o(results[0].pass).equals(false)
o(results[0].testError instanceof Error).equals(true)
o(o.cleanStackTrace(results[0].testError).indexOf("test-ospec.js:" + (line + 3) + ":")).notEquals(-1)
done()
}))
} else {
done()
}
})
})
o.spec("o.timeout", function() {
o("throws when called out of test definitions", function(done) {
var oo = o.new()
var count = 0
try { oo.timeout(1) } catch (e) { count++ }
oo.spec("a spec", function() {
try { oo.timeout(1) } catch (e) { count++ }
})
oo("", function() {
oo.timeout(30)
return {then: function(f) {setTimeout(f)}}
})
oo.run(function(){
o(count).equals(2)
o.before(function(done) {
callAsync(function() {
a = 1
done()
})
})
o.after(function(done) {
callAsync(function() {
a = 0
o("works", function(done) {
var oo = o.new()
var t = new Date
oo("", function() {
oo.timeout(10)
return {then: function() {}}
})
oo.run(function(){
o(new Date - t >= 10).equals(true)
o(200 > new Date - t).equals(true)
done()
})
})
})
o.spec("o.specTimeout", function() {
o("throws when called inside of test definitions", function(done) {
var err
var oo = o.new()
oo("", function() {
try { oo.specTimeout(5) } catch (e) {err = e}
return {then: function(f) {setTimeout(f)}}
})
oo.run(function(){
o(err instanceof Error).equals(true)
o.beforeEach(function(done) {
callAsync(function() {
b = 1
done()
})
})
o.afterEach(function(done) {
callAsync(function() {
b = 0
o("works", function(done) {
var oo = o.new()
var t
oo.specTimeout(10)
oo.beforeEach(function () {
t = new Date
})
oo.afterEach(function () {
var diff = new Date - t
o(diff >= 10).equals(true)
o(diff < 200).equals(true)
})
oo("", function() {
oo(true).equals(true)
return {then: function() {}}
})
oo.run(function(results) {
o(results.length).equals(2)
o(results[0].pass).equals(true)
o(results[1].pass).equals(false)
done()
})
})
o("The parent and sibling suites are not affected by the specTimeout", function(done) {
var oo = o.new()
var t
o("async hooks", function(done) {
callAsync(function() {
var spy = o.spy()
spy(a)
oo.specTimeout(50)
oo.beforeEach(function () {
t = new Date
})
oo.afterEach(function () {
var diff = new Date - t
o(diff >= 50).equals(true)
o(diff < 80).equals(true)
})
o(a).equals(b)
o(a).equals(1)("a and b should be initialized")
oo.spec("nested 1", function () {
oo.specTimeout(80)
})
oo("", function() {
oo(true).equals(true)
return {then: function() {}}
})
oo.spec("nested 2", function () {
oo.specTimeout(80)
})
oo.spec("nested 3", function () {
oo("", function() {
oo(true).equals(true)
return {then: function() {}}
})
})
oo.run(function(results) {
o(results.length).equals(4)
o(results[0].pass).equals(true)
o(results[1].pass).equals(false)
o(results[2].pass).equals(true)
o(results[3].pass).equals(false)
done()
})
})
o("nested suites inherit the specTimeout", function(done) {
var oo = o.new()
oo.specTimeout(50)
oo.spec("nested", function () {
oo.spec("deeply", function() {
var t
oo.beforeEach(function () {
t = new Date
})
oo.afterEach(function () {
var diff = new Date - t
o(diff >= 50).equals(true)
o(diff < 80).equals(true)
})
oo("", function() {
oo(true).equals(true)
return {then: function() {}}
})
})
})
oo.run(function(results) {
o(results.length).equals(2)
o(results[0].pass).equals(true)
o(results[1].pass).equals(false)
done()
})
})
})
o.spec("calling done() twice throws", function () {
o("two successes", function(done) {
var oo = o.new()
var err = null
oo("foo", function(oodone) {
try {
oodone()
oodone()
} catch (e) {
err = e
}
o(err instanceof Error).equals(true)
o(err.message).equals("`oodone()` should only be called once")
})
oo.run(function(results) {
o(results.length).equals(1)
o(results[0].pass).equals(true)
done()
})
})
o("a success followed by an error", function(done) {
var oo = o.new()
var err = null
oo("foo", function(oodone) {
try {
oodone()
oodone("error")
} catch (e) {
err = e
}
o(err instanceof Error).equals(true)
o(err.message).equals("`oodone()` should only be called once")
})
oo.run(function(results) {
o(results.length).equals(1)
o(results[0].pass).equals(true)
done()
})
})
o("two errors", function(done) {
var oo = o.new()
var err = null
oo("foo", function(oodone) {
try {
oodone("bar")
oodone("baz")
} catch (e) {
err = e
}
o(err instanceof Error).equals(true)
o(err.message).equals("`oodone()` should only be called once")
})
oo.run(function(results) {
o(results.length).equals(1)
o(results[0].pass).equals(false)
o(results[0].message).equals("bar")
done()
})
})
o("an error followed by a success", function(done) {
var oo = o.new()
var err = null
oo("foo", function(oodone) {
try {
oodone("bar")
oodone()
} catch (e) {
err = e
}
o(err instanceof Error).equals(true)
o(err.message).equals("`oodone()` should only be called once")
})
oo.run(function(results) {
o(results.length).equals(1)
o(results[0].pass).equals(false)
o(results[0].message).equals("bar")
done()
})
})
@ -227,7 +670,7 @@ o.spec("ospec", function() {
} catch(error) {
var trace = o.cleanStackTrace(error)
o(trace).notEquals("break")
o(trace.includes("test-ospec.js")).equals(true)
o(trace.indexOf("test-ospec.js") !== -1).equals(true)
}
})
})
@ -279,3 +722,79 @@ o.spec("ospec", function() {
})
})
})
o.spec("the done parser", function() {
o("accepts non-English names", function() {
var oo = o.new()
var threw = false
oo("test", function(完了) {
oo(true).equals(true)
完了()
})
try {oo.run(function(){})} catch(e) {threw = true}
o(threw).equals(false)
})
o("tolerates comments", function() {
var oo = o.new()
var threw = false
oo("test", function(/*hey
*/ /**/ //ho
done /*hey
*/ /**/ //huuu
, timeout
) {
timeout(5)
oo(true).equals(true)
done()
})
try {oo.run(function(){})} catch(e) {threw = true}
o(threw).equals(false)
})
/*eslint-disable no-eval*/
try {eval("(()=>{})()"); o.spec("with ES6 arrow functions", function() {
function getCommentContent(f) {
f = f.toString()
return f.slice(f.indexOf("/*") + 2, f.lastIndexOf("*/"))
}
o("has no false positives 1", function(){
var oo = o.new()
var threw = false
eval(getCommentContent(function(){/*
oo(
'Async test parser mistakenly identified 1st token after a parens to be `done` reference',
done => {
oo(threw).equals(false)
done()
}
)
*/}))
try {oo.run(function(){})} catch(e) {threw = true}
o(threw).equals(false)
})
o("has no false negatives", function(){
var oo = o.new()
var threw = false
eval(getCommentContent(function(){/*
oo(
"Multiple references to the wrong thing doesn't fool the checker",
done => {
oo(threw).equals(false)
oo(threw).equals(false)
}
)
*/}))
try {oo.run(function(){})} catch(e) {threw = true}
o(threw).equals(true)
})
o("isn't fooled by comments", function(){
var oo = o.new()
var threw = false
oo(
"comments won't throw the parser off",
eval("done /*hey*/ /**/ => {oo(threw).equals(false);done()}")
)
try {oo.run(function(){})} catch(e) {threw = true}
o(threw).equals(false)
})
})} catch (e) {/*ES5 env, or no eval, ignore*/}
/*eslint-enable no-eval*/
})

3540
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -7,10 +7,11 @@
"main": "mithril.js",
"repository": "MithrilJS/mithril.js",
"scripts": {
"dev": "node bundler/cli browser.js -o mithril.js -w",
"dev": "node bundler/cli browser.js -output mithril.js -watch",
"build": "npm run build-browser & npm run build-min",
"build-browser": "node bundler/cli browser.js -o mithril.js",
"build-min": "node bundler/cli browser.js -o mithril.min.js -m",
"build-browser": "node bundler/cli browser.js -output mithril.js",
"build-min": "node bundler/cli browser.js -output mithril.min.js -minify",
"precommit": "lint-staged",
"lintdocs": "node docs/lint",
"gendocs": "node docs/generate",
"lint": "eslint . || true",
@ -27,14 +28,24 @@
"devDependencies": {
"@alrra/travis-scripts": "^3.0.1",
"benchmark": "^2.1.4",
"dedent": "^0.7.0",
"eslint": "^3.19.0",
"gh-pages": "^0.12.0",
"glob": "^7.1.2",
"istanbul": "^0.4.5",
"marked": "^0.3.6"
"lint-staged": "^4.0.4",
"locater": "^1.3.0",
"marked": "^0.3.19",
"pinpoint": "^1.1.0"
},
"bin": {
"ospec": "./ospec/bin/ospec",
"bundle": "./bundler/bin/bundle"
"ospec": "./ospec/bin/ospec"
},
"lint-staged": {
"*.js": [
"eslint . --fix",
"git add"
]
},
"dependencies": {}
}

View file

@ -13,7 +13,9 @@
<script src="../test-utils/pushStateMock.js"></script>
<script src="../test-utils/xhrMock.js"></script>
<script src="../test-utils/browserMock.js"></script>
<script src="../mithril.js"></script>
<script src="../render/vnode.js"></script>
<script src="../render/render.js"></script>
<script src="../render/hyperscript.js"></script>
<script src="../node_modules/lodash/lodash.js"></script>
<script src="../node_modules/benchmark/benchmark.js"></script>
<script src="test-perf.js"></script>

View file

@ -31,7 +31,7 @@ var browserMock = require("../test-utils/browserMock")
// Do this silly dance so browser testing works
var B = typeof Benchmark === "undefined" ? require("benchmark") : Benchmark
var m, scratch;
var scratch;
// set up browser env on before running tests
var doc = typeof document !== "undefined" ? document : null
@ -43,15 +43,20 @@ if(!doc) {
doc = mock.document
}
// Have to include mithril AFTER browser polyfill is set up
m = require("../mithril") // eslint-disable-line global-require
var m = require("../render/hyperscript")
m.render = require("../render/render")(window).render
scratch = doc.createElement("div");
(doc.body || doc.documentElement).appendChild(scratch)
function resetScratch() {
doc.documentElement.innerHTML = "<div></div>"
scratch = doc.documentElement.firstChild
}
resetScratch()
// Initialize benchmark suite
var suite = new B.Suite("mithril perf")
var xuite = {add: function(options) {console.log("skipping " + options.name)}} // eslint-disable-line no-unused-vars
suite.on("start", function() {
this.start = Date.now();
@ -60,7 +65,7 @@ suite.on("start", function() {
suite.on("cycle", function(e) {
console.log(e.target.toString())
scratch.innerHTML = ""
resetScratch()
})
suite.on("complete", function() {
@ -70,7 +75,7 @@ suite.on("complete", function() {
suite.on("error", console.error.bind(console))
suite.add({
name : "rerender without changes",
name : "rerender identical vnode",
onStart : function() {
this.vdom = m("div", {class: "foo bar", "data-foo": "bar", p: 2},
m("header",
@ -119,6 +124,53 @@ suite.add({
}
})
suite.add({
name : "rerender same tree",
fn : function() {
m.render(scratch, m("div", {class: "foo bar", "data-foo": "bar", p: 2},
m("header",
m("h1", {class: "asdf"}, "a ", "b", " c ", 0, " d"),
m("nav",
m("a", {href: "/foo"}, "Foo"),
m("a", {href: "/bar"}, "Bar")
)
),
m("main",
m("form", {onSubmit: function onSubmit() {}},
m("input", {type: "checkbox", checked: true}),
m("input", {type: "checkbox", checked: false}),
m("fieldset",
m("label",
m("input", {type: "radio", checked: true})
),
m("label",
m("input", {type: "radio"})
)
),
m("button-bar",
m("button",
{style: "width:10px; height:10px; border:1px solid #FFF;"},
"Normal CSS"
),
m("button",
{style: "top:0 ; right: 20"},
"Poor CSS"
),
m("button",
{style: "invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;", icon: true},
"Poorer CSS"
),
m("button",
{style: {margin: 0, padding: "10px", overflow: "visible"}},
"Object CSS"
)
)
)
)
))
}
})
suite.add({
name : "construct large VDOM tree",
@ -181,12 +233,12 @@ suite.add({
suite.add({
name : "mutate styles/properties",
// minSamples: 100,
onStart : function () {
var counter = 0
var keyLooper = function (n) { return function (c) { return c % n ? (c + "px") : c } }
var get = function (obj, i) { return obj[i%obj.length] }
var classes = ["foo", "foo bar", "", "baz-bat", null, "fooga"]
var classes = ["foo", "foo bar", "", "baz-bat", null, "fooga", null, null, undefined]
var styles = []
var multivalue = ["0 1px", "0 0 1px 0", "0", "1px", "20px 10px", "7em 5px", "1px 0 5em 2px"]
var stylekeys = [
@ -212,21 +264,26 @@ suite.add({
this.count = 0
this.app = function (index) {
return m("div",
{
class: get(classes, index),
"data-index": index,
title: index.toString(36)
},
m("input", {type: "checkbox", checked: index % 3 == 0}),
m("input", {value: "test " + (Math.floor(index / 4)), disabled: index % 10 ? null : true}),
m("div", {class: get(classes, index * 11)},
m("p", {style: get(styles, index)}, "p1"),
m("p", {style: get(styles, index + 1)}, "p2"),
m("p", {style: get(styles, index * 2)}, "p3"),
m("p", {style: get(styles, index * 3 + 1)}, "p4")
var last = index + 300
var vnodes = []
for (; index < last; index++) vnodes.push(
m("div.booga",
{
class: get(classes, index),
"data-index": index,
title: index.toString(36)
},
m("input.dooga", {type: "checkbox", checked: index % 3 == 0}),
m("input", {value: "test " + (Math.floor(index / 4)), disabled: index % 10 ? null : true}),
m("div", {class: get(classes, index * 11)},
m("p", {style: get(styles, index)}, "p1"),
m("p", {style: get(styles, index + 1)}, "p2"),
m("p", {style: get(styles, index * 2)}, "p3"),
m("p.zooga", {style: get(styles, index * 3 + 1), className: get(classes, index * 7)}, "p4")
)
)
)
return vnodes
}
},
@ -311,16 +368,9 @@ var Root = {
}
}
suite.add({
name : "repeated trees (recycling)",
fn : function () {
m.render(scratch, [m(Root)])
m.render(scratch, [])
}
})
suite.add({
name : "repeated trees (no recycling)",
name : "repeated trees",
fn : function () {
m.render(scratch, [m(Root)])
m.render(scratch, [])

112
promise/polyfill.js Normal file
View file

@ -0,0 +1,112 @@
"use strict"
/** @constructor */
var PromisePolyfill = function(executor) {
if (!(this instanceof PromisePolyfill)) throw new Error("Promise must be called with `new`")
if (typeof executor !== "function") throw new TypeError("executor must be a function")
var self = this, resolvers = [], rejectors = [], resolveCurrent = handler(resolvers, true), rejectCurrent = handler(rejectors, false)
var instance = self._instance = {resolvers: resolvers, rejectors: rejectors}
var callAsync = typeof setImmediate === "function" ? setImmediate : setTimeout
function handler(list, shouldAbsorb) {
return function execute(value) {
var then
try {
if (shouldAbsorb && value != null && (typeof value === "object" || typeof value === "function") && typeof (then = value.then) === "function") {
if (value === self) throw new TypeError("Promise can't be resolved w/ itself")
executeOnce(then.bind(value))
}
else {
callAsync(function() {
if (!shouldAbsorb && list.length === 0) console.error("Possible unhandled promise rejection:", value)
for (var i = 0; i < list.length; i++) list[i](value)
resolvers.length = 0, rejectors.length = 0
instance.state = shouldAbsorb
instance.retry = function() {execute(value)}
})
}
}
catch (e) {
rejectCurrent(e)
}
}
}
function executeOnce(then) {
var runs = 0
function run(fn) {
return function(value) {
if (runs++ > 0) return
fn(value)
}
}
var onerror = run(rejectCurrent)
try {then(run(resolveCurrent), onerror)} catch (e) {onerror(e)}
}
executeOnce(executor)
}
PromisePolyfill.prototype.then = function(onFulfilled, onRejection) {
var self = this, instance = self._instance
function handle(callback, list, next, state) {
list.push(function(value) {
if (typeof callback !== "function") next(value)
else try {resolveNext(callback(value))} catch (e) {if (rejectNext) rejectNext(e)}
})
if (typeof instance.retry === "function" && state === instance.state) instance.retry()
}
var resolveNext, rejectNext
var promise = new PromisePolyfill(function(resolve, reject) {resolveNext = resolve, rejectNext = reject})
handle(onFulfilled, instance.resolvers, resolveNext, true), handle(onRejection, instance.rejectors, rejectNext, false)
return promise
}
PromisePolyfill.prototype.catch = function(onRejection) {
return this.then(null, onRejection)
}
PromisePolyfill.prototype.finally = function(callback) {
return this.then(
function(value) {
return PromisePolyfill.resolve(callback()).then(function() {
return value
})
},
function(reason) {
return PromisePolyfill.resolve(callback()).then(function() {
return PromisePolyfill.reject(reason);
})
}
)
}
PromisePolyfill.resolve = function(value) {
if (value instanceof PromisePolyfill) return value
return new PromisePolyfill(function(resolve) {resolve(value)})
}
PromisePolyfill.reject = function(value) {
return new PromisePolyfill(function(resolve, reject) {reject(value)})
}
PromisePolyfill.all = function(list) {
return new PromisePolyfill(function(resolve, reject) {
var total = list.length, count = 0, values = []
if (list.length === 0) resolve([])
else for (var i = 0; i < list.length; i++) {
(function(i) {
function consume(value) {
count++
values[i] = value
if (count === total) resolve(values)
}
if (list[i] != null && (typeof list[i] === "object" || typeof list[i] === "function") && typeof list[i].then === "function") {
list[i].then(consume, reject)
}
else consume(list[i])
})(i)
}
})
}
PromisePolyfill.race = function(list) {
return new PromisePolyfill(function(resolve, reject) {
for (var i = 0; i < list.length; i++) {
list[i].then(resolve, reject)
}
})
}
module.exports = PromisePolyfill

View file

@ -1,105 +1,20 @@
"use strict"
/** @constructor */
var PromisePolyfill = function(executor) {
if (!(this instanceof PromisePolyfill)) throw new Error("Promise must be called with `new`")
if (typeof executor !== "function") throw new TypeError("executor must be a function")
var self = this, resolvers = [], rejectors = [], resolveCurrent = handler(resolvers, true), rejectCurrent = handler(rejectors, false)
var instance = self._instance = {resolvers: resolvers, rejectors: rejectors}
var callAsync = typeof setImmediate === "function" ? setImmediate : setTimeout
function handler(list, shouldAbsorb) {
return function execute(value) {
var then
try {
if (shouldAbsorb && value != null && (typeof value === "object" || typeof value === "function") && typeof (then = value.then) === "function") {
if (value === self) throw new TypeError("Promise can't be resolved w/ itself")
executeOnce(then.bind(value))
}
else {
callAsync(function() {
if (!shouldAbsorb && list.length === 0) console.error("Possible unhandled promise rejection:", value)
for (var i = 0; i < list.length; i++) list[i](value)
resolvers.length = 0, rejectors.length = 0
instance.state = shouldAbsorb
instance.retry = function() {execute(value)}
})
}
}
catch (e) {
rejectCurrent(e)
}
}
}
function executeOnce(then) {
var runs = 0
function run(fn) {
return function(value) {
if (runs++ > 0) return
fn(value)
}
}
var onerror = run(rejectCurrent)
try {then(run(resolveCurrent), onerror)} catch (e) {onerror(e)}
}
executeOnce(executor)
}
PromisePolyfill.prototype.then = function(onFulfilled, onRejection) {
var self = this, instance = self._instance
function handle(callback, list, next, state) {
list.push(function(value) {
if (typeof callback !== "function") next(value)
else try {resolveNext(callback(value))} catch (e) {if (rejectNext) rejectNext(e)}
})
if (typeof instance.retry === "function" && state === instance.state) instance.retry()
}
var resolveNext, rejectNext
var promise = new PromisePolyfill(function(resolve, reject) {resolveNext = resolve, rejectNext = reject})
handle(onFulfilled, instance.resolvers, resolveNext, true), handle(onRejection, instance.rejectors, rejectNext, false)
return promise
}
PromisePolyfill.prototype.catch = function(onRejection) {
return this.then(null, onRejection)
}
PromisePolyfill.resolve = function(value) {
if (value instanceof PromisePolyfill) return value
return new PromisePolyfill(function(resolve) {resolve(value)})
}
PromisePolyfill.reject = function(value) {
return new PromisePolyfill(function(resolve, reject) {reject(value)})
}
PromisePolyfill.all = function(list) {
return new PromisePolyfill(function(resolve, reject) {
var total = list.length, count = 0, values = []
if (list.length === 0) resolve([])
else for (var i = 0; i < list.length; i++) {
(function(i) {
function consume(value) {
count++
values[i] = value
if (count === total) resolve(values)
}
if (list[i] != null && (typeof list[i] === "object" || typeof list[i] === "function") && typeof list[i].then === "function") {
list[i].then(consume, reject)
}
else consume(list[i])
})(i)
}
})
}
PromisePolyfill.race = function(list) {
return new PromisePolyfill(function(resolve, reject) {
for (var i = 0; i < list.length; i++) {
list[i].then(resolve, reject)
}
})
}
var PromisePolyfill = require("./polyfill")
if (typeof window !== "undefined") {
if (typeof window.Promise === "undefined") window.Promise = PromisePolyfill
if (typeof window.Promise === "undefined") {
window.Promise = PromisePolyfill
} else if (!window.Promise.prototype.finally) {
window.Promise.prototype.finally = PromisePolyfill.prototype.finally
}
module.exports = window.Promise
} else if (typeof global !== "undefined") {
if (typeof global.Promise === "undefined") global.Promise = PromisePolyfill
if (typeof global.Promise === "undefined") {
global.Promise = PromisePolyfill
} else if (!global.Promise.prototype.finally) {
global.Promise.prototype.finally = PromisePolyfill.prototype.finally
}
module.exports = global.Promise
} else {
module.exports = PromisePolyfill

View file

@ -2,7 +2,7 @@
var o = require("../../ospec/ospec")
var callAsync = require("../../test-utils/callAsync")
var Promise = require("../../promise/promise")
var Promise = require("../../promise/polyfill")
o.spec("promise", function() {
o.spec("constructor", function() {
@ -15,6 +15,7 @@ o.spec("promise", function() {
o("constructor has correct methods", function() {
o(typeof Promise.prototype.then).equals("function")
o(typeof Promise.prototype.catch).equals("function")
o(typeof Promise.prototype.finally).equals("function")
o(typeof Promise.resolve).equals("function")
o(typeof Promise.reject).equals("function")
o(typeof Promise.race).equals("function")
@ -53,6 +54,78 @@ o.spec("promise", function() {
o(value).equals(1)
}).then(done)
})
o("finally lets a fulfilled value pass though", function(done) {
var promise = Promise.resolve(1)
var spy = o.spy(function(){return 2})
promise.finally(spy).then(function(value){
o(value).equals(1)
o(spy.callCount).equals(1)
o(spy.args.length).equals(0)
o(spy.this).equals(undefined)
done()
})
})
o("finally lets a rejected reason pass though", function(done) {
var promise = Promise.reject(1)
var spy = o.spy(function(){return 2})
promise.finally(spy).catch(function(reason){
o(reason).equals(1)
o(spy.callCount).equals(1)
o(spy.args.length).equals(0)
o(spy.this).equals(undefined)
done()
})
})
o("finally overrrides a fulfilled value when it throws", function(done) {
var promise = Promise.resolve(1)
var spy = o.spy(function(){throw 2})
promise.finally(spy).catch(function(reason){
o(reason).equals(2)
o(spy.callCount).equals(1)
o(spy.args.length).equals(0)
o(spy.this).equals(undefined)
done()
})
})
o("finally overrrides a fulfilled value when it returns a rejected Promise", function(done) {
var promise = Promise.resolve(1)
var spy = o.spy(function(){return Promise.reject(2)})
promise.finally(spy).catch(function(reason){
o(reason).equals(2)
o(spy.callCount).equals(1)
o(spy.args.length).equals(0)
o(spy.this).equals(undefined)
done()
})
})
o("finally overrrides a rejected reason when it throws", function(done) {
var promise = Promise.reject(1)
var spy = o.spy(function(){throw 2})
promise.finally(spy).catch(function(reason){
o(reason).equals(2)
o(spy.callCount).equals(1)
o(spy.args.length).equals(0)
o(spy.this).equals(undefined)
done()
})
})
o("finally overrrides a rejected reason when it returns a rejected Promise", function(done) {
var promise = Promise.reject(1)
var spy = o.spy(function(){return Promise.reject(2)})
promise.finally(spy).catch(function(reason){
o(reason).equals(2)
o(spy.callCount).equals(1)
o(spy.args.length).equals(0)
o(spy.this).equals(undefined)
done()
})
})
})
o.spec("resolve", function() {
o("resolves once", function(done) {

View file

@ -31,7 +31,8 @@ function compileSelector(selector) {
function execSelector(state, attrs, children) {
var hasAttrs = false, childList, text
var className = attrs.className || attrs.class
var classAttr = hasOwn.call(attrs, "class") ? "class" : "className"
var className = attrs[classAttr]
if (!isEmpty(state.attrs) && !isEmpty(attrs)) {
var newAttrs = {}
@ -46,21 +47,20 @@ function execSelector(state, attrs, children) {
}
for (var key in state.attrs) {
if (hasOwn.call(state.attrs, key)) {
if (hasOwn.call(state.attrs, key) && key !== "className" && !hasOwn.call(attrs, key)){
attrs[key] = state.attrs[key]
}
}
if (className != null || state.attrs.className != null) attrs.className =
className != null
? state.attrs.className != null
? state.attrs.className + " " + className
: className
: state.attrs.className != null
? state.attrs.className
: null
if (className !== undefined) {
if (attrs.class !== undefined) {
attrs.class = undefined
attrs.className = className
}
if (state.attrs.className != null) {
attrs.className = state.attrs.className + " " + className
}
}
if (classAttr === "class") attrs.class = null
for (var key in attrs) {
if (hasOwn.call(attrs, key) && key !== "key") {
@ -75,20 +75,15 @@ function execSelector(state, attrs, children) {
childList = children
}
return Vnode(state.tag, attrs.key, hasAttrs ? attrs : undefined, childList, text)
return Vnode(state.tag, attrs.key, hasAttrs ? attrs : null, childList, text)
}
function hyperscript(selector) {
// Because sloppy mode sucks
var attrs = arguments[1], start = 2, children
if (selector == null || typeof selector !== "string" && typeof selector !== "function" && typeof selector.view !== "function") {
throw Error("The selector must be either a string or a component.");
}
if (typeof selector === "string") {
var cached = selectorCache[selector] || compileSelector(selector)
}
var attrs = arguments[1], start = 2, children
if (attrs == null) {
attrs = {}
@ -105,12 +100,10 @@ function hyperscript(selector) {
while (start < arguments.length) children.push(arguments[start++])
}
var normalized = Vnode.normalizeChildren(children)
if (typeof selector === "string") {
return execSelector(cached, attrs, normalized)
return execSelector(selectorCache[selector] || compileSelector(selector), attrs, Vnode.normalizeChildren(children))
} else {
return Vnode(selector, attrs.key, attrs, normalized)
return Vnode(selector, attrs.key, attrs, children)
}
}

File diff suppressed because it is too large Load diff

View file

@ -21,6 +21,7 @@
<script src="test-fragment.js"></script>
<script src="test-normalize.js"></script>
<script src="test-normalizeChildren.js"></script>
<script src="test-normalizeComponentChildren.js"></script>
<script src="test-createText.js"></script>
<script src="test-createHTML.js"></script>
<script src="test-createFragment.js"></script>

View file

@ -44,47 +44,92 @@ o.spec("attributes", function() {
o(b.dom.hasAttribute("id")).equals(true)
o(b.dom.getAttribute("id")).equals("test")
// #1804
render(root, [c]);
// #1804
// TODO: uncomment
// o(c.dom.hasAttribute("id")).equals(false)
o(c.dom.hasAttribute("id")).equals(false)
})
})
o.spec("customElements", function(){
o("when vnode is customElement, custom setAttribute called", function(){
var normal = [
{tag: "input", attrs: {value: "hello"}},
{tag: "input", attrs: {value: "hello"}},
{tag: "input", attrs: {value: "hello"}}
]
var custom = [
{tag: "custom-element", attrs: {custom: "x"}},
{tag: "input", attrs: {is: "something-special", custom: "x"}},
{tag: "custom-element", attrs: {is: "something-special", custom: "x"}}
]
var view = normal.concat(custom)
o("when vnode is customElement without property, custom setAttribute called", function(){
var f = $window.document.createElement
var spy
var spies = []
$window.document.createElement = function(tag, is){
var el = f(tag, is)
if(!spy){
spy = o.spy(el.setAttribute)
}
var spy = o.spy(el.setAttribute)
el.setAttribute = spy
spies.push(spy)
spy.elem = el
return el
}
render(root, view)
render(root, [
{tag: "input", attrs: {value: "hello"}},
{tag: "input", attrs: {value: "hello"}},
{tag: "input", attrs: {value: "hello"}},
{tag: "custom-element", attrs: {custom: "x"}},
{tag: "input", attrs: {is: "something-special", custom: "x"}},
{tag: "custom-element", attrs: {is: "something-special", custom: "x"}}
])
o(spy.callCount).equals(custom.length)
o(spies[0].callCount).equals(0)
o(spies[1].callCount).equals(0)
o(spies[2].callCount).equals(0)
o(spies[3].calls).deepEquals([{this: spies[3].elem, args: ["custom", "x"]}])
o(spies[4].calls).deepEquals([{this: spies[4].elem, args: ["custom", "x"]}])
o(spies[5].calls).deepEquals([{this: spies[5].elem, args: ["custom", "x"]}])
})
o("when vnode is customElement with property, custom setAttribute not called", function(){
var f = $window.document.createElement
var spies = []
var getters = []
var setters = []
$window.document.createElement = function(tag, is){
var el = f(tag, is)
var spy = o.spy(el.setAttribute)
el.setAttribute = spy
spies.push(spy)
spy.elem = el
if (tag === "custom-element" || is && is.is === "something-special") {
var custom = "foo"
var getter, setter
Object.defineProperty(el, "custom", {
configurable: true,
enumerable: true,
get: getter = o.spy(function () { return custom }),
set: setter = o.spy(function (value) { custom = value })
})
getters.push(getter)
setters.push(setter)
}
return el
}
render(root, [
{tag: "input", attrs: {value: "hello"}},
{tag: "input", attrs: {value: "hello"}},
{tag: "input", attrs: {value: "hello"}},
{tag: "custom-element", attrs: {custom: "x"}},
{tag: "input", attrs: {is: "something-special", custom: "x"}},
{tag: "custom-element", attrs: {is: "something-special", custom: "x"}}
])
o(spies[0].callCount).equals(0)
o(spies[1].callCount).equals(0)
o(spies[2].callCount).equals(0)
o(spies[3].callCount).equals(0)
o(spies[4].callCount).equals(0)
o(spies[5].callCount).equals(0)
o(getters[0].callCount).equals(0)
o(getters[1].callCount).equals(0)
o(getters[2].callCount).equals(0)
o(setters[0].calls).deepEquals([{this: spies[3].elem, args: ["x"]}])
o(setters[1].calls).deepEquals([{this: spies[4].elem, args: ["x"]}])
o(setters[2].calls).deepEquals([{this: spies[5].elem, args: ["x"]}])
})
})
@ -147,7 +192,7 @@ o.spec("attributes", function() {
o("a lack of attribute removes `value`", function() {
var a = {tag: "input", attrs: {}}
var b = {tag: "input", attrs: {value: "test"}}
// var c = {tag: "input", attrs: {}}
var c = {tag: "input", attrs: {}}
render(root, [a])
@ -158,10 +203,9 @@ o.spec("attributes", function() {
o(a.dom.value).equals("test")
// https://github.com/MithrilJS/mithril.js/issues/1804#issuecomment-304521235
// TODO: Uncomment
// render(root, [c])
render(root, [c])
// o(a.dom.value).equals("")
o(a.dom.value).equals("")
})
o("can be set as number", function() {
var a = {tag: "input", attrs: {value: 1}}
@ -276,17 +320,16 @@ o.spec("attributes", function() {
o.spec("textarea.value", function() {
o("can be removed by not passing a value", function() {
var a = {tag: "textarea", attrs: {value:"x"}}
// var b = {tag: "textarea", attrs: {}}
var b = {tag: "textarea", attrs: {}}
render(root, [a])
o(a.dom.value).equals("x")
// https://github.com/MithrilJS/mithril.js/issues/1804#issuecomment-304521235
// TODO: Uncomment
// render(root, [b])
render(root, [b])
// o(b.dom.value).equals("")
o(b.dom.value).equals("")
})
o("isn't set when equivalent to the previous value and focused", function() {
var $window = domMock({spy: o.spy})
@ -352,7 +395,7 @@ o.spec("attributes", function() {
o(canvas.dom.width).equals(100)
})
})
o.spec("svg class", function() {
o.spec("svg", function() {
o("when className is specified then it should be added as a class", function() {
var a = {tag: "svg", attrs: {className: "test"}}
@ -360,6 +403,26 @@ o.spec("attributes", function() {
o(a.dom.attributes["class"].value).equals("test")
})
/* eslint-disable no-script-url */
o("handles xlink:href", function() {
var vnode = {tag: "svg", ns: "http://www.w3.org/2000/svg", children: [
{tag: "a", ns: "http://www.w3.org/2000/svg", attrs: {"xlink:href": "javascript:;"}}
]}
render(root, [vnode])
o(vnode.dom.nodeName).equals("svg")
o(vnode.dom.firstChild.attributes["href"].value).equals("javascript:;")
o(vnode.dom.firstChild.attributes["href"].namespaceURI).equals("http://www.w3.org/1999/xlink")
vnode = {tag: "svg", ns: "http://www.w3.org/2000/svg", children: [
{tag: "a", ns: "http://www.w3.org/2000/svg", attrs: {}}
]}
render(root, [vnode])
o(vnode.dom.nodeName).equals("svg")
o("href" in vnode.dom.firstChild.attributes).equals(false)
})
/* eslint-enable no-script-url */
})
o.spec("option.value", function() {
o("can be set as text", function() {
@ -376,7 +439,7 @@ o.spec("attributes", function() {
o(a.dom.value).equals("1")
})
o("null becomes the empty string", function() {
o("null removes the attribute", function() {
var a = {tag: "option", attrs: {value: null}}
var b = {tag: "option", attrs: {value: "test"}}
var c = {tag: "option", attrs: {value: null}}
@ -384,7 +447,7 @@ o.spec("attributes", function() {
render(root, [a]);
o(a.dom.value).equals("")
o(a.dom.getAttribute("value")).equals("")
o(a.dom.hasAttribute("value")).equals(false)
render(root, [b]);
@ -394,7 +457,7 @@ o.spec("attributes", function() {
render(root, [c]);
o(c.dom.value).equals("")
o(c.dom.getAttribute("value")).equals("")
o(c.dom.hasAttribute("value")).equals(false)
})
o("'' and 0 are different values", function() {
var a = {tag: "option", attrs: {value: 0}, children:[{tag:"#", children:""}]}
@ -462,6 +525,19 @@ o.spec("attributes", function() {
{tag:"option", attrs: {value: ""}}
]}
}
/* FIXME
This incomplete test is meant for testing #1916.
However it cannot be completed until #1978 is addressed
which is a lack a working select.selected / option.selected
attribute. Ask isiahmeadows.
o("render select options", function() {
var select = {tag: "select", selectedIndex: 0, children: [
{tag:"option", attrs: {value: "1", selected: ""}}
]}
render(root, select)
})
*/
o("can be set as text", function() {
var a = makeSelect()
var b = makeSelect("2")

View file

@ -764,97 +764,6 @@ o.spec("component", function() {
o(attrs[hook].callCount).equals(methods[hook].callCount)(hook)
})
})
o("lifecycle timing megatest (for a single component with the state overwritten)", function() {
var methods = {
view: o.spy(function(vnode) {
o(vnode.state).equals(1)
return ""
})
}
var attrs = {}
var hooks = [
"oninit", "oncreate", "onbeforeupdate",
"onupdate", "onbeforeremove", "onremove"
]
hooks.forEach(function(hook) {
// the `attrs` hooks are called before the component ones
attrs[hook] = o.spy(function(vnode) {
o(vnode.state).equals(1)
o(attrs[hook].callCount).equals(methods[hook].callCount + 1)
})
methods[hook] = o.spy(function(vnode) {
o(vnode.state).equals(1)
o(attrs[hook].callCount).equals(methods[hook].callCount)
})
})
var attrsOninit = attrs.oninit
var methodsOninit = methods.oninit
attrs.oninit = o.spy(function(vnode){
vnode.state = 1
return attrsOninit.call(this, vnode)
})
methods.oninit = o.spy(function(vnode){
vnode.state = 1
return methodsOninit.call(this, vnode)
})
var component = createComponent(methods)
o(methods.view.callCount).equals(0)
o(methods.oninit.callCount).equals(0)
o(methods.oncreate.callCount).equals(0)
o(methods.onbeforeupdate.callCount).equals(0)
o(methods.onupdate.callCount).equals(0)
o(methods.onbeforeremove.callCount).equals(0)
o(methods.onremove.callCount).equals(0)
hooks.forEach(function(hook) {
o(attrs[hook].callCount).equals(methods[hook].callCount)(hook)
})
render(root, [{tag: component, attrs: attrs}])
o(methods.view.callCount).equals(1)
o(methods.oninit.callCount).equals(1)
o(methods.oncreate.callCount).equals(1)
o(methods.onbeforeupdate.callCount).equals(0)
o(methods.onupdate.callCount).equals(0)
o(methods.onbeforeremove.callCount).equals(0)
o(methods.onremove.callCount).equals(0)
hooks.forEach(function(hook) {
o(attrs[hook].callCount).equals(methods[hook].callCount)(hook)
})
render(root, [{tag: component, attrs: attrs}])
o(methods.view.callCount).equals(2)
o(methods.oninit.callCount).equals(1)
o(methods.oncreate.callCount).equals(1)
o(methods.onbeforeupdate.callCount).equals(1)
o(methods.onupdate.callCount).equals(1)
o(methods.onbeforeremove.callCount).equals(0)
o(methods.onremove.callCount).equals(0)
hooks.forEach(function(hook) {
o(attrs[hook].callCount).equals(methods[hook].callCount)(hook)
})
render(root, [])
o(methods.view.callCount).equals(2)
o(methods.oninit.callCount).equals(1)
o(methods.oncreate.callCount).equals(1)
o(methods.onbeforeupdate.callCount).equals(1)
o(methods.onupdate.callCount).equals(1)
o(methods.onbeforeremove.callCount).equals(1)
o(methods.onremove.callCount).equals(1)
hooks.forEach(function(hook) {
o(attrs[hook].callCount).equals(methods[hook].callCount)(hook)
})
})
o("hook state and arguments validation", function(){
var methods = {
view: o.spy(function(vnode) {
@ -899,7 +808,7 @@ o.spec("component", function() {
o(methods[hook].args.length).equals(attrs[hook].args.length)(hook)
})
})
o("recycled components get a fresh state", function() {
o("no recycling occurs (was: recycled components get a fresh state)", function() {
var step = 0
var firstState
var view = o.spy(function(vnode) {
@ -918,7 +827,7 @@ o.spec("component", function() {
step = 1
render(root, [{tag: "div", children: [{tag: component, key: 1}]}])
o(child).equals(root.firstChild.firstChild)
o(child).notEquals(root.firstChild.firstChild) // this used to be a recycling pool test
o(view.callCount).equals(2)
})
})

View file

@ -1,4 +1,3 @@
/* eslint-disable no-script-url */
"use strict"
var o = require("../../ospec/ospec")
@ -54,6 +53,7 @@ o.spec("createElement", function() {
o(vnode.dom.childNodes[0].nodeName).equals("A")
o(vnode.dom.childNodes[1].nodeName).equals("B")
})
/* eslint-disable no-script-url */
o("creates svg", function() {
var vnode = {tag: "svg", ns: "http://www.w3.org/2000/svg", children: [
{tag: "a", ns: "http://www.w3.org/2000/svg", attrs: {"xlink:href": "javascript:;"}},
@ -71,6 +71,7 @@ o.spec("createElement", function() {
o(vnode.dom.childNodes[1].firstChild.nodeName).equals("body")
o(vnode.dom.childNodes[1].firstChild.namespaceURI).equals("http://www.w3.org/1999/xhtml")
})
/* eslint-enable no-script-url */
o("sets attributes correctly for svg", function() {
var vnode = {tag: "svg", ns: "http://www.w3.org/2000/svg", attrs: {viewBox: "0 0 100 100"}}
render(root, [vnode])

View file

@ -31,7 +31,7 @@ o.spec("createHTML", function() {
o(vnode.dom).equals(null)
o(vnode.domSize).equals(0)
})
o("handles multiple children", function() {
o("handles multiple children in HTML", function() {
var vnode = {tag: "<", children: "<a></a><b></b>"}
render(root, [vnode])
@ -51,4 +51,34 @@ o.spec("createHTML", function() {
o(vnode.dom.nodeName).equals(tag.toUpperCase())
})
})
o("creates SVG", function() {
var vnode = {tag: "<", children: "<g></g>"}
render(root, [{tag:"svg", children: [vnode]}])
o(vnode.dom.nodeName).equals("g")
o(vnode.dom.namespaceURI).equals("http://www.w3.org/2000/svg")
})
o("creates text SVG", function() {
var vnode = {tag: "<", children: "a"}
render(root, [{tag:"svg", children: [vnode]}])
o(vnode.dom.nodeValue).equals("a")
})
o("handles empty SVG", function() {
var vnode = {tag: "<", children: ""}
render(root, [{tag:"svg", children: [vnode]}])
o(vnode.dom).equals(null)
o(vnode.domSize).equals(0)
})
o("handles multiple children in SVG", function() {
var vnode = {tag: "<", children: "<g></g><text></text>"}
render(root, [{tag:"svg", children: [vnode]}])
o(vnode.domSize).equals(2)
o(vnode.dom.nodeName).equals("g")
o(vnode.dom.namespaceURI).equals("http://www.w3.org/2000/svg")
o(vnode.dom.nextSibling.nodeName).equals("text")
o(vnode.dom.nextSibling.namespaceURI).equals("http://www.w3.org/2000/svg")
})
})

View file

@ -32,6 +32,73 @@ o.spec("event", function() {
o(onevent.this).equals(div.dom)
o(onevent.args[0].type).equals("click")
o(onevent.args[0].target).equals(div.dom)
o(e.$defaultPrevented).equals(false)
o(e.$propagationStopped).equals(false)
})
o("handles onclick returning false", function() {
var spy = o.spy(function () { return false })
var div = {tag: "div", attrs: {onclick: spy}}
var e = $window.document.createEvent("MouseEvents")
e.initEvent("click", true, true)
render(root, [div])
div.dom.dispatchEvent(e)
o(spy.callCount).equals(1)
o(spy.this).equals(div.dom)
o(spy.args[0].type).equals("click")
o(spy.args[0].target).equals(div.dom)
o(onevent.callCount).equals(1)
o(onevent.this).equals(div.dom)
o(onevent.args[0].type).equals("click")
o(onevent.args[0].target).equals(div.dom)
o(e.$defaultPrevented).equals(true)
o(e.$propagationStopped).equals(true)
})
o("handles click EventListener object", function() {
var spy = o.spy()
var listener = {handleEvent: spy}
var div = {tag: "div", attrs: {onclick: listener}}
var e = $window.document.createEvent("MouseEvents")
e.initEvent("click", true, true)
render(root, [div])
div.dom.dispatchEvent(e)
o(spy.callCount).equals(1)
o(spy.this).equals(listener)
o(spy.args[0].type).equals("click")
o(spy.args[0].target).equals(div.dom)
o(onevent.callCount).equals(1)
o(onevent.this).equals(div.dom)
o(onevent.args[0].type).equals("click")
o(onevent.args[0].target).equals(div.dom)
o(e.$defaultPrevented).equals(false)
o(e.$propagationStopped).equals(false)
})
o("handles click EventListener object returning false", function() {
var spy = o.spy(function () { return false })
var listener = {handleEvent: spy}
var div = {tag: "div", attrs: {onclick: listener}}
var e = $window.document.createEvent("MouseEvents")
e.initEvent("click", true, true)
render(root, [div])
div.dom.dispatchEvent(e)
o(spy.callCount).equals(1)
o(spy.this).equals(listener)
o(spy.args[0].type).equals("click")
o(spy.args[0].target).equals(div.dom)
o(onevent.callCount).equals(1)
o(onevent.this).equals(div.dom)
o(onevent.args[0].type).equals("click")
o(onevent.args[0].target).equals(div.dom)
o(e.$defaultPrevented).equals(false)
o(e.$propagationStopped).equals(false)
})
o("removes event", function() {
@ -49,6 +116,129 @@ o.spec("event", function() {
o(spy.callCount).equals(0)
})
o("removes event when null", function() {
var spy = o.spy()
var vnode = {tag: "a", attrs: {onclick: spy}}
var updated = {tag: "a", attrs: {onclick: null}}
render(root, [vnode])
render(root, [updated])
var e = $window.document.createEvent("MouseEvents")
e.initEvent("click", true, true)
vnode.dom.dispatchEvent(e)
o(spy.callCount).equals(0)
})
o("removes event when undefined", function() {
var spy = o.spy()
var vnode = {tag: "a", attrs: {onclick: spy}}
var updated = {tag: "a", attrs: {onclick: undefined}}
render(root, [vnode])
render(root, [updated])
var e = $window.document.createEvent("MouseEvents")
e.initEvent("click", true, true)
vnode.dom.dispatchEvent(e)
o(spy.callCount).equals(0)
})
o("removes event added via addEventListener when null", function() {
var spy = o.spy()
var vnode = {tag: "a", attrs: {ontouchstart: spy}}
var updated = {tag: "a", attrs: {ontouchstart: null}}
render(root, [vnode])
render(root, [updated])
var e = $window.document.createEvent("TouchEvents")
e.initEvent("touchstart", true, true)
vnode.dom.dispatchEvent(e)
o(spy.callCount).equals(0)
})
o("removes event added via addEventListener", function() {
var spy = o.spy()
var vnode = {tag: "a", attrs: {ontouchstart: spy}}
var updated = {tag: "a", attrs: {}}
render(root, [vnode])
render(root, [updated])
var e = $window.document.createEvent("TouchEvents")
e.initEvent("touchstart", true, true)
vnode.dom.dispatchEvent(e)
o(spy.callCount).equals(0)
})
o("removes event added via addEventListener when undefined", function() {
var spy = o.spy()
var vnode = {tag: "a", attrs: {ontouchstart: spy}}
var updated = {tag: "a", attrs: {ontouchstart: undefined}}
render(root, [vnode])
render(root, [updated])
var e = $window.document.createEvent("TouchEvents")
e.initEvent("touchstart", true, true)
vnode.dom.dispatchEvent(e)
o(spy.callCount).equals(0)
})
o("removes EventListener object", function() {
var spy = o.spy()
var listener = {handleEvent: spy}
var vnode = {tag: "a", attrs: {onclick: listener}}
var updated = {tag: "a", attrs: {}}
render(root, [vnode])
render(root, [updated])
var e = $window.document.createEvent("MouseEvents")
e.initEvent("click", true, true)
vnode.dom.dispatchEvent(e)
o(spy.callCount).equals(0)
})
o("removes EventListener object when null", function() {
var spy = o.spy()
var listener = {handleEvent: spy}
var vnode = {tag: "a", attrs: {onclick: listener}}
var updated = {tag: "a", attrs: {onclick: null}}
render(root, [vnode])
render(root, [updated])
var e = $window.document.createEvent("MouseEvents")
e.initEvent("click", true, true)
vnode.dom.dispatchEvent(e)
o(spy.callCount).equals(0)
})
o("removes EventListener object when undefined", function() {
var spy = o.spy()
var listener = {handleEvent: spy}
var vnode = {tag: "a", attrs: {onclick: listener}}
var updated = {tag: "a", attrs: {onclick: undefined}}
render(root, [vnode])
render(root, [updated])
var e = $window.document.createEvent("MouseEvents")
e.initEvent("click", true, true)
vnode.dom.dispatchEvent(e)
o(spy.callCount).equals(0)
})
o("fires onclick only once after redraw", function() {
var spy = o.spy()
var div = {tag: "div", attrs: {id: "a", onclick: spy}}
@ -72,6 +262,30 @@ o.spec("event", function() {
o(div.dom.attributes["id"].value).equals("b")
})
o("fires click EventListener object only once after redraw", function() {
var spy = o.spy()
var listener = {handleEvent: spy}
var div = {tag: "div", attrs: {id: "a", onclick: listener}}
var updated = {tag: "div", attrs: {id: "b", onclick: listener}}
var e = $window.document.createEvent("MouseEvents")
e.initEvent("click", true, true)
render(root, [div])
render(root, [updated])
div.dom.dispatchEvent(e)
o(spy.callCount).equals(1)
o(spy.this).equals(listener)
o(spy.args[0].type).equals("click")
o(spy.args[0].target).equals(div.dom)
o(onevent.callCount).equals(1)
o(onevent.this).equals(div.dom)
o(onevent.args[0].type).equals("click")
o(onevent.args[0].target).equals(div.dom)
o(div.dom).equals(updated.dom)
o(div.dom.attributes["id"].value).equals("b")
})
o("handles ontransitionend", function() {
var spy = o.spy()
var div = {tag: "div", attrs: {ontransitionend: spy}}
@ -90,4 +304,24 @@ o.spec("event", function() {
o(onevent.args[0].type).equals("transitionend")
o(onevent.args[0].target).equals(div.dom)
})
o("handles transitionend EventListener object", function() {
var spy = o.spy()
var listener = {handleEvent: spy}
var div = {tag: "div", attrs: {ontransitionend: listener}}
var e = $window.document.createEvent("HTMLEvents")
e.initEvent("transitionend", true, true)
render(root, [div])
div.dom.dispatchEvent(e)
o(spy.callCount).equals(1)
o(spy.this).equals(listener)
o(spy.args[0].type).equals("transitionend")
o(spy.args[0].target).equals(div.dom)
o(onevent.callCount).equals(1)
o(onevent.this).equals(div.dom)
o(onevent.args[0].type).equals("transitionend")
o(onevent.args[0].target).equals(div.dom)
})
})

View file

@ -16,52 +16,51 @@ o.spec("hyperscript", function() {
o(vnode.tag).equals("a")
})
o("v1.0.1 bug-for-bug regression suite", function(){
o("class and className normalization", function(){
o(m("a", {
class: null
}).attrs).deepEquals({
class: undefined,
className: null
class: null
})
o(m("a", {
class: undefined
}).attrs).deepEquals({
class: undefined,
class: null
})
o(m("a", {
class: false
}).attrs).deepEquals({
class: undefined,
class: null,
className: false
})
o(m("a", {
class: true
}).attrs).deepEquals({
class: undefined,
class: null,
className: true
})
o(m("a.x", {
class: null
}).attrs).deepEquals({
class: undefined,
className: "x null"
class: null,
className: "x"
})
o(m("a.x", {
class: undefined
}).attrs).deepEquals({
class: undefined,
class: null,
className: "x"
})
o(m("a.x", {
class: false
}).attrs).deepEquals({
class: undefined,
class: null,
className: "x false"
})
o(m("a.x", {
class: true
}).attrs).deepEquals({
class: undefined,
class: null,
className: "x true"
})
o(m("a", {
@ -97,7 +96,7 @@ o.spec("hyperscript", function() {
o(m("a.x", {
className: false
}).attrs).deepEquals({
className: "x"
className: "x false"
})
o(m("a.x", {
className: true
@ -272,7 +271,7 @@ o.spec("hyperscript", function() {
var vnode = m("div", {key:"a"})
o(vnode.tag).equals("div")
o(vnode.attrs).equals(undefined)
o(vnode.attrs).equals(null)
o(vnode.key).equals("a")
})
o("handles many attrs", function() {
@ -303,6 +302,63 @@ o.spec("hyperscript", function() {
o(vnode.attrs.className).equals("a b")
})
})
o.spec("custom element attrs", function() {
o("handles string attr", function() {
var vnode = m("custom-element", {a: "b"})
o(vnode.tag).equals("custom-element")
o(vnode.attrs.a).equals("b")
})
o("handles falsy string attr", function() {
var vnode = m("custom-element", {a: ""})
o(vnode.tag).equals("custom-element")
o(vnode.attrs.a).equals("")
})
o("handles number attr", function() {
var vnode = m("custom-element", {a: 1})
o(vnode.tag).equals("custom-element")
o(vnode.attrs.a).equals(1)
})
o("handles falsy number attr", function() {
var vnode = m("custom-element", {a: 0})
o(vnode.tag).equals("custom-element")
o(vnode.attrs.a).equals(0)
})
o("handles boolean attr", function() {
var vnode = m("custom-element", {a: true})
o(vnode.tag).equals("custom-element")
o(vnode.attrs.a).equals(true)
})
o("handles falsy boolean attr", function() {
var vnode = m("custom-element", {a: false})
o(vnode.tag).equals("custom-element")
o(vnode.attrs.a).equals(false)
})
o("handles only key in attrs", function() {
var vnode = m("custom-element", {key:"a"})
o(vnode.tag).equals("custom-element")
o(vnode.attrs).equals(null)
o(vnode.key).equals("a")
})
o("handles many attrs", function() {
var vnode = m("custom-element", {a: "b", c: "d"})
o(vnode.tag).equals("custom-element")
o(vnode.attrs.a).equals("b")
o(vnode.attrs.c).equals("d")
})
o("handles className attrs property", function() {
var vnode = m("custom-element", {className: "a"})
o(vnode.attrs.className).equals("a")
})
})
o.spec("children", function() {
o("handles string single child", function() {
var vnode = m("div", {}, ["a"])
@ -490,20 +546,20 @@ o.spec("hyperscript", function() {
o("handles children without attr", function() {
var vnode = m("div", [m("i"), m("s")])
o(vnode.attrs).equals(undefined)
o(vnode.attrs).equals(null)
o(vnode.children[0].tag).equals("i")
o(vnode.children[1].tag).equals("s")
})
o("handles child without attr unwrapped", function() {
var vnode = m("div", m("i"))
o(vnode.attrs).equals(undefined)
o(vnode.attrs).equals(null)
o(vnode.children[0].tag).equals("i")
})
o("handles children without attr unwrapped", function() {
var vnode = m("div", m("i"), m("s"))
o(vnode.attrs).equals(undefined)
o(vnode.attrs).equals(null)
o(vnode.children[0].tag).equals("i")
o(vnode.children[1].tag).equals("s")
})
@ -524,6 +580,15 @@ o.spec("hyperscript", function() {
m(".a", attrs)
o(attrs).deepEquals({a: "b"})
})
o("non-nullish attr takes precedence over selector", function() {
o(m("[a=b]", {a: "c"}).attrs).deepEquals({a: "c"})
})
o("null attr takes precedence over selector", function() {
o(m("[a=b]", {a: null}).attrs).deepEquals({a: null})
})
o("undefined attr takes precedence over selector", function() {
o(m("[a=b]", {a: undefined}).attrs).deepEquals({a: undefined})
})
o("handles fragment children without attr unwrapped", function() {
var vnode = m("div", [m("i")], [m("s")])
@ -551,19 +616,29 @@ o.spec("hyperscript", function() {
o.spec("components", function() {
o("works with POJOs", function() {
var component = {
view: function() {
return m("div")
}
view: function() {}
}
var vnode = m(component, {id: "a"}, "b")
o(vnode.tag).equals(component)
o(vnode.attrs.id).equals("a")
o(vnode.children.length).equals(1)
o(vnode.children[0].tag).equals("#")
o(vnode.children[0].children).equals("b")
o(vnode.children[0]).equals("b")
})
o("works with functions", function() {
o("works with constructibles", function() {
var component = o.spy()
component.prototype.view = function() {}
var vnode = m(component, {id: "a"}, "b")
o(component.callCount).equals(0)
o(vnode.tag).equals(component)
o(vnode.attrs.id).equals("a")
o(vnode.children.length).equals(1)
o(vnode.children[0]).equals("b")
})
o("works with closures", function () {
var component = o.spy()
var vnode = m(component, {id: "a"}, "b")
@ -573,8 +648,7 @@ o.spec("hyperscript", function() {
o(vnode.tag).equals(component)
o(vnode.attrs.id).equals("a")
o(vnode.children.length).equals(1)
o(vnode.children[0].tag).equals("#")
o(vnode.children[0].children).equals("b")
o(vnode.children[0]).equals("b")
})
})
})

View file

@ -59,6 +59,16 @@ o.spec("form inputs", function() {
o(updated.dom.value).equals("aaa")
})
o("clear element value if vdom value is set to undefined (aka removed)", function() {
var input = {tag: "input", attrs: {value: "aaa", oninput: function() {}}}
var updated = {tag: "input", attrs: {value: undefined, oninput: function() {}}}
render(root, [input])
render(root, [updated])
o(updated.dom.value).equals("")
})
o("syncs input checked attribute if DOM value differs from vdom value", function() {
var input = {tag: "input", attrs: {type: "checkbox", checked: true, onclick: function() {}}}
var updated = {tag: "input", attrs: {type: "checkbox", checked: true, onclick: function() {}}}

View file

@ -0,0 +1,34 @@
"use strict"
var o = require("../../ospec/ospec")
var m = require("../../render/hyperscript")
var domMock = require("../../test-utils/domMock")
var vdom = require("../../render/render")
o.spec("component children", function () {
var $window = domMock()
var root = $window.document.createElement("div")
var render = vdom($window).render
o.spec("component children", function () {
var component = {
view: function (vnode) {
return vnode.children
}
}
var vnode = m(component, "a")
render(root, vnode)
o("are not normalized on ingestion", function () {
o(vnode.children[0]).equals("a")
})
o("are normalized upon view interpolation", function () {
o(vnode.instance.children.length).equals(1)
o(vnode.instance.children[0].tag).equals("#")
o(vnode.instance.children[0].children).equals("a")
})
})
})

View file

@ -36,10 +36,7 @@ o.spec("onbeforeremove", function() {
o(update.callCount).equals(0)
})
o("calls onbeforeremove when removing element", function(done) {
var vnode = {tag: "div", attrs: {
oninit: function() {vnode.state = {}},
onbeforeremove: remove
}}
var vnode = {tag: "div", attrs: {onbeforeremove: remove}}
render(root, [vnode])
render(root, [])
@ -47,6 +44,7 @@ o.spec("onbeforeremove", function() {
function remove(node) {
o(node).equals(vnode)
o(this).equals(vnode.state)
o(this != null && typeof this === "object").equals(true)
o(root.childNodes.length).equals(1)
o(root.firstChild).equals(vnode.dom)

View file

@ -129,7 +129,7 @@ o.spec("onbeforeupdate", function() {
render(root, temp)
render(root, updated)
o(vnodes[0].dom).equals(updated[0].dom)
o(vnodes[0].dom).notEquals(updated[0].dom) // this used to be a recycling pool test
o(updated[0].dom.nodeName).equals("DIV")
o(onbeforeupdate.callCount).equals(0)
})

View file

@ -199,4 +199,27 @@ o.spec("oninit", function() {
o(vnode.dom.oninit).equals(undefined)
o(vnode.dom.attributes["oninit"]).equals(undefined)
})
o("No spurious oninit calls in mapped keyed diff when the pool is involved (#1992)", function () {
var oninit1 = o.spy()
var oninit2 = o.spy()
var oninit3 = o.spy()
render(root, [
{tag: "p", key: 1, attrs: {oninit: oninit1}},
{tag: "p", key: 2, attrs: {oninit: oninit2}},
{tag: "p", key: 3, attrs: {oninit: oninit3}},
])
render(root, [
{tag: "p", key: 1, attrs: {oninit: oninit1}},
{tag: "p", key: 3, attrs: {oninit: oninit3}},
])
render(root, [
{tag: "p", key: 3, attrs: {oninit: oninit3}},
])
o(oninit1.callCount).equals(1)
o(oninit2.callCount).equals(1)
o(oninit3.callCount).equals(1)
})
})

View file

@ -91,7 +91,7 @@ o.spec("onremove", function() {
o(vnode.dom.attributes["onremove"]).equals(undefined)
o(vnode.events).equals(undefined)
})
o("calls onremove on recycle", function() {
o("calls onremove on keyed nodes", function() {
var remove = o.spy()
var vnodes = [{tag: "div", key: 1}]
var temp = [{tag: "div", key: 2, attrs: {onremove: remove}}]
@ -101,6 +101,7 @@ o.spec("onremove", function() {
render(root, temp)
render(root, updated)
o(vnodes[0].dom).notEquals(updated[0].dom) // this used to be a recycling pool test
o(remove.callCount).equals(1)
})
o("does not recycle when there's an onremove", function() {
@ -191,6 +192,28 @@ o.spec("onremove", function() {
o(spy.callCount).equals(0)
o(threw).equals(false)
})
o("onremove doesn't fire on nodes that go from pool to pool (#1990)", function() {
var onremove = o.spy();
render(root, [m("div", m("div")), m("div", m("div", {onremove: onremove}))]);
render(root, [m("div", m("div"))]);
render(root, []);
o(onremove.callCount).equals(1)
})
o("doesn't fire when removing the children of a node that's brought back from the pool (#1991 part 2)", function() {
var onremove = o.spy()
var vnode = {tag: "div", key: 1, children: [{tag: "div", attrs: {onremove: onremove}}]}
var temp = {tag: "div", key: 2}
var updated = {tag: "div", key: 1, children: [{tag: "p"}]}
render(root, [vnode])
render(root, [temp])
render(root, [updated])
o(vnode.dom).notEquals(updated.dom) // this used to be a recycling pool test
o(onremove.callCount).equals(1)
})
})
})
})

View file

@ -0,0 +1,614 @@
"use strict"
var o = require("../../ospec/ospec")
var m = require("../../render/hyperscript")
var domMock = require("../../test-utils/domMock")
var vdom = require("../../render/render")
o.spec("render/hyperscript integration", function() {
var $window, root, render
o.beforeEach(function() {
$window = domMock()
root = $window.document.createElement("div")
render = vdom($window).render
})
o.spec("setting class", function() {
o("selector only", function() {
render(root, m(".foo"))
o(root.firstChild.className).equals("foo")
})
o("class only", function() {
render(root, m("div", {class: "foo"}))
o(root.firstChild.className).equals("foo")
})
o("className only", function() {
render(root, m("div", {className: "foo"}))
o(root.firstChild.className).equals("foo")
})
o("selector and class", function() {
render(root, m(".bar", {class: "foo"}))
o(root.firstChild.className.split(" ").sort()).deepEquals(["bar", "foo"])
})
o("selector and className", function() {
render(root, m(".bar", {className: "foo"}))
o(root.firstChild.className.split(" ").sort()).deepEquals(["bar", "foo"])
})
o("selector and a null class", function() {
render(root, m(".foo", {class: null}))
o(root.firstChild.className).equals("foo")
})
o("selector and a null className", function() {
render(root, m(".foo", {className: null}))
o(root.firstChild.className).equals("foo")
})
o("selector and an undefined class", function() {
render(root, m(".foo", {class: undefined}))
o(root.firstChild.className).equals("foo")
})
o("selector and an undefined className", function() {
render(root, m(".foo", {className: undefined}))
o(root.firstChild.className).equals("foo")
})
})
o.spec("updating class", function() {
o.spec("from selector only", function() {
o("to selector only", function() {
render(root, m(".foo1"))
render(root, m(".foo2"))
o(root.firstChild.className).equals("foo2")
})
o("to class only", function() {
render(root, m(".foo1"))
render(root, m("div", {class: "foo2"}))
o(root.firstChild.className).equals("foo2")
})
o("to className only", function() {
render(root, m(".foo1"))
render(root, m("div", {className: "foo2"}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and class", function() {
render(root, m(".foo1"))
render(root, m(".bar2", {class: "foo2"}))
o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
})
o("to selector and className", function() {
render(root, m(".foo1"))
render(root, m(".bar2", {className: "foo2"}))
o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
})
o("to selector and a null class", function() {
render(root, m(".foo1"))
render(root, m(".foo2", {class: null}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and a null className", function() {
render(root, m(".foo1"))
render(root, m(".foo2", {className: null}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and an undefined class", function() {
render(root, m(".foo1"))
render(root, m(".foo2", {class: undefined}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and an undefined className", function() {
render(root, m(".foo1"))
render(root, m(".foo2", {className: undefined}))
o(root.firstChild.className).equals("foo2")
})
})
o.spec("from class only", function() {
o("to selector only", function() {
render(root, m("div", {class: "foo2"}))
render(root, m(".foo2"))
o(root.firstChild.className).equals("foo2")
})
o("to class only", function() {
render(root, m("div", {class: "foo2"}))
render(root, m("div", {class: "foo2"}))
o(root.firstChild.className).equals("foo2")
})
o("to className only", function() {
render(root, m("div", {class: "foo2"}))
render(root, m("div", {className: "foo2"}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and class", function() {
render(root, m("div", {class: "foo2"}))
render(root, m(".bar2", {class: "foo2"}))
o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
})
o("to selector and className", function() {
render(root, m(".bar2", {className: "foo2"}))
o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
})
o("to selector and a null class", function() {
render(root, m("div", {class: "foo2"}))
render(root, m(".foo2", {class: null}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and a null className", function() {
render(root, m("div", {class: "foo2"}))
render(root, m(".foo2", {className: null}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and an undefined class", function() {
render(root, m("div", {class: "foo2"}))
render(root, m(".foo2", {class: undefined}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and an undefined className", function() {
render(root, m("div", {class: "foo2"}))
render(root, m(".foo2", {className: undefined}))
o(root.firstChild.className).equals("foo2")
})
})
o.spec("from ", function() {
o("to selector only", function() {
render(root, m(".foo2"))
o(root.firstChild.className).equals("foo2")
})
o("to class only", function() {
render(root, m("div", {class: "foo2"}))
o(root.firstChild.className).equals("foo2")
})
o("to className only", function() {
render(root, m("div", {className: "foo2"}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and class", function() {
render(root, m(".bar2", {class: "foo2"}))
o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
})
o("to selector and className", function() {
render(root, m(".bar2", {className: "foo2"}))
o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
})
o("to selector and a null class", function() {
render(root, m(".foo2", {class: null}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and a null className", function() {
render(root, m(".foo2", {className: null}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and an undefined class", function() {
render(root, m(".foo2", {class: undefined}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and an undefined className", function() {
render(root, m(".foo2", {className: undefined}))
o(root.firstChild.className).equals("foo2")
})
})
o.spec("from className only", function() {
o("to selector only", function() {
render(root, m("div", {className: "foo1"}))
render(root, m(".foo2"))
o(root.firstChild.className).equals("foo2")
})
o("to class only", function() {
render(root, m("div", {className: "foo1"}))
render(root, m("div", {class: "foo2"}))
o(root.firstChild.className).equals("foo2")
})
o("to className only", function() {
render(root, m("div", {className: "foo1"}))
render(root, m("div", {className: "foo2"}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and class", function() {
render(root, m("div", {className: "foo1"}))
render(root, m(".bar2", {class: "foo2"}))
o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
})
o("to selector and className", function() {
render(root, m("div", {className: "foo1"}))
render(root, m(".bar2", {className: "foo2"}))
o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
})
o("to selector and a null class", function() {
render(root, m("div", {className: "foo1"}))
render(root, m(".foo2", {class: null}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and a null className", function() {
render(root, m("div", {className: "foo1"}))
render(root, m(".foo2", {className: null}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and an undefined class", function() {
render(root, m("div", {className: "foo1"}))
render(root, m(".foo2", {class: undefined}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and an undefined className", function() {
render(root, m("div", {className: "foo1"}))
render(root, m(".foo2", {className: undefined}))
o(root.firstChild.className).equals("foo2")
})
})
o.spec("from selector and class", function() {
o("to selector only", function() {
render(root, m(".bar1", {class: "foo1"}))
render(root, m(".foo2"))
o(root.firstChild.className).equals("foo2")
})
o("to class only", function() {
render(root, m(".bar1", {class: "foo1"}))
render(root, m("div", {class: "foo2"}))
o(root.firstChild.className).equals("foo2")
})
o("to className only", function() {
render(root, m(".bar1", {class: "foo1"}))
render(root, m("div", {className: "foo2"}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and class", function() {
render(root, m(".bar1", {class: "foo1"}))
render(root, m(".bar2", {class: "foo2"}))
o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
})
o("to selector and className", function() {
render(root, m(".bar1", {class: "foo1"}))
render(root, m(".bar2", {className: "foo2"}))
o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
})
o("to selector and a null class", function() {
render(root, m(".bar1", {class: "foo1"}))
render(root, m(".foo2", {class: null}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and a null className", function() {
render(root, m(".bar1", {class: "foo1"}))
render(root, m(".foo2", {className: null}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and an undefined class", function() {
render(root, m(".bar1", {class: "foo1"}))
render(root, m(".foo2", {class: undefined}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and an undefined className", function() {
render(root, m(".bar1", {class: "foo1"}))
render(root, m(".foo2", {className: undefined}))
o(root.firstChild.className).equals("foo2")
})
})
o.spec("from selector and className", function() {
o("to selector only", function() {
render(root, m(".bar1", {className: "foo1"}))
render(root, m(".foo2"))
o(root.firstChild.className).equals("foo2")
})
o("to class only", function() {
render(root, m(".bar1", {className: "foo1"}))
render(root, m("div", {class: "foo2"}))
o(root.firstChild.className).equals("foo2")
})
o("to className only", function() {
render(root, m(".bar1", {className: "foo1"}))
render(root, m("div", {className: "foo2"}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and class", function() {
render(root, m(".bar1", {className: "foo1"}))
render(root, m(".bar2", {class: "foo2"}))
o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
})
o("to selector and className", function() {
render(root, m(".bar1", {className: "foo1"}))
render(root, m(".bar2", {className: "foo2"}))
o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
})
o("to selector and a null class", function() {
render(root, m(".bar1", {className: "foo1"}))
render(root, m(".foo2", {class: null}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and a null className", function() {
render(root, m(".bar1", {className: "foo1"}))
render(root, m(".foo2", {className: null}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and an undefined class", function() {
render(root, m(".bar1", {className: "foo1"}))
render(root, m(".foo2", {class: undefined}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and an undefined className", function() {
render(root, m(".bar1", {className: "foo1"}))
render(root, m(".foo2", {className: undefined}))
o(root.firstChild.className).equals("foo2")
})
})
o.spec("from and a null class", function() {
o("to selector only", function() {
render(root, m(".foo1", {class: null}))
render(root, m(".foo2"))
o(root.firstChild.className).equals("foo2")
})
o("to class only", function() {
render(root, m(".foo1", {class: null}))
render(root, m("div", {class: "foo2"}))
o(root.firstChild.className).equals("foo2")
})
o("to className only", function() {
render(root, m(".foo1", {class: null}))
render(root, m("div", {className: "foo2"}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and class", function() {
render(root, m(".foo1", {class: null}))
render(root, m(".bar2", {class: "foo2"}))
o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
})
o("to selector and className", function() {
render(root, m(".foo1", {class: null}))
render(root, m(".bar2", {className: "foo2"}))
o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
})
o("to selector and a null class", function() {
render(root, m(".foo1", {class: null}))
render(root, m(".foo2", {class: null}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and a null className", function() {
render(root, m(".foo1", {class: null}))
render(root, m(".foo2", {className: null}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and an undefined class", function() {
render(root, m(".foo1", {class: null}))
render(root, m(".foo2", {class: undefined}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and an undefined className", function() {
render(root, m(".foo1", {class: null}))
render(root, m(".foo2", {className: undefined}))
o(root.firstChild.className).equals("foo2")
})
})
o.spec("from selector and a null className", function() {
o("to selector only", function() {
render(root, m(".foo1", {className: null}))
render(root, m(".foo2"))
o(root.firstChild.className).equals("foo2")
})
o("to class only", function() {
render(root, m(".foo1", {className: null}))
render(root, m("div", {class: "foo2"}))
o(root.firstChild.className).equals("foo2")
})
o("to className only", function() {
render(root, m(".foo1", {className: null}))
render(root, m("div", {className: "foo2"}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and class", function() {
render(root, m(".foo1", {className: null}))
render(root, m(".bar2", {class: "foo2"}))
o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
})
o("to selector and className", function() {
render(root, m(".foo1", {className: null}))
render(root, m(".bar2", {className: "foo2"}))
o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
})
o("to selector and a null class", function() {
render(root, m(".foo1", {className: null}))
render(root, m(".foo2", {class: null}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and a null className", function() {
render(root, m(".foo1", {className: null}))
render(root, m(".foo2", {className: null}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and an undefined class", function() {
render(root, m(".foo1", {className: null}))
render(root, m(".foo2", {class: undefined}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and an undefined className", function() {
render(root, m(".foo1", {className: null}))
render(root, m(".foo2", {className: undefined}))
o(root.firstChild.className).equals("foo2")
})
})
o.spec("from selector and an undefined class", function() {
o("to selector only", function() {
render(root, m(".foo1", {class: undefined}))
render(root, m(".foo2"))
o(root.firstChild.className).equals("foo2")
})
o("to class only", function() {
render(root, m(".foo1", {class: undefined}))
render(root, m("div", {class: "foo2"}))
o(root.firstChild.className).equals("foo2")
})
o("to className only", function() {
render(root, m(".foo1", {class: undefined}))
render(root, m("div", {className: "foo2"}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and class", function() {
render(root, m(".foo1", {class: undefined}))
render(root, m(".bar2", {class: "foo2"}))
o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
})
o("to selector and className", function() {
render(root, m(".foo1", {class: undefined}))
render(root, m(".bar2", {className: "foo2"}))
o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
})
o("to selector and a null class", function() {
render(root, m(".foo1", {class: undefined}))
render(root, m(".foo2", {class: null}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and a null className", function() {
render(root, m(".foo1", {class: undefined}))
render(root, m(".foo2", {className: null}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and an undefined class", function() {
render(root, m(".foo1", {class: undefined}))
render(root, m(".foo2", {class: undefined}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and an undefined className", function() {
render(root, m(".foo1", {class: undefined}))
render(root, m(".foo2", {className: undefined}))
o(root.firstChild.className).equals("foo2")
})
})
o.spec("from selector and an undefined className", function() {
o("to selector only", function() {
render(root, m(".foo1", {className: undefined}))
render(root, m(".foo2"))
o(root.firstChild.className).equals("foo2")
})
o("to class only", function() {
render(root, m(".foo1", {className: undefined}))
render(root, m("div", {class: "foo2"}))
o(root.firstChild.className).equals("foo2")
})
o("to className only", function() {
render(root, m(".foo1", {className: undefined}))
render(root, m("div", {className: "foo2"}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and class", function() {
render(root, m(".foo1", {className: undefined}))
render(root, m(".bar2", {class: "foo2"}))
o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
})
o("to selector and className", function() {
render(root, m(".foo1", {className: undefined}))
render(root, m(".bar2", {className: "foo2"}))
o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
})
o("to selector and a null class", function() {
render(root, m(".foo1", {className: undefined}))
render(root, m(".foo2", {class: null}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and a null className", function() {
render(root, m(".foo1", {className: undefined}))
render(root, m(".foo2", {className: null}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and an undefined class", function() {
render(root, m(".foo1", {className: undefined}))
render(root, m(".foo2", {class: undefined}))
o(root.firstChild.className).equals("foo2")
})
o("to selector and an undefined className", function() {
render(root, m(".foo1", {className: undefined}))
render(root, m(".foo2", {className: undefined}))
o(root.firstChild.className).equals("foo2")
})
})
})
})

View file

@ -192,6 +192,85 @@ o.spec("updateElement", function() {
o(updated.dom.style.backgroundColor).equals("")
o(updated.dom.style.color).equals("gold")
})
o("does not re-render element styles for equivalent style objects", function() {
var style = {color: "gold"}
var vnode = {tag: "a", attrs: {style: style}}
render(root, [vnode])
root.firstChild.style.color = "red"
style = {color: "gold"}
var updated = {tag: "a", attrs: {style: style}}
render(root, [updated])
o(updated.dom.style.color).equals("red")
})
o("setting style to `null` removes all styles", function() {
var vnode = {"tag": "p", attrs: {style: "background-color: red"}}
var updated = {"tag": "p", attrs: {style: null}}
render(root, [vnode])
o("style" in vnode.dom.attributes).equals(true)
o(vnode.dom.attributes.style.value).equals("background-color: red;")
render(root, [updated])
//browsers disagree here
try {
o(updated.dom.attributes.style.value).equals("")
} catch (e) {
o("style" in updated.dom.attributes).equals(false)
}
})
o("setting style to `undefined` removes all styles", function() {
var vnode = {"tag": "p", attrs: {style: "background-color: red"}}
var updated = {"tag": "p", attrs: {style: undefined}}
render(root, [vnode])
o("style" in vnode.dom.attributes).equals(true)
o(vnode.dom.attributes.style.value).equals("background-color: red;")
render(root, [updated])
//browsers disagree here
try {
o(updated.dom.attributes.style.value).equals("")
} catch (e) {
o("style" in updated.dom.attributes).equals(false)
}
})
o("not setting style removes all styles", function() {
var vnode = {"tag": "p", attrs: {style: "background-color: red"}}
var updated = {"tag": "p", attrs: {}}
render(root, [vnode])
o("style" in vnode.dom.attributes).equals(true)
o(vnode.dom.attributes.style.value).equals("background-color: red;")
render(root, [updated])
//browsers disagree here
try {
o(updated.dom.attributes.style.value).equals("")
} catch (e) {
o("style" in updated.dom.attributes).equals(false)
}
})
o("replaces el", function() {
var vnode = {tag: "a"}
var updated = {tag: "b"}
@ -224,7 +303,7 @@ o.spec("updateElement", function() {
o(updated.dom.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg")
})
o("restores correctly when recycling", function() {
o("doesn't restore since we're not recycling", function() {
var vnode = {tag: "div", key: 1}
var updated = {tag: "div", key: 2}
@ -237,9 +316,9 @@ o.spec("updateElement", function() {
var c = vnode.dom
o(root.childNodes.length).equals(1)
o(a).equals(c)
o(a).notEquals(c) // this used to be a recycling pool test
})
o("restores correctly when recycling via map", function() {
o("doesn't restore since we're not recycling (via map)", function() {
var a = {tag: "div", key: 1}
var b = {tag: "div", key: 2}
var c = {tag: "div", key: 3}
@ -256,6 +335,6 @@ o.spec("updateElement", function() {
var y = root.childNodes[1]
o(root.childNodes.length).equals(3)
o(x).equals(y)
o(x).notEquals(y) // this used to be a recycling pool test
})
})

View file

@ -134,17 +134,17 @@ o.spec("updateNodes", function() {
o("reverses els w/ odd count", function() {
var vnodes = [{tag: "a", key: 1}, {tag: "b", key: 2}, {tag: "i", key: 3}]
var updated = [{tag: "i", key: 3}, {tag: "b", key: 2}, {tag: "a", key: 1}]
var expectedTags = updated.map(function(vn) {return vn.tag})
render(root, vnodes)
render(root, updated)
var tagNames = [].map.call(root.childNodes, function(n) {return n.nodeName.toLowerCase()})
o(root.childNodes.length).equals(3)
o(updated[0].dom.nodeName).equals("I")
o(updated[0].dom).equals(root.childNodes[0])
o(updated[1].dom.nodeName).equals("B")
o(updated[1].dom).equals(root.childNodes[1])
o(updated[2].dom.nodeName).equals("A")
o(updated[2].dom).equals(root.childNodes[2])
o(tagNames).deepEquals(expectedTags)
})
o("creates el at start", function() {
var vnodes = [{tag: "a", key: 1}]
@ -264,6 +264,21 @@ o.spec("updateNodes", function() {
o(updated[2].dom.nodeName).equals("S")
o(updated[2].dom).equals(root.childNodes[2])
})
o("creates, deletes, reverses els at same time with '__proto__' key", function() {
var vnodes = [{tag: "a", key: "__proto__"}, {tag: "i", key: 3}, {tag: "b", key: 2}]
var updated = [{tag: "b", key: 2}, {tag: "a", key: "__proto__"}, {tag: "s", key: 4}]
render(root, vnodes)
render(root, updated)
o(root.childNodes.length).equals(3)
o(updated[0].dom.nodeName).equals("B")
o(updated[0].dom).equals(root.childNodes[0])
o(updated[1].dom.nodeName).equals("A")
o(updated[1].dom).equals(root.childNodes[1])
o(updated[2].dom.nodeName).equals("S")
o(updated[2].dom).equals(root.childNodes[2])
})
o("adds to empty array followed by el", function() {
var vnodes = [{tag: "[", key: 1, children: []}, {tag: "b", key: 2}]
var updated = [{tag: "[", key: 1, children: [{tag: "a"}]}, {tag: "b", key: 2}]
@ -614,6 +629,38 @@ o.spec("updateNodes", function() {
o(updated[0].dom.nodeName).equals("I")
o(updated[0].dom).equals(root.childNodes[0])
})
o("cached keyed nodes move when the list is reversed", function(){
var a = {tag: "a", key: "a"}
var b = {tag: "b", key: "b"}
var c = {tag: "c", key: "c"}
var d = {tag: "d", key: "d"}
render(root, [a, b, c, d])
render(root, [d, c, b, a])
o(root.childNodes.length).equals(4)
o(root.childNodes[0].nodeName).equals("D")
o(root.childNodes[1].nodeName).equals("C")
o(root.childNodes[2].nodeName).equals("B")
o(root.childNodes[3].nodeName).equals("A")
})
o("cached keyed nodes move when diffed via the map", function() {
var onupdate = o.spy()
var a = {tag: "a", key: "a", attrs: {onupdate: onupdate}}
var b = {tag: "b", key: "b", attrs: {onupdate: onupdate}}
var c = {tag: "c", key: "c", attrs: {onupdate: onupdate}}
var d = {tag: "d", key: "d", attrs: {onupdate: onupdate}}
render(root, [a, b, c, d])
render(root, [b, d, a, c])
o(root.childNodes.length).equals(4)
o(root.childNodes[0].nodeName).equals("B")
o(root.childNodes[1].nodeName).equals("D")
o(root.childNodes[2].nodeName).equals("A")
o(root.childNodes[3].nodeName).equals("C")
o(onupdate.callCount).equals(0)
})
o("removes then create different bigger", function() {
var vnodes = [{tag: "a", key: 1}, {tag: "b", key: 2}]
var temp = []
@ -776,7 +823,7 @@ o.spec("updateNodes", function() {
o(root.childNodes[0].childNodes[1].childNodes.length).equals(1)
o(root.childNodes[1].childNodes.length).equals(0)
})
o("recycles", function() {
o("doesn't recycle", function() {
var vnodes = [{tag: "div", key: 1}]
var temp = []
var updated = [{tag: "div", key: 1}]
@ -785,10 +832,10 @@ o.spec("updateNodes", function() {
render(root, temp)
render(root, updated)
o(vnodes[0].dom).equals(updated[0].dom)
o(vnodes[0].dom).notEquals(updated[0].dom) // this used to be a recycling pool test
o(updated[0].dom.nodeName).equals("DIV")
})
o("recycles when not keyed", function() {
o("doesn't recycle when not keyed", function() {
var vnodes = [{tag: "div"}]
var temp = []
var updated = [{tag: "div"}]
@ -798,19 +845,22 @@ o.spec("updateNodes", function() {
render(root, updated)
o(root.childNodes.length).equals(1)
o(vnodes[0].dom).equals(updated[0].dom)
o(vnodes[0].dom).notEquals(updated[0].dom) // this used to be a recycling pool test
o(updated[0].dom.nodeName).equals("DIV")
})
o("recycles deep", function() {
o("doesn't recycle deep", function() {
var vnodes = [{tag: "div", children: [{tag: "a", key: 1}]}]
var temp = [{tag: "div"}]
var updated = [{tag: "div", children: [{tag: "a", key: 1}]}]
render(root, vnodes)
var oldChild = vnodes[0].dom.firstChild
render(root, temp)
render(root, updated)
o(vnodes[0].dom.firstChild).equals(updated[0].dom.firstChild)
o(oldChild).notEquals(updated[0].dom.firstChild) // this used to be a recycling pool test
o(updated[0].dom.firstChild.nodeName).equals("A")
})
o("mixed unkeyed tags are not broken by recycle", function() {
@ -839,6 +889,19 @@ o.spec("updateNodes", function() {
o(root.childNodes[0].nodeName).equals("A")
o(root.childNodes[1].nodeName).equals("B")
})
o("onremove doesn't fire from nodes in the pool (#1990)", function () {
var onremove = o.spy()
render(root, [
{tag: "div", children: [{tag: "div", attrs: {onremove: onremove}}]},
{tag: "div", children: [{tag: "div", attrs: {onremove: onremove}}]}
])
render(root, [
{tag: "div", children: [{tag: "div", attrs: {onremove: onremove}}]}
])
render(root,[])
o(onremove.callCount).equals(2)
})
o("cached, non-keyed nodes skip diff", function () {
var onupdate = o.spy();
var cached = {tag:"a", attrs:{onupdate: onupdate}}
@ -857,6 +920,72 @@ o.spec("updateNodes", function() {
o(onupdate.callCount).equals(0)
})
o("keyed cached elements are re-initialized when brought back from the pool (#2003)", function () {
var onupdate = o.spy()
var oncreate = o.spy()
var cached = {
tag: "B", key: 1, children: [
{tag: "A", attrs: {oncreate: oncreate, onupdate: onupdate}, text: "A"}
]
}
render(root, [{tag: "div", children: [cached]}])
render(root, [])
render(root, [{tag: "div", children: [cached]}])
o(oncreate.callCount).equals(2)
o(onupdate.callCount).equals(0)
})
o("unkeyed cached elements are re-initialized when brought back from the pool (#2003)", function () {
var onupdate = o.spy()
var oncreate = o.spy()
var cached = {
tag: "B", children: [
{tag: "A", attrs: {oncreate: oncreate, onupdate: onupdate}, text: "A"}
]
}
render(root, [{tag: "div", children: [cached]}])
render(root, [])
render(root, [{tag: "div", children: [cached]}])
o(oncreate.callCount).equals(2)
o(onupdate.callCount).equals(0)
})
o("keyed cached elements are re-initialized when brought back from nested pools (#2003)", function () {
var onupdate = o.spy()
var oncreate = o.spy()
var cached = {
tag: "B", key: 1, children: [
{tag: "A", attrs: {oncreate: oncreate, onupdate: onupdate}, text: "A"}
]
}
render(root, [{tag: "div", children: [cached]}])
render(root, [{tag: "div", children: []}])
render(root, [])
render(root, [{tag: "div", children: [cached]}])
o(oncreate.callCount).equals(2)
o(onupdate.callCount).equals(0)
})
o("unkeyed cached elements are re-initialized when brought back from nested pools (#2003)", function () {
var onupdate = o.spy()
var oncreate = o.spy()
var cached = {
tag: "B", children: [
{tag: "A", attrs: {oncreate: oncreate, onupdate: onupdate}, text: "A"}
]
}
render(root, [{tag: "div", children: [cached]}])
render(root, [{tag: "div", children: []}])
render(root, [])
render(root, [{tag: "div", children: [cached]}])
o(oncreate.callCount).equals(2)
o(onupdate.callCount).equals(0)
})
o("null stays in place", function() {
var create = o.spy()
var update = o.spy()
@ -904,6 +1033,231 @@ o.spec("updateNodes", function() {
o(vnode.dom).notEquals(updated.dom)
})
o("don't add back elements from fragments that are restored from the pool #1991", function() {
render(root, [
{tag: "[", children: []},
{tag: "[", children: []}
])
render(root, [
{tag: "[", children: []},
{tag: "[", children: [{tag: "div"}]}
])
render(root, [
{tag: "[", children: [null]}
])
render(root, [
{tag: "[", children: []},
{tag: "[", children: []}
])
o(root.childNodes.length).equals(0)
})
o("don't add back elements from fragments that are being removed #1991", function() {
render(root, [
{tag: "[", children: []},
{tag: "p"},
])
render(root, [
{tag: "[", children: [{tag: "div", text: 5}]}
])
render(root, [
{tag: "[", children: []},
{tag: "[", children: []}
])
o(root.childNodes.length).equals(0)
})
o("handles null values in unkeyed lists of different length (#2003)", function() {
var oncreate = o.spy();
var onremove = o.spy();
var onupdate = o.spy();
function attrs() {
return {oncreate: oncreate, onremove: onremove, onupdate: onupdate}
}
render(root, [{tag: "div", attrs: attrs()}, null]);
render(root, [null, {tag: "div", attrs: attrs()}, null]);
o(oncreate.callCount).equals(2)
o(onremove.callCount).equals(1)
o(onupdate.callCount).equals(0)
})
o("supports changing the element of a keyed element in a list when traversed bottom-up", function() {
try {
render(root, [{tag: "a", key: 2}])
render(root, [{tag: "b", key: 1}, {tag: "b", key: 2}])
o(root.childNodes.length).equals(2)
o(root.childNodes[0].nodeName).equals("B")
o(root.childNodes[1].nodeName).equals("B")
} catch (e) {
o(e).equals(null)
}
})
o("supports changing the element of a keyed element in a list when looking up nodes using the map", function() {
try {
render(root, [{tag: "x", key: 1}, {tag: "y", key: 2}, {tag: "z", key: 3}])
render(root, [{tag: "b", key: 2}, {tag: "c", key: 1}, {tag: "d", key: 4}, {tag: "e", key: 3}])
o(root.childNodes.length).equals(4)
o(root.childNodes[0].nodeName).equals("B")
o(root.childNodes[1].nodeName).equals("C")
o(root.childNodes[2].nodeName).equals("D")
o(root.childNodes[3].nodeName).equals("E")
} catch (e) {
o(e).equals(null)
}
})
o("don't fetch the nextSibling from the pool", function() {
render(root, [{tag: "[", children: [{tag: "div", key: 1}, {tag: "div", key: 2}]}, {tag: "p"}])
render(root, [{tag: "[", children: []}, {tag: "p"}])
render(root, [{tag: "[", children: [{tag: "div", key: 2}, {tag: "div", key: 1}]}, {tag: "p"}])
o([].map.call(root.childNodes, function(el) {return el.nodeName})).deepEquals(["DIV", "DIV", "P"])
})
o("minimizes DOM operations when scrambling a keyed lists", function() {
var vnodes = [{tag: "a", key: "a"}, {tag: "b", key: "b"}, {tag: "c", key: "c"}, {tag: "d", key: "d"}]
var updated = [{tag: "b", key: "b"}, {tag: "a", key: "a"}, {tag: "d", key: "d"}, {tag: "c", key: "c"}]
var expectedTagNames = updated.map(function(vn) {return vn.tag})
render(root, vnodes)
root.appendChild = o.spy(root.appendChild)
root.insertBefore = o.spy(root.insertBefore)
render(root, updated)
var tagNames = [].map.call(root.childNodes, function(n) {return n.nodeName.toLowerCase()})
o(root.appendChild.callCount + root.insertBefore.callCount).equals(2)
o(tagNames).deepEquals(expectedTagNames)
})
o("minimizes DOM operations when reversing a keyed lists with an odd number of items", function() {
var vnodes = [{tag: "a", key: "a"}, {tag: "b", key: "b"}, {tag: "c", key: "c"}, {tag: "d", key: "d"}]
var updated = [{tag: "d", key: "d"}, {tag: "c", key: "c"}, {tag: "b", key: "b"}, {tag: "a", key: "a"}]
var expectedTagNames = updated.map(function(vn) {return vn.tag})
render(root, vnodes)
root.appendChild = o.spy(root.appendChild)
root.insertBefore = o.spy(root.insertBefore)
render(root, updated)
var tagNames = [].map.call(root.childNodes, function(n) {return n.nodeName.toLowerCase()})
o(root.appendChild.callCount + root.insertBefore.callCount).equals(3)
o(tagNames).deepEquals(expectedTagNames)
})
o("minimizes DOM operations when reversing a keyed lists with an even number of items", function() {
var vnodes = [{tag: "a", key: "a"}, {tag: "b", key: "b"}, {tag: "c", key: "c"}]
var updated = [{tag: "c", key: "c"}, {tag: "b", key: "b"}, {tag: "a", key: "a"}]
var expectedTagNames = updated.map(function(vn) {return vn.tag})
render(root, vnodes)
root.appendChild = o.spy(root.appendChild)
root.insertBefore = o.spy(root.insertBefore)
render(root, updated)
var tagNames = [].map.call(root.childNodes, function(n) {return n.nodeName.toLowerCase()})
o(root.appendChild.callCount + root.insertBefore.callCount).equals(2)
o(tagNames).deepEquals(expectedTagNames)
})
o("minimizes DOM operations when scrambling a keyed lists with prefixes and suffixes", function() {
var vnodes = [{tag: "i", key: "i"}, {tag: "a", key: "a"}, {tag: "b", key: "b"}, {tag: "c", key: "c"}, {tag: "d", key: "d"}, {tag: "j", key: "j"}]
var updated = [{tag: "i", key: "i"}, {tag: "b", key: "b"}, {tag: "a", key: "a"}, {tag: "d", key: "d"}, {tag: "c", key: "c"}, {tag: "j", key: "j"}]
var expectedTagNames = updated.map(function(vn) {return vn.tag})
render(root, vnodes)
root.appendChild = o.spy(root.appendChild)
root.insertBefore = o.spy(root.insertBefore)
render(root, updated)
var tagNames = [].map.call(root.childNodes, function(n) {return n.nodeName.toLowerCase()})
o(root.appendChild.callCount + root.insertBefore.callCount).equals(2)
o(tagNames).deepEquals(expectedTagNames)
})
o("minimizes DOM operations when reversing a keyed lists with an odd number of items with prefixes and suffixes", function() {
var vnodes = [{tag: "i", key: "i"}, {tag: "a", key: "a"}, {tag: "b", key: "b"}, {tag: "c", key: "c"}, {tag: "d", key: "d"}, {tag: "j", key: "j"}]
var updated = [{tag: "i", key: "i"}, {tag: "d", key: "d"}, {tag: "c", key: "c"}, {tag: "b", key: "b"}, {tag: "a", key: "a"}, {tag: "j", key: "j"}]
var expectedTagNames = updated.map(function(vn) {return vn.tag})
render(root, vnodes)
root.appendChild = o.spy(root.appendChild)
root.insertBefore = o.spy(root.insertBefore)
render(root, updated)
var tagNames = [].map.call(root.childNodes, function(n) {return n.nodeName.toLowerCase()})
o(root.appendChild.callCount + root.insertBefore.callCount).equals(3)
o(tagNames).deepEquals(expectedTagNames)
})
o("minimizes DOM operations when reversing a keyed lists with an even number of items with prefixes and suffixes", function() {
var vnodes = [{tag: "i", key: "i"}, {tag: "a", key: "a"}, {tag: "b", key: "b"}, {tag: "c", key: "c"}, {tag: "j", key: "j"}]
var updated = [{tag: "i", key: "i"}, {tag: "c", key: "c"}, {tag: "b", key: "b"}, {tag: "a", key: "a"}, {tag: "j", key: "j"}]
var expectedTagNames = updated.map(function(vn) {return vn.tag})
render(root, vnodes)
root.appendChild = o.spy(root.appendChild)
root.insertBefore = o.spy(root.insertBefore)
render(root, updated)
var tagNames = [].map.call(root.childNodes, function(n) {return n.nodeName.toLowerCase()})
o(root.appendChild.callCount + root.insertBefore.callCount).equals(2)
o(tagNames).deepEquals(expectedTagNames)
})
o("scrambling sample 1", function() {
function vnodify(str) {
return str.split(",").map(function(k) {return {tag: k, key: k}})
}
var vnodes = vnodify("k0,k1,k2,k3,k4,k5,k6,k7,k8,k9")
var updated = vnodify("k4,k1,k2,k9,k0,k3,k6,k5,k8,k7")
var expectedTagNames = updated.map(function(vn) {return vn.tag})
render(root, vnodes)
root.appendChild = o.spy(root.appendChild)
root.insertBefore = o.spy(root.insertBefore)
render(root, updated)
var tagNames = [].map.call(root.childNodes, function(n) {return n.nodeName.toLowerCase()})
o(root.appendChild.callCount + root.insertBefore.callCount).equals(5)
o(tagNames).deepEquals(expectedTagNames)
})
o("scrambling sample 2", function() {
function vnodify(str) {
return str.split(",").map(function(k) {return {tag: k, key: k}})
}
var vnodes = vnodify("k0,k1,k2,k3,k4,k5,k6,k7,k8,k9")
var updated = vnodify("b,d,k1,k0,k2,k3,k4,a,c,k5,k6,k7,k8,k9")
var expectedTagNames = updated.map(function(vn) {return vn.tag})
render(root, vnodes)
root.appendChild = o.spy(root.appendChild)
root.insertBefore = o.spy(root.insertBefore)
render(root, updated)
var tagNames = [].map.call(root.childNodes, function(n) {return n.nodeName.toLowerCase()})
o(root.appendChild.callCount + root.insertBefore.callCount).equals(5)
o(tagNames).deepEquals(expectedTagNames)
})
components.forEach(function(cmp){
o.spec(cmp.kind, function(){
var createComponent = cmp.create
@ -940,6 +1294,19 @@ o.spec("updateNodes", function() {
o(root.childNodes[0].nodeName).equals("A")
o(root.childNodes[1].nodeName).equals("S")
})
o("removing a component that returns a fragment doesn't throw (regression test for incidental bug introduced while debugging some Flems)", function() {
var component = createComponent({
view: function() {return {tag: "[", children:[{tag: "a"}, {tag: "b"}]}}
})
try {
render(root, [{tag: component}])
render(root, [])
o(root.childNodes.length).equals(0)
} catch (e) {
o(e).equals(null)
}
})
})
})
})

View file

@ -0,0 +1,157 @@
"use strict"
var o = require("../../ospec/ospec")
var domMock = require("../../test-utils/domMock")
var vdom = require("../../render/render")
// pilfered and adapted from https://github.com/domvm/domvm/blob/7aaec609e4c625b9acf9a22d035d6252a5ca654f/test/src/flat-list-keyed-fuzz.js
o.spec("updateNodes keyed list Fuzzer", function() {
var i = 0, $window, root, render
o.beforeEach(function() {
$window = domMock()
root = $window.document.createElement("div")
render = vdom($window).render
})
void [
{delMax: 0, movMax: 50, insMax: 9},
{delMax: 3, movMax: 5, insMax: 5},
{delMax: 7, movMax: 15, insMax: 0},
{delMax: 5, movMax: 100, insMax: 3},
{delMax: 5, movMax: 0, insMax: 3},
].forEach(function(c) {
var tests = 250
while (tests--) {
var test = fuzzTest(c.delMax, c.movMax, c.insMax)
o(i++ + ": " + test.list.join() + " -> " + test.updated.join(), function() {
render(root, test.list.map(function(x){return {tag: x, key: x}}))
addSpies(root)
render(root, test.updated.map(function(x){return {tag: x, key: x}}))
if (root.appendChild.callCount + root.insertBefore.callCount !== test.expected.creations + test.expected.moves) console.log(test, {aC: root.appendChild.callCount, iB: root.insertBefore.callCount}, [].map.call(root.childNodes, function(n){return n.nodeName.toLowerCase()}))
o(root.appendChild.callCount + root.insertBefore.callCount).equals(test.expected.creations + test.expected.moves)("moves")
o(root.removeChild.callCount).equals(test.expected.deletions)("deletions")
o([].map.call(root.childNodes, function(n){return n.nodeName.toLowerCase()})).deepEquals(test.updated)
})
}
})
})
// https://en.wikipedia.org/wiki/Longest_increasing_subsequence
// impl borrowed from https://github.com/ivijs/ivi
function longestIncreasingSubsequence(a) {
var p = a.slice()
var result = []
result.push(0)
var u
var v
for (var i = 0, il = a.length; i < il; ++i) {
var j = result[result.length - 1]
if (a[j] < a[i]) {
p[i] = j
result.push(i)
continue
}
u = 0
v = result.length - 1
while (u < v) {
var c = ((u + v) / 2) | 0 // eslint-disable-line no-bitwise
if (a[result[c]] < a[i]) {
u = c + 1
} else {
v = c
}
}
if (a[i] < a[result[u]]) {
if (u > 0) {
p[i] = result[u - 1]
}
result[u] = i
}
}
u = result.length
v = result[u - 1]
while (u-- > 0) {
result[u] = v
v = p[v]
}
return result
}
function rand(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min
}
function ins(arr, qty) {
var p = ["a","b","c","d","e","f","g","h","i"]
while (qty-- > 0)
arr.splice(rand(0, arr.length - 1), 0, p.shift())
}
function del(arr, qty) {
while (qty-- > 0)
arr.splice(rand(0, arr.length - 1), 1)
}
function mov(arr, qty) {
while (qty-- > 0) {
var from = rand(0, arr.length - 1)
var to = rand(0, arr.length - 1)
arr.splice(to, 0, arr.splice(from, 1)[0])
}
}
function fuzzTest(delMax, movMax, insMax) {
var list = ["k0","k1","k2","k3","k4","k5","k6","k7","k8","k9"]
var copy = list.slice()
var delCount = rand(0, delMax),
movCount = rand(0, movMax),
insCount = rand(0, insMax)
del(copy, delCount)
mov(copy, movCount)
var expected = {
creations: insCount,
deletions: delCount,
moves: 0
}
if (movCount > 0) {
var newPos = copy.map(function(v) {
return list.indexOf(v)
}).filter(function(i) {
return i != -1
})
var lis = longestIncreasingSubsequence(newPos)
expected.moves = copy.length - lis.length
}
ins(copy, insCount)
return {
expected: expected,
list: list,
updated: copy
}
}
function addSpies(node) {
node.appendChild = o.spy(node.appendChild)
node.insertBefore = o.spy(node.insertBefore)
node.removeChild = o.spy(node.removeChild)
}

View file

@ -1,16 +1,17 @@
"use strict"
function Vnode(tag, key, attrs, children, text, dom) {
return {tag: tag, key: key, attrs: attrs, children: children, text: text, dom: dom, domSize: undefined, state: undefined, _state: undefined, events: undefined, instance: undefined, skip: false}
return {tag: tag, key: key, attrs: attrs, children: children, text: text, dom: dom, domSize: undefined, state: undefined, events: undefined, instance: undefined}
}
Vnode.normalize = function(node) {
if (Array.isArray(node)) return Vnode("[", undefined, undefined, Vnode.normalizeChildren(node), undefined, undefined)
if (node != null && typeof node !== "object") return Vnode("#", undefined, undefined, node === false ? "" : node, undefined, undefined)
return node
}
Vnode.normalizeChildren = function normalizeChildren(children) {
for (var i = 0; i < children.length; i++) {
children[i] = Vnode.normalize(children[i])
Vnode.normalizeChildren = function normalizeChildren(input) {
var children = []
for (var i = 0; i < input.length; i++) {
children[i] = Vnode.normalize(input[i])
}
return children
}

View file

@ -75,6 +75,10 @@ module.exports = function($window, Promise) {
}
if (args.withCredentials) xhr.withCredentials = args.withCredentials
if (args.timeout) xhr.timeout = args.timeout
if (args.responseType) xhr.responseType = args.responseType
for (var key in args.headers) if ({}.hasOwnProperty.call(args.headers, key)) {
xhr.setRequestHeader(key, args.headers[key])
}
@ -88,12 +92,13 @@ module.exports = function($window, Promise) {
if (xhr.readyState === 4) {
try {
var response = (args.extract !== extract) ? args.extract(xhr, args) : args.deserialize(args.extract(xhr, args))
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304 || FILE_PROTOCOL_REGEX.test(args.url)) {
if (args.extract !== extract || (xhr.status >= 200 && xhr.status < 300) || xhr.status === 304 || FILE_PROTOCOL_REGEX.test(args.url)) {
resolve(cast(args.type, response))
}
else {
var error = new Error(xhr.responseText)
for (var key in response) error[key] = response[key]
error.code = xhr.status
error.response = response
reject(error)
}
}
@ -159,7 +164,7 @@ module.exports = function($window, Promise) {
function deserialize(data) {
try {return data !== "" ? JSON.parse(data) : null}
catch (e) {throw new Error(data)}
catch (e) {throw new Error("Invalid JSON: " + data)}
}
function extract(xhr) {return xhr.responseText}

View file

@ -230,7 +230,7 @@ o.spec("xhr", function() {
}
})
xhr({method: "GET", url: "/item", deserialize: deserialize}).then(function(data) {
o(data).equals('{"test":123}')
o(data).equals("{\"test\":123}")
}).then(done)
})
o("deserialize parameter works in POST", function(done) {
@ -244,7 +244,7 @@ o.spec("xhr", function() {
}
})
xhr({method: "POST", url: "/item", deserialize: deserialize}).then(function(data) {
o(data).equals('{"test":123}')
o(data).equals("{\"test\":123}")
}).then(done)
})
o("extract parameter works in GET", function(done) {
@ -258,7 +258,7 @@ o.spec("xhr", function() {
}
})
xhr({method: "GET", url: "/item", extract: extract}).then(function(data) {
o(data).equals('{"test":123}')
o(data).equals("{\"test\":123}")
}).then(done)
})
o("extract parameter works in POST", function(done) {
@ -272,7 +272,7 @@ o.spec("xhr", function() {
}
})
xhr({method: "POST", url: "/item", extract: extract}).then(function(data) {
o(data).equals('{"test":123}')
o(data).equals("{\"test\":123}")
}).then(done)
})
o("ignores deserialize if extract is defined", function(done) {
@ -435,6 +435,34 @@ o.spec("xhr", function() {
done()
})
})
o("set timeout to xhr instance", function() {
mock.$defineRoutes({
"GET /item": function() {
return {status: 200, responseText: ""}
}
})
return xhr({
method: "GET", url: "/item",
timeout: 42,
config: function(xhr) {
o(xhr.timeout).equals(42)
}
})
})
o("set responseType to xhr instance", function() {
mock.$defineRoutes({
"GET /item": function() {
return {status: 200, responseText: ""}
}
})
return xhr({
method: "GET", url: "/item",
responseType: "blob",
config: function(xhr) {
o(xhr.responseType).equals("blob")
}
})
})
/*o("data maintains after interpolate", function() {
mock.$defineRoutes({
"PUT /items/:x": function() {
@ -458,9 +486,10 @@ o.spec("xhr", function() {
xhr({method: "GET", url: "/item"}).catch(function(e) {
o(e instanceof Error).equals(true)
o(e.message).equals(JSON.stringify({error: "error"}))
o(e.code).equals(500)
}).then(done)
})
o("extends Error with JSON response", function(done) {
o("adds response to Error", function(done) {
mock.$defineRoutes({
"GET /item": function() {
return {status: 500, responseText: JSON.stringify({message: "error", stack: "error on line 1"})}
@ -468,8 +497,8 @@ o.spec("xhr", function() {
})
xhr({method: "GET", url: "/item"}).catch(function(e) {
o(e instanceof Error).equals(true)
o(e.message).equals("error")
o(e.stack).equals("error on line 1")
o(e.response.message).equals("error")
o(e.response.stack).equals("error on line 1")
}).then(done)
})
o("rejects on non-JSON server error", function(done) {
@ -479,7 +508,7 @@ o.spec("xhr", function() {
}
})
xhr({method: "GET", url: "/item"}).catch(function(e) {
o(e.message).equals("error")
o(e.message).equals("Invalid JSON: error")
}).then(done)
})
o("triggers all branched catches upon rejection", function(done) {
@ -518,5 +547,35 @@ o.spec("xhr", function() {
o(e instanceof Error).equals(true)
}).then(done)
})
o("does not reject on status error code when extract provided", function(done) {
mock.$defineRoutes({
"GET /item": function() {
return {status: 500, responseText: JSON.stringify({message: "error"})}
}
})
xhr({
method: "GET", url: "/item",
extract: function(xhr) {return JSON.parse(xhr.responseText)}
}).then(function(data) {
o(data.message).equals("error")
done()
})
})
o("rejects on error in extract", function(done) {
mock.$defineRoutes({
"GET /item": function() {
return {status: 200, responseText: JSON.stringify({a: 1})}
}
})
xhr({
method: "GET", url: "/item",
extract: function() {throw new Error("error")}
}).catch(function(e) {
o(e instanceof Error).equals(true)
o(e.message).equals("error")
}).then(function() {
done()
})
})
})
})

7
stream/change-log.md Normal file
View file

@ -0,0 +1,7 @@
# Change log for stream
## 2.0.0
- stream: Removed `valueOf` & `toString` methods ([#2150](https://github.com/MithrilJS/mithril.js/pull/2150)
## 1.1.0
- stream: Move the "use strict" directive inside the IIFE [#1831](https://github.com/MithrilJS/mithril.js/issues/1831) ([#1893](https://github.com/MithrilJS/mithril.js/pull/1893))

View file

@ -19,7 +19,7 @@ function initStream(stream) {
stream.constructor = createStream
stream._state = {id: guid++, value: undefined, state: 0, derive: undefined, recover: undefined, deps: {}, parents: [], endStream: undefined, unregister: undefined}
stream.map = stream["fantasy-land/map"] = map, stream["fantasy-land/ap"] = ap, stream["fantasy-land/of"] = createStream
stream.valueOf = valueOf, stream.toJSON = toJSON, stream.toString = valueOf
stream.toJSON = toJSON
Object.defineProperties(stream, {
end: {get: function() {
@ -53,7 +53,7 @@ function updateDependency(stream, mustSync) {
var state = stream._state, parents = state.parents
if (parents.length > 0 && parents.every(active) && (mustSync || parents.some(changed))) {
var value = stream._state.derive()
if (value === HALT) return false
if (value === HALT) return unregisterStream(stream)
updateState(stream, value)
}
}
@ -101,7 +101,6 @@ function unregisterStream(stream) {
function map(fn) {return combine(function(stream) {return fn(stream())}, [this])}
function ap(stream) {return combine(function(s1, s2) {return s1()(s2())}, [stream, this])}
function valueOf() {return this._state.value}
function toJSON() {return this._state.value != null && typeof this._state.value.toJSON === "function" ? this._state.value.toJSON() : this._state.value}
function valid(stream) {return stream._state }
@ -117,7 +116,9 @@ function merge(streams) {
function scan(reducer, seed, stream) {
var newStream = combine(function (s) {
return seed = reducer(seed, s._state.value)
var next = reducer(seed, s._state.value)
if (next !== HALT) return seed = next
return HALT
}, [stream])
if (newStream._state.state === 0) newStream(seed)

View file

@ -8,11 +8,8 @@
<script src="../../ospec/ospec.js"></script>
<script src="../../test-utils/callAsync.js"></script>
<script src="../stream.js"></script>
<script src="../scan.js"></script>
<script src="../scanMerge.js"></script>
<script src="test-stream.js"></script>
<script src="test-scanMerge.js"></script>
<script src="test-scanMerge.js"></script>
<script>require("../../ospec/ospec").run()</script>
</body>

View file

@ -30,4 +30,36 @@ o.spec("scan", function() {
o(result[2]).equals(undefined)
o(result[3]).deepEquals({a: 1})
})
o("reducer can return HALT to prevent child updates", function() {
var count = 0
var action = stream()
var store = stream.scan(function (arr, value) {
switch (typeof value) {
case "number":
return arr.concat(value)
default:
return stream.HALT
}
}, [], action)
var child = store.map(function (p) {
count++
return p
})
var result
action(7)
action("11")
action(undefined)
action({a: 1})
result = child()
// check we got the expect result
o(result[0]).equals(7)
// check child received minimum # of updates
o(count).equals(2)
})
})

View file

@ -164,6 +164,27 @@ o.spec("stream", function() {
o(b()).equals(undefined)
o(count).equals(0)
})
o("combine can conditionaly halt", function() {
var count = 0
var halt = false
var a = Stream(1)
var b = Stream.combine(function(a) {
if (halt) {
return Stream.HALT
}
return a()
}, [a])["fantasy-land/map"](function(a) {
count++
return a
})
o(b()).equals(1)
o(count).equals(1)
halt = true
count = 0
a(2)
o(b()).equals(1)
o(count).equals(0)
})
o("combine will throw with a helpful error if given non-stream values", function () {
var spy = o.spy()
var a = Stream(1)
@ -276,31 +297,6 @@ o.spec("stream", function() {
o(spy.callCount).equals(1)
})
})
o.spec("valueOf", function() {
o("works", function() {
o(Stream(1).valueOf()).equals(1)
o(Stream("a").valueOf()).equals("a")
o(Stream(true).valueOf()).equals(true)
o(Stream(null).valueOf()).equals(null)
o(Stream(undefined).valueOf()).equals(undefined)
o(Stream({a: 1}).valueOf()).deepEquals({a: 1})
o(Stream([1, 2, 3]).valueOf()).deepEquals([1, 2, 3])
o(Stream().valueOf()).equals(undefined)
})
o("allows implicit value access in mathematical operations", function() {
o(Stream(1) + Stream(1)).equals(2)
})
})
o.spec("toString", function() {
o("aliases valueOf", function() {
var stream = Stream(1)
o(stream.toString).equals(stream.valueOf)
})
o("allows implicit value access in string operations", function() {
o(Stream("a") + Stream("b")).equals("ab")
})
})
o.spec("toJSON", function() {
o("works", function() {
o(Stream(1).toJSON()).equals(1)

View file

@ -2,7 +2,7 @@
/*
Known limitations:
- the innerHTML setter and the DOMParser only support a small subset of the true HTML/XML syntax.
- `option.selected` can't be set/read when the option doesn't have a `select` parent
- `element.attributes` is just a map of attribute names => Attr objects stubs
- ...
@ -18,6 +18,9 @@ module.exports = function(options) {
options = options || {}
var spy = options.spy || function(f){return f}
var spymap = []
var hasOwn = ({}.hasOwnProperty)
function registerSpies(element, spies) {
if(options.spy) {
var i = spymap.indexOf(element)
@ -37,6 +40,30 @@ module.exports = function(options) {
function isModernEvent(type) {
return type === "transitionstart" || type === "transitionend" || type === "animationstart" || type === "animationend"
}
function dispatchEvent(e) {
var stopped = false
e.stopImmediatePropagation = function() {
e.stopPropagation()
stopped = true
}
e.currentTarget = this
if (this._events[e.type] != null) {
for (var i = 0; i < this._events[e.type].handlers.length; i++) {
var useCapture = this._events[e.type].options[i].capture
if (useCapture && e.eventPhase < 3 || !useCapture && e.eventPhase > 1) {
var handler = this._events[e.type].handlers[i]
if (typeof handler === "function") try {handler.call(this, e)} catch(e) {setTimeout(function(){throw e})}
else try {handler.handleEvent(e)} catch(e) {setTimeout(function(){throw e})}
if (stopped) return
}
}
}
// this is inaccurate. Normally the event fires in definition order, including legacy events
// this would require getters/setters for each of them though and we haven't gotten around to
// adding them since it would be at a high perf cost or would entail some heavy refactoring of
// the mocks (prototypes instead of closures).
if (e.eventPhase > 1 && typeof this["on" + e.type] === "function" && !isModernEvent(e.type)) try {this["on" + e.type](e)} catch(e) {setTimeout(function(){throw e})}
}
function appendChild(child) {
var ancestor = this
while (ancestor !== child && ancestor !== null) ancestor = ancestor.parentNode
@ -47,7 +74,7 @@ module.exports = function(options) {
var index = this.childNodes.indexOf(child)
if (index > -1) this.childNodes.splice(index, 1)
if (child.nodeType === 11) {
while (child.firstChild != null) this.appendChild(child.firstChild)
while (child.firstChild != null) appendChild.call(this, child.firstChild)
child.childNodes = []
}
else {
@ -75,8 +102,9 @@ module.exports = function(options) {
var index = this.childNodes.indexOf(child)
if (reference !== null && refIndex < 0) throw new TypeError("Invalid argument")
if (index > -1) this.childNodes.splice(index, 1)
if (reference === null) this.appendChild(child)
if (reference === null) appendChild.call(this, child)
else {
if (index !== -1 && refIndex > index) refIndex--
if (child.nodeType === 11) {
this.childNodes.splice.apply(this.childNodes, [refIndex, 0].concat(child.childNodes))
while (child.firstChild) {
@ -102,9 +130,8 @@ module.exports = function(options) {
// this is the correct kind of conversion, passing a Symbol throws in browsers too.
var nodeValue = "" + value
/*eslint-enable no-implicit-coercion*/
this.attributes[name] = {
namespaceURI: null,
namespaceURI: hasOwn.call(this.attributes, name) ? this.attributes[name].namespaceURI : null,
get value() {return nodeValue},
set value(value) {
/*eslint-disable no-implicit-coercion*/
@ -159,9 +186,43 @@ module.exports = function(options) {
res.unshift(declList)
return res
}
function parseMarkup(value, root, voidElements, xmlns) {
var depth = 0, stack = [root]
value.replace(/<([a-z0-9\-]+?)((?:\s+?[^=]+?=(?:"[^"]*?"|'[^']*?'|[^\s>]*))*?)(\s*\/)?>|<\/([a-z0-9\-]+?)>|([^<]+)/g, function(match, startTag, attrs, selfClosed, endTag, text) {
if (startTag) {
var element = xmlns == null ? $window.document.createElement(startTag) : $window.document.createElementNS(xmlns, startTag)
attrs.replace(/\s+?([^=]+?)=(?:"([^"]*?)"|'([^']*?)'|([^\s>]*))/g, function(match, key, doubleQuoted, singleQuoted, unquoted) {
var keyParts = key.split(":")
var name = keyParts.pop()
var ns = keyParts[0]
var value = doubleQuoted || singleQuoted || unquoted || ""
if (ns != null) element.setAttributeNS(ns, name, value)
else element.setAttribute(name, value)
})
stack[depth].appendChild(element)
if (!selfClosed && voidElements.indexOf(startTag.toLowerCase()) < 0) stack[++depth] = element
}
else if (endTag) {
depth--
}
else if (text) {
stack[depth].appendChild($window.document.createTextNode(text)) // FIXME handle html entities
}
})
}
function DOMParser() {}
DOMParser.prototype.parseFromString = function(src, mime) {
if (mime !== "image/svg+xml") throw new Error("The DOMParser mock only supports the \"image/svg+xml\" MIME type")
var match = src.match(/^<svg xmlns="http:\/\/www\.w3\.org\/2000\/svg">(.*)<\/svg>$/)
if (!match) throw new Error("Please provide a bare SVG tag with the xmlns as only attribute")
var value = match[1]
var root = $window.document.createElementNS("http://www.w3.org/2000/svg", "svg")
parseMarkup(value, root, [], "http://www.w3.org/2000/svg")
return {documentElement: root}
}
var activeElement
var $window = {
DOMParser: DOMParser,
document: {
createElement: function(tag) {
var cssText = ""
@ -186,7 +247,7 @@ module.exports = function(options) {
}
}
}
cssText = buf.join(" ")
element.setAttribute("style", cssText = buf.join(" "))
}
}
})
@ -217,33 +278,21 @@ module.exports = function(options) {
},
set textContent(value) {
this.childNodes = []
if (value !== "") this.appendChild($window.document.createTextNode(value))
if (value !== "") appendChild.call(this, $window.document.createTextNode(value))
},
set innerHTML(value) {
var voidElements = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"]
while (this.firstChild) this.removeChild(this.firstChild)
var stack = [this], depth = 0, voidElements = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"]
value.replace(/<([a-z0-9\-]+?)((?:\s+?[^=]+?=(?:"[^"]*?"|'[^']*?'|[^\s>]*))*?)(\s*\/)?>|<\/([a-z0-9\-]+?)>|([^<]+)/g, function(match, startTag, attrs, selfClosed, endTag, text) {
if (startTag) {
var element = $window.document.createElement(startTag)
attrs.replace(/\s+?([^=]+?)=(?:"([^"]*?)"|'([^']*?)'|([^\s>]*))/g, function(match, key, doubleQuoted, singleQuoted, unquoted) {
var keyParts = key.split(":")
var name = keyParts.pop()
var ns = keyParts[0]
var value = doubleQuoted || singleQuoted || unquoted || ""
if (ns != null) element.setAttributeNS(ns, name, value)
else element.setAttribute(name, value)
})
stack[depth].appendChild(element)
if (!selfClosed && voidElements.indexOf(startTag.toLowerCase()) < 0) stack[++depth] = element
}
else if (endTag) {
depth--
}
else if (text) {
stack[depth].appendChild($window.document.createTextNode(text)) // FIXME handle html entities
}
})
var match = value.match(/^<svg xmlns="http:\/\/www\.w3\.org\/2000\/svg">(.*)<\/svg>$/), root, ns
if (match) {
var value = match[1]
root = $window.document.createElementNS("http://www.w3.org/2000/svg", "svg")
ns = "http://www.w3.org/2000/svg"
appendChild.call(this, root)
} else {
root = this
}
parseMarkup(value, root, voidElements, ns)
},
get style() {
return style
@ -260,39 +309,121 @@ module.exports = function(options) {
else this.setAttribute("class", value)
},
focus: function() {activeElement = this},
addEventListener: function(type, callback) {
if (events[type] == null) events[type] = [callback]
else events[type].push(callback)
addEventListener: function(type, handler, options) {
if (arguments.length > 2) {
if (typeof options === "object" && options != null) throw new TypeError("NYI: addEventListener options")
else if (typeof options !== "boolean") throw new TypeError("boolean expected for useCapture")
else options = {capture: options}
} else {
options = {capture: false}
}
if (events[type] == null) events[type] = {handlers: [handler], options: [options]}
else {
var found = false
for (var i = 0; i < events[type].handlers.length; i++) {
if (events[type].handlers[i] === handler && events[type].options[i].capture === options.capture) {
found = true
break
}
}
if (!found) {
events[type].handlers.push(handler)
events[type].options.push(options)
}
}
},
removeEventListener: function(type, callback) {
removeEventListener: function(type, handler, options) {
if (arguments.length > 2) {
if (typeof options === "object" && options != null) throw new TypeError("NYI: addEventListener options")
else if (typeof options !== "boolean") throw new TypeError("boolean expected for useCapture")
else options = {capture: options}
} else {
options = {capture: false}
}
if (events[type] != null) {
var index = events[type].indexOf(callback)
if (index > -1) events[type].splice(index, 1)
for (var i = 0; i < events[type].handlers.length; i++) {
if (events[type].handlers[i] === handler && events[type].options[i].capture === options.capture) {
events[type].handlers.splice(i, 1)
events[type].options.splice(i, 1)
break;
}
}
}
},
dispatchEvent: function(e) {
if (this.nodeName === "INPUT" && this.attributes["type"] != null && this.attributes["type"].value === "checkbox" && e.type === "click") {
this.checked = !this.checked
var parents = []
if (this.parentNode != null) {
var parent = this.parentNode
do {
parents.push(parent)
parent = parent.parentNode
} while (parent != null)
}
e.target = this
if (events[e.type] != null) {
for (var i = 0; i < events[e.type].length; i++) {
events[e.type][i].call(this, e)
var prevented = false
e.preventDefault = function() {
prevented = true
}
Object.defineProperty(e, "$defaultPrevented", {
configurable: true,
get: function () { return prevented }
})
var stopped = false
e.stopPropagation = function() {
stopped = true
}
Object.defineProperty(e, "$propagationStopped", {
configurable: true,
get: function () { return prevented }
})
e.eventPhase = 1
try {
for (var i = parents.length - 1; 0 <= i; i--) {
dispatchEvent.call(parents[i], e)
if (stopped) {
return
}
}
e.eventPhase = 2
dispatchEvent.call(this, e)
if (stopped) {
return
}
e.eventPhase = 3
for (var i = 0; i < parents.length; i++) {
dispatchEvent.call(parents[i], e)
if (stopped) {
return
}
}
} catch(e) {
throw e
} finally {
e.eventPhase = 0
if (!prevented) {
if (this.nodeName === "INPUT" && this.attributes["type"] != null && this.attributes["type"].value === "checkbox" && e.type === "click") {
this.checked = !this.checked
}
}
}
e.preventDefault = function() {
// TODO: should this do something?
}
if (typeof this["on" + e.type] === "function" && !isModernEvent(e.type)) this["on" + e.type](e)
},
onclick: null,
_events: events
}
if (element.nodeName === "A") {
Object.defineProperty(element, "href", {
get: function() {return this.attributes["href"] === undefined ? "" : "[FIXME implement]"},
set: function(value) {this.setAttribute("href", value)},
get: function() {
if (this.namespaceURI === "http://www.w3.org/2000/svg") {
var val = this.hasAttribute("href") ? this.attributes.href.value : ""
return {baseVal: val, animVal: val}
} else return this.attributes["href"] === undefined ? "" : "[FIXME implement]"
},
set: function(value) {
// This is a readonly attribute for SVG, todo investigate MathML which may have yet another IDL
if (this.namespaceURI !== "http://www.w3.org/2000/svg") this.setAttribute("href", value)
},
enumerable: true,
})
}
@ -443,7 +574,7 @@ module.exports = function(options) {
if (element.nodeName === "OPTION") {
var valueSetter = spy(function(value) {
/*eslint-disable no-implicit-coercion*/
this.setAttribute("value", value === null ? "" : "" + value)
this.setAttribute("value", "" + value)
/*eslint-enable no-implicit-coercion*/
})
Object.defineProperty(element, "value", {
@ -513,7 +644,8 @@ module.exports = function(options) {
},
createEvent: function() {
return {
initEvent: function(type) {this.type = type},
eventPhase: 0,
initEvent: function(type) {this.type = type}
}
},
get activeElement() {return activeElement},

View file

@ -7,21 +7,20 @@
<script src="../../module/module.js"></script>
<script src="../../ospec/ospec.js"></script>
<script src="../../querystring/parse.js"></script>
<script src="../../test-utils/callAsync.js"></script>
<script src="../../test-utils/parseURL.js"></script>
<script src="../../test-utils/pushStateMock.js"></script>
<script src="../../test-utils/xhrMock.js"></script>
<script src="../../test-utils/domMock.js"></script>
<script src="../../test-utils/browserMock.js"></script>
<script src="../../test-utils/component.js"></script>
<script src="../../test-utils/components.js"></script>
<script src="test-callAsync.js"></script>
<script src="test-parseURL.js"></script>
<script src="test-pushStateMock.js"></script>
<script src="test-xhrMock.js"></script>
<script src="test-domMock.js"></script>
<script src="test-browserMock.js"></script>
<script src="test-component.js"></script>
<script src="test-components.js"></script>
<script>require("../../ospec/ospec").run()</script>
</body>

View file

@ -4,9 +4,10 @@ var o = require("../../ospec/ospec")
var domMock = require("../../test-utils/domMock")
o.spec("domMock", function() {
var $document
var $document, $window
o.beforeEach(function() {
$document = domMock().document
$window = domMock()
$document = $window.document
})
o.spec("createElement", function() {
@ -241,6 +242,28 @@ o.spec("domMock", function() {
o(a.parentNode).equals(parent)
o(b.parentNode).equals(parent)
})
o("moves existing node forward but not at the end", function() {
var parent = $document.createElement("div")
var a = $document.createElement("a")
var b = $document.createElement("b")
var c = $document.createElement("c")
parent.appendChild(a)
parent.appendChild(b)
parent.appendChild(c)
parent.insertBefore(a, c)
o(parent.childNodes.length).equals(3)
o(parent.childNodes[0]).equals(b)
o(parent.childNodes[1]).equals(a)
o(parent.childNodes[2]).equals(c)
o(parent.firstChild).equals(b)
o(parent.firstChild.nextSibling).equals(a)
o(parent.firstChild.nextSibling.nextSibling).equals(c)
o(a.parentNode).equals(parent)
o(b.parentNode).equals(parent)
o(c.parentNode).equals(parent)
})
o("removes from old parent", function() {
var parent = $document.createElement("div")
var source = $document.createElement("span")
@ -341,6 +364,12 @@ o.spec("domMock", function() {
o(div.getAttribute("id")).equals("aaa")
})
o("works for attributes with a namespace", function() {
var div = $document.createElement("div")
div.setAttributeNS("http://www.w3.org/1999/xlink", "href", "aaa")
o(div.getAttribute("href")).equals("aaa")
})
})
o.spec("setAttribute", function() {
@ -406,18 +435,40 @@ o.spec("domMock", function() {
o.spec("setAttributeNS", function() {
o("works", function() {
var div = $document.createElement("div")
div.setAttributeNS("http://www.w3.org/1999/xlink", "href", "aaa")
var a = $document.createElementNS("http://www.w3.org/2000/svg", "a")
a.setAttributeNS("http://www.w3.org/1999/xlink", "href", "/aaa")
o(div.attributes["href"].value).equals("aaa")
o(div.attributes["href"].namespaceURI).equals("http://www.w3.org/1999/xlink")
o(a.href).deepEquals({baseVal: "/aaa", animVal: "/aaa"})
o(a.attributes["href"].value).equals("/aaa")
o(a.attributes["href"].namespaceURI).equals("http://www.w3.org/1999/xlink")
})
o("works w/ number", function() {
var div = $document.createElement("div")
div.setAttributeNS("http://www.w3.org/1999/xlink", "href", 123)
var a = $document.createElementNS("http://www.w3.org/2000/svg", "a")
a.setAttributeNS("http://www.w3.org/1999/xlink", "href", 123)
o(div.attributes["href"].value).equals("123")
o(div.attributes["href"].namespaceURI).equals("http://www.w3.org/1999/xlink")
o(a.href).deepEquals({baseVal: "123", animVal: "123"})
o(a.attributes["href"].value).equals("123")
o(a.attributes["href"].namespaceURI).equals("http://www.w3.org/1999/xlink")
})
o("attributes with a namespace can be querried, updated and removed with non-NS functions", function() {
var a = $document.createElementNS("http://www.w3.org/2000/svg", "a")
a.setAttributeNS("http://www.w3.org/1999/xlink", "href", "/aaa")
o(a.hasAttribute("href")).equals(true)
o(a.getAttribute("href")).equals("/aaa")
a.setAttribute("href", "/bbb")
o(a.href).deepEquals({baseVal: "/bbb", animVal: "/bbb"})
o(a.getAttribute("href")).equals("/bbb")
o(a.attributes["href"].value).equals("/bbb")
o(a.attributes["href"].namespaceURI).equals("http://www.w3.org/1999/xlink")
a.removeAttribute("href")
o(a.hasAttribute("href")).equals(false)
o(a.getAttribute("href")).equals(null)
o("href" in a.attributes).equals(false)
})
})
@ -497,6 +548,45 @@ o.spec("domMock", function() {
o(a.parentNode).equals(null)
})
o("empty SVG document", function() {
var div = $document.createElement("div")
div.innerHTML = "<svg xmlns=\"http://www.w3.org/2000/svg\"></svg>"
o(typeof div.firstChild).notEquals(undefined)
o(div.firstChild.nodeName).equals("svg")
o(div.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg")
o(div.firstChild.childNodes.length).equals(0)
})
o("text elements", function() {
var div = $document.createElement("div")
div.innerHTML =
"<svg xmlns=\"http://www.w3.org/2000/svg\">"
+ "<text>hello</text>"
+ "<text> </text>"
+ "<text>world</text>"
+ "</svg>"
o(div.firstChild.nodeName).equals("svg")
o(div.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg")
var nodes = div.firstChild.childNodes
o(nodes.length).equals(3)
o(nodes[0].nodeName).equals("text")
o(nodes[0].namespaceURI).equals("http://www.w3.org/2000/svg")
o(nodes[0].childNodes.length).equals(1)
o(nodes[0].childNodes[0].nodeName).equals("#text")
o(nodes[0].childNodes[0].nodeValue).equals("hello")
o(nodes[1].nodeName).equals("text")
o(nodes[1].namespaceURI).equals("http://www.w3.org/2000/svg")
o(nodes[1].childNodes.length).equals(1)
o(nodes[1].childNodes[0].nodeName).equals("#text")
o(nodes[1].childNodes[0].nodeValue).equals(" ")
o(nodes[2].nodeName).equals("text")
o(nodes[2].namespaceURI).equals("http://www.w3.org/2000/svg")
o(nodes[2].childNodes.length).equals(1)
o(nodes[2].childNodes[0].nodeName).equals("#text")
o(nodes[2].childNodes[0].nodeValue).equals("world")
})
})
o.spec("focus", function() {
o("body is active by default", function() {
@ -528,6 +618,7 @@ o.spec("domMock", function() {
o(div.style.backgroundColor).equals("red")
o(div.style.borderBottom).equals("1px solid red")
o(div.attributes.style.value).equals("background-color: red; border-bottom: 1px solid red;")
})
o("removing via setting style.cssText string works", function() {
var div = $document.createElement("div")
@ -535,6 +626,7 @@ o.spec("domMock", function() {
div.style.cssText = ""
o(div.style.background).equals("")
o(div.attributes.style.value).equals("")
})
o("the final semicolon is optional when setting style.cssText", function() {
var div = $document.createElement("div")
@ -542,6 +634,7 @@ o.spec("domMock", function() {
o(div.style.background).equals("red")
o(div.style.cssText).equals("background: red;")
o(div.attributes.style.value).equals("background: red;")
})
o("'cssText' as a property name is ignored when setting style.cssText", function(){
var div = $document.createElement("div")
@ -611,13 +704,57 @@ o.spec("domMock", function() {
o(spy.args[0].type).equals("click")
o(spy.args[0].target).equals(div)
})
o("removeEventListener works", function(done) {
o("removeEventListener works (bubbling phase)", function() {
div.addEventListener("click", spy, false)
div.removeEventListener("click", spy, false)
div.dispatchEvent(e)
o(spy.callCount).equals(0)
done()
})
o("removeEventListener works (capture phase)", function() {
div.addEventListener("click", spy, true)
div.removeEventListener("click", spy, true)
div.dispatchEvent(e)
o(spy.callCount).equals(0)
})
o("removeEventListener is selective (bubbling phase)", function() {
var other = o.spy()
div.addEventListener("click", spy, false)
div.addEventListener("click", other, false)
div.removeEventListener("click", spy, false)
div.dispatchEvent(e)
o(spy.callCount).equals(0)
o(other.callCount).equals(1)
})
o("removeEventListener is selective (capture phase)", function() {
var other = o.spy()
div.addEventListener("click", spy, true)
div.addEventListener("click", other, true)
div.removeEventListener("click", spy, true)
div.dispatchEvent(e)
o(spy.callCount).equals(0)
o(other.callCount).equals(1)
})
o("removeEventListener only removes the handler related to a given phase (1/2)", function() {
spy = o.spy(function(e) {o(e.eventPhase).equals(3)})
$document.body.addEventListener("click", spy, true)
$document.body.addEventListener("click", spy, false)
$document.body.removeEventListener("click", spy, true)
div.dispatchEvent(e)
o(spy.callCount).equals(1)
})
o("removeEventListener only removes the handler related to a given phase (2/2)", function() {
spy = o.spy(function(e) {o(e.eventPhase).equals(1)})
$document.body.addEventListener("click", spy, true)
$document.body.addEventListener("click", spy, false)
$document.body.removeEventListener("click", spy, false)
div.dispatchEvent(e)
o(spy.callCount).equals(1)
})
o("click fires onclick", function() {
div.onclick = spy
@ -655,6 +792,488 @@ o.spec("domMock", function() {
done()
})
})
o.spec("capture and bubbling phases", function() {
var div, e
o.beforeEach(function() {
div = $document.createElement("div")
e = $document.createEvent("MouseEvents")
e.initEvent("click", true, true)
$document.body.appendChild(div)
})
o.afterEach(function() {
$document.body.removeChild(div)
})
o("capture and bubbling events both fire on the target in the order they were defined, regardless of the phase", function () {
var sequence = []
var capture = o.spy(function(ev){
sequence.push("capture")
o(ev).equals(e)
o(ev.eventPhase).equals(2)
o(ev.target).equals(div)
o(ev.currentTarget).equals(div)
})
var bubble = o.spy(function(ev){
sequence.push("bubble")
o(ev).equals(e)
o(ev.eventPhase).equals(2)
o(ev.target).equals(div)
o(ev.currentTarget).equals(div)
})
div.addEventListener("click", bubble, false)
div.addEventListener("click", capture, true)
div.dispatchEvent(e)
o(capture.callCount).equals(1)
o(bubble.callCount).equals(1)
o(sequence).deepEquals(["bubble", "capture"])
})
o("capture and bubbling events both fire on the parent", function () {
var sequence = []
var capture = o.spy(function(ev){
sequence.push("capture")
o(ev).equals(e)
o(ev.eventPhase).equals(1)
o(ev.target).equals(div)
o(ev.currentTarget).equals($document.body)
})
var bubble = o.spy(function(ev){
sequence.push("bubble")
o(ev).equals(e)
o(ev.eventPhase).equals(3)
o(ev.target).equals(div)
o(ev.currentTarget).equals($document.body)
})
$document.body.addEventListener("click", bubble, false)
$document.body.addEventListener("click", capture, true)
div.dispatchEvent(e)
o(capture.callCount).equals(1)
o(bubble.callCount).equals(1)
o(sequence).deepEquals(["capture", "bubble"])
})
o("useCapture defaults to false", function () {
var sequence = []
var parent = o.spy(function(ev){
sequence.push("parent")
o(ev).equals(e)
o(ev.eventPhase).equals(3)
o(ev.target).equals(div)
o(ev.currentTarget).equals($document.body)
})
var target = o.spy(function(ev){
sequence.push("target")
o(ev).equals(e)
o(ev.eventPhase).equals(2)
o(ev.target).equals(div)
o(ev.currentTarget).equals(div)
})
$document.body.addEventListener("click", parent)
div.addEventListener("click", target)
div.dispatchEvent(e)
o(parent.callCount).equals(1)
o(target.callCount).equals(1)
o(sequence).deepEquals(["target", "parent"])
})
o("legacy handlers fire on the bubbling phase", function () {
var sequence = []
var parent = o.spy(function(ev){
sequence.push("parent")
o(ev).equals(e)
o(ev.eventPhase).equals(3)
o(ev.target).equals(div)
o(ev.currentTarget).equals($document.body)
})
var target = o.spy(function(ev){
sequence.push("target")
o(ev).equals(e)
o(ev.eventPhase).equals(2)
o(ev.target).equals(div)
o(ev.currentTarget).equals(div)
})
$document.body.addEventListener("click", parent)
$document.body.onclick = parent
div.addEventListener("click", target)
div.dispatchEvent(e)
o(parent.callCount).equals(2)
o(target.callCount).equals(1)
o(sequence).deepEquals(["target", "parent", "parent"])
})
o("events do not propagate to child nodes", function() {
var target = o.spy(function(ev){
o(ev).equals(e)
o(ev.eventPhase).equals(2)
o(ev.target).equals($document.body)
o(ev.currentTarget).equals($document.body)
})
var child = o.spy(function(){
})
$document.body.addEventListener("click", target)
div.addEventListener("click", child)
$document.body.dispatchEvent(e)
o(target.callCount).equals(1)
o(child.callCount).equals(0)
})
o("e.stopPropagation 1/6", function () {
var capParent = o.spy(function(e){e.stopPropagation()})
var capTarget = o.spy()
var bubTarget = o.spy()
var legacyTarget = o.spy()
var bubParent = o.spy()
var legacyParent = o.spy()
$document.body.addEventListener("click", capParent, true)
$document.body.addEventListener("click", bubParent, false)
$document.body.onclick = legacyParent
div.addEventListener("click", capTarget, true)
div.addEventListener("click", bubTarget, false)
div.onclick = legacyTarget
div.dispatchEvent(e)
o(capParent.callCount).equals(1)
o(capTarget.callCount).equals(0)
o(bubTarget.callCount).equals(0)
o(legacyTarget.callCount).equals(0)
o(bubParent.callCount).equals(0)
o(legacyParent.callCount).equals(0)
})
o("e.stopPropagation 2/6", function () {
var capParent = o.spy()
var capTarget = o.spy(function(e){e.stopPropagation()})
var bubTarget = o.spy()
var legacyTarget = o.spy()
var bubParent = o.spy()
var legacyParent = o.spy()
$document.body.addEventListener("click", capParent, true)
$document.body.addEventListener("click", bubParent, false)
$document.body.onclick = legacyParent
div.addEventListener("click", capTarget, true)
div.addEventListener("click", bubTarget, false)
div.onclick = legacyTarget
div.dispatchEvent(e)
o(capParent.callCount).equals(1)
o(capTarget.callCount).equals(1)
o(bubTarget.callCount).equals(1)
o(legacyTarget.callCount).equals(1)
o(bubParent.callCount).equals(0)
o(legacyParent.callCount).equals(0)
})
o("e.stopPropagation 3/6", function () {
var capParent = o.spy()
var capTarget = o.spy()
var bubTarget = o.spy(function(e){e.stopPropagation()})
var legacyTarget = o.spy()
var bubParent = o.spy()
var legacyParent = o.spy()
$document.body.addEventListener("click", capParent, true)
$document.body.addEventListener("click", bubParent, false)
$document.body.onclick = legacyParent
div.addEventListener("click", capTarget, true)
div.addEventListener("click", bubTarget, false)
div.onclick = legacyTarget
div.dispatchEvent(e)
o(capParent.callCount).equals(1)
o(capTarget.callCount).equals(1)
o(bubTarget.callCount).equals(1)
o(legacyTarget.callCount).equals(1)
o(bubParent.callCount).equals(0)
o(legacyParent.callCount).equals(0)
})
o("e.stopPropagation 4/6", function () {
var capParent = o.spy()
var capTarget = o.spy()
var bubTarget = o.spy()
var legacyTarget = o.spy(function(e){e.stopPropagation()})
var bubParent = o.spy()
var legacyParent = o.spy()
$document.body.addEventListener("click", capParent, true)
$document.body.addEventListener("click", bubParent, false)
$document.body.onclick = legacyParent
div.addEventListener("click", capTarget, true)
div.addEventListener("click", bubTarget, false)
div.onclick = legacyTarget
div.dispatchEvent(e)
o(capParent.callCount).equals(1)
o(capTarget.callCount).equals(1)
o(bubTarget.callCount).equals(1)
o(legacyTarget.callCount).equals(1)
o(bubParent.callCount).equals(0)
o(legacyParent.callCount).equals(0)
})
o("e.stopPropagation 5/6", function () {
var capParent = o.spy()
var capTarget = o.spy()
var bubTarget = o.spy()
var legacyTarget = o.spy()
var bubParent = o.spy(function(e){e.stopPropagation()})
var legacyParent = o.spy()
$document.body.addEventListener("click", capParent, true)
$document.body.addEventListener("click", bubParent, false)
$document.body.onclick = legacyParent
div.addEventListener("click", capTarget, true)
div.addEventListener("click", bubTarget, false)
div.onclick = legacyTarget
div.dispatchEvent(e)
o(capParent.callCount).equals(1)
o(capTarget.callCount).equals(1)
o(bubTarget.callCount).equals(1)
o(legacyTarget.callCount).equals(1)
o(bubParent.callCount).equals(1)
o(legacyParent.callCount).equals(1)
})
o("e.stopPropagation 6/6", function () {
var capParent = o.spy()
var capTarget = o.spy()
var legacyTarget = o.spy()
var bubTarget = o.spy()
var bubParent = o.spy()
var legacyParent = o.spy(function(e){e.stopPropagation()})
$document.body.addEventListener("click", capParent, true)
$document.body.addEventListener("click", bubParent, false)
$document.body.onclick = legacyParent
div.addEventListener("click", capTarget, true)
div.addEventListener("click", bubTarget, false)
div.onclick = legacyTarget
div.dispatchEvent(e)
o(capParent.callCount).equals(1)
o(capTarget.callCount).equals(1)
o(bubTarget.callCount).equals(1)
o(legacyTarget.callCount).equals(1)
o(bubParent.callCount).equals(1)
o(legacyParent.callCount).equals(1)
})
o("e.stopImmediatePropagation 1/6", function () {
var capParent = o.spy(function(e){e.stopImmediatePropagation()})
var capTarget = o.spy()
var bubTarget = o.spy()
var legacyTarget = o.spy()
var bubParent = o.spy()
var legacyParent = o.spy()
$document.body.addEventListener("click", capParent, true)
$document.body.addEventListener("click", bubParent, false)
$document.body.onclick = legacyParent
div.addEventListener("click", capTarget, true)
div.addEventListener("click", bubTarget, false)
div.onclick = legacyTarget
div.dispatchEvent(e)
o(capParent.callCount).equals(1)
o(capTarget.callCount).equals(0)
o(bubTarget.callCount).equals(0)
o(legacyTarget.callCount).equals(0)
o(bubParent.callCount).equals(0)
o(legacyParent.callCount).equals(0)
})
o("e.stopImmediatePropagation 2/6", function () {
var capParent = o.spy()
var capTarget = o.spy(function(e){e.stopImmediatePropagation()})
var bubTarget = o.spy()
var legacyTarget = o.spy()
var bubParent = o.spy()
var legacyParent = o.spy()
$document.body.addEventListener("click", capParent, true)
$document.body.addEventListener("click", bubParent, false)
$document.body.onclick = legacyParent
div.addEventListener("click", capTarget, true)
div.addEventListener("click", bubTarget, false)
div.onclick = legacyTarget
div.dispatchEvent(e)
o(capParent.callCount).equals(1)
o(capTarget.callCount).equals(1)
o(bubTarget.callCount).equals(0)
o(legacyTarget.callCount).equals(0)
o(bubParent.callCount).equals(0)
o(legacyParent.callCount).equals(0)
})
o("e.stopImmediatePropagation 3/6", function () {
var capParent = o.spy()
var capTarget = o.spy()
var bubTarget = o.spy(function(e){e.stopImmediatePropagation()})
var legacyTarget = o.spy()
var bubParent = o.spy()
var legacyParent = o.spy()
$document.body.addEventListener("click", capParent, true)
$document.body.addEventListener("click", bubParent, false)
$document.body.onclick = legacyParent
div.addEventListener("click", capTarget, true)
div.addEventListener("click", bubTarget, false)
div.onclick = legacyTarget
div.dispatchEvent(e)
o(capParent.callCount).equals(1)
o(capTarget.callCount).equals(1)
o(bubTarget.callCount).equals(1)
o(legacyTarget.callCount).equals(0)
o(bubParent.callCount).equals(0)
o(legacyParent.callCount).equals(0)
})
o("e.stopImmediatePropagation 4/6", function () {
var capParent = o.spy()
var capTarget = o.spy()
var bubTarget = o.spy()
var legacyTarget = o.spy(function(e){e.stopImmediatePropagation()})
var bubParent = o.spy()
var legacyParent = o.spy()
$document.body.addEventListener("click", capParent, true)
$document.body.addEventListener("click", bubParent, false)
$document.body.onclick = legacyParent
div.addEventListener("click", capTarget, true)
div.addEventListener("click", bubTarget, false)
div.onclick = legacyTarget
div.dispatchEvent(e)
o(capParent.callCount).equals(1)
o(capTarget.callCount).equals(1)
o(bubTarget.callCount).equals(1)
o(legacyTarget.callCount).equals(1)
o(bubParent.callCount).equals(0)
o(legacyParent.callCount).equals(0)
})
o("e.stopImmediatePropagation 5/6", function () {
var capParent = o.spy()
var capTarget = o.spy()
var bubTarget = o.spy()
var legacyTarget = o.spy()
var bubParent = o.spy(function(e){e.stopImmediatePropagation()})
var legacyParent = o.spy()
$document.body.addEventListener("click", capParent, true)
$document.body.addEventListener("click", bubParent, false)
$document.body.onclick = legacyParent
div.addEventListener("click", capTarget, true)
div.addEventListener("click", bubTarget, false)
div.onclick = legacyTarget
div.dispatchEvent(e)
o(capParent.callCount).equals(1)
o(capTarget.callCount).equals(1)
o(bubTarget.callCount).equals(1)
o(legacyTarget.callCount).equals(1)
o(bubParent.callCount).equals(1)
o(legacyParent.callCount).equals(0)
})
o("e.stopImmediatePropagation 6/6", function () {
var capParent = o.spy()
var capTarget = o.spy()
var legacyTarget = o.spy()
var bubTarget = o.spy()
var bubParent = o.spy()
var legacyParent = o.spy(function(e){e.stopImmediatePropagation()})
$document.body.addEventListener("click", capParent, true)
$document.body.addEventListener("click", bubParent, false)
$document.body.onclick = legacyParent
div.addEventListener("click", capTarget, true)
div.addEventListener("click", bubTarget, false)
div.onclick = legacyTarget
div.dispatchEvent(e)
o(capParent.callCount).equals(1)
o(capTarget.callCount).equals(1)
o(bubTarget.callCount).equals(1)
o(legacyTarget.callCount).equals(1)
o(bubParent.callCount).equals(1)
o(legacyParent.callCount).equals(1)
})
o("errors thrown in handlers don't interrupt the chain", function(done) {
var errMsg = "The presence of these six errors in the log is expected in non-Node.js environments"
var handler = o.spy(function(){throw errMsg})
$document.body.addEventListener("click", handler, true)
$document.body.addEventListener("click", handler, false)
$document.body.onclick = handler
div.addEventListener("click", handler, true)
div.addEventListener("click", handler, false)
div.onclick = handler
div.dispatchEvent(e)
o(handler.callCount).equals(6)
// Swallow the async errors in NodeJS
if (typeof process !== "undefined" && typeof process.once === "function"){
process.once("uncaughtException", function(e) {
if (e !== errMsg) throw e
process.once("uncaughtException", function(e) {
if (e !== errMsg) throw e
process.once("uncaughtException", function(e) {
if (e !== errMsg) throw e
process.once("uncaughtException", function(e) {
if (e !== errMsg) throw e
process.once("uncaughtException", function(e) {
if (e !== errMsg) throw e
process.once("uncaughtException", function(e) {
if (e !== errMsg) throw e
done()
})
})
})
})
})
})
} else {
done()
}
})
})
})
o.spec("attributes", function() {
o.spec("a[href]", function() {
@ -678,6 +1297,13 @@ o.spec("domMock", function() {
o(a.href).notEquals("")
o(a.attributes["href"].value).equals("")
})
o("property is read-only for SVG elements", function() {
var a = $document.createElementNS("http://www.w3.org/2000/svg", "a")
a.href = "/foo"
o(a.href).deepEquals({baseVal: "", animVal: ""})
o("href" in a.attributes).equals(false)
})
})
o.spec("input[checked]", function() {
o("only exists in input elements", function() {
@ -731,6 +1357,18 @@ o.spec("domMock", function() {
o(input.checked).equals(true)
})
o("doesn't toggle on click when preventDefault() is used", function() {
var input = $document.createElement("input")
input.setAttribute("type", "checkbox")
input.checked = false
input.onclick = function(e) {e.preventDefault()}
var e = $document.createEvent("MouseEvents")
e.initEvent("click", true, true)
input.dispatchEvent(e)
o(input.checked).equals(false)
})
})
o.spec("input[value]", function() {
o("only exists in input elements", function() {
@ -954,11 +1592,11 @@ o.spec("domMock", function() {
o(select.selectedIndex).equals(1)
}
})
o("option.value = null is converted to the empty string", function() {
o("option.value = null is converted to 'null'", function() {
var option = $document.createElement("option")
option.value = null
o(option.value).equals("")
o(option.value).equals("null")
})
o("setting valid value works with optgroup", function() {
var select = $document.createElement("select")
@ -1254,4 +1892,62 @@ o.spec("domMock", function() {
o(spies.valueSetter.args[0]).equals("aaa")
})
})
o.spec("DOMParser for SVG", function(){
var $DOMParser
o.beforeEach(function() {
$DOMParser = $window.DOMParser
})
o("basics", function(){
o(typeof $DOMParser).equals("function")
var parser = new $DOMParser()
o(parser instanceof $DOMParser).equals(true)
o(typeof parser.parseFromString).equals("function")
})
o("empty document", function() {
var parser = new $DOMParser()
var doc = parser.parseFromString(
"<svg xmlns=\"http://www.w3.org/2000/svg\"></svg>",
"image/svg+xml"
)
o(typeof doc.documentElement).notEquals(undefined)
o(doc.documentElement.nodeName).equals("svg")
o(doc.documentElement.namespaceURI).equals("http://www.w3.org/2000/svg")
o(doc.documentElement.childNodes.length).equals(0)
})
o("text elements", function() {
var parser = new $DOMParser()
var doc = parser.parseFromString(
"<svg xmlns=\"http://www.w3.org/2000/svg\">"
+ "<text>hello</text>"
+ "<text> </text>"
+ "<text>world</text>"
+ "</svg>",
"image/svg+xml"
)
o(doc.documentElement.nodeName).equals("svg")
o(doc.documentElement.namespaceURI).equals("http://www.w3.org/2000/svg")
var nodes = doc.documentElement.childNodes
o(nodes.length).equals(3)
o(nodes[0].nodeName).equals("text")
o(nodes[0].namespaceURI).equals("http://www.w3.org/2000/svg")
o(nodes[0].childNodes.length).equals(1)
o(nodes[0].childNodes[0].nodeName).equals("#text")
o(nodes[0].childNodes[0].nodeValue).equals("hello")
o(nodes[1].nodeName).equals("text")
o(nodes[1].namespaceURI).equals("http://www.w3.org/2000/svg")
o(nodes[1].childNodes.length).equals(1)
o(nodes[1].childNodes[0].nodeName).equals("#text")
o(nodes[1].childNodes[0].nodeValue).equals(" ")
o(nodes[2].nodeName).equals("text")
o(nodes[2].namespaceURI).equals("http://www.w3.org/2000/svg")
o(nodes[2].childNodes.length).equals(1)
o(nodes[2].childNodes[0].nodeName).equals("#text")
o(nodes[2].childNodes[0].nodeValue).equals("world")
})
})
})

View file

@ -0,0 +1,91 @@
"use strict"
var o = require("../../ospec/ospec")
var throttleMocker = require("../../test-utils/throttleMock")
o.spec("throttleMock", function() {
o("works with one callback", function() {
var throttleMock = throttleMocker()
var spy = o.spy()
o(throttleMock.queueLength()).equals(0)
var throttled = throttleMock.throttle(spy)
o(throttleMock.queueLength()).equals(0)
o(spy.callCount).equals(0)
throttled()
o(throttleMock.queueLength()).equals(1)
o(spy.callCount).equals(0)
throttled()
o(throttleMock.queueLength()).equals(1)
o(spy.callCount).equals(0)
throttleMock.fire()
o(throttleMock.queueLength()).equals(0)
o(spy.callCount).equals(1)
throttleMock.fire()
o(spy.callCount).equals(1)
})
o("works with two callbacks", function() {
var throttleMock = throttleMocker()
var spy1 = o.spy()
var spy2 = o.spy()
o(throttleMock.queueLength()).equals(0)
var throttled1 = throttleMock.throttle(spy1)
o(throttleMock.queueLength()).equals(0)
o(spy1.callCount).equals(0)
o(spy2.callCount).equals(0)
throttled1()
o(throttleMock.queueLength()).equals(1)
o(spy1.callCount).equals(0)
o(spy2.callCount).equals(0)
throttled1()
o(throttleMock.queueLength()).equals(1)
o(spy1.callCount).equals(0)
o(spy2.callCount).equals(0)
var throttled2 = throttleMock.throttle(spy2)
o(throttleMock.queueLength()).equals(1)
o(spy1.callCount).equals(0)
o(spy2.callCount).equals(0)
throttled2()
o(throttleMock.queueLength()).equals(2)
o(spy1.callCount).equals(0)
o(spy2.callCount).equals(0)
throttled2()
o(throttleMock.queueLength()).equals(2)
o(spy1.callCount).equals(0)
o(spy2.callCount).equals(0)
throttleMock.fire()
o(throttleMock.queueLength()).equals(0)
o(spy1.callCount).equals(1)
o(spy2.callCount).equals(1)
throttleMock.fire()
o(spy1.callCount).equals(1)
o(spy2.callCount).equals(1)
})
})

View file

@ -0,0 +1,27 @@
"use strict"
module.exports = function() {
var queue = []
return {
throttle: function(fn) {
var pending = false
return function() {
if (!pending) {
queue.push(function(){
pending = false
fn()
})
pending = true
}
}
},
fire: function() {
var tasks = queue
queue = []
tasks.forEach(function(fn) {fn()})
},
queueLength: function(){
return queue.length
}
}
}

View file

@ -9,6 +9,7 @@
<script src="../querystring/parse.js"></script>
<script src="../test-utils/parseURL.js"></script>
<script src="../test-utils/callAsync.js"></script>
<script src="../test-utils/components.js"></script>
<script src="../test-utils/domMock.js"></script>
<script src="../test-utils/pushStateMock.js"></script>
<script src="../test-utils/xhrMock.js"></script>

View file

@ -163,14 +163,24 @@ o.spec("api", function() {
var count = 0
var root = window.document.createElement("div")
m.mount(root, createComponent({view: function() {count++}}))
o(count).equals(1)
m.redraw()
o(count).equals(1)
setTimeout(function() {
m.redraw()
o(count).equals(2)
done()
}, FRAME_BUDGET)
})
o("sync", function() {
var root = window.document.createElement("div")
var view = o.spy()
m.mount(root, createComponent({view: view}))
o(view.callCount).equals(1)
m.redraw.sync()
o(view.callCount).equals(2)
})
})
})
})