diff --git a/.eslintignore b/.eslintignore index 4b17d86d2..645d14912 100644 --- a/.eslintignore +++ b/.eslintignore @@ -7,6 +7,4 @@ /.eslintcache # These are artifacts from various scripts -/mithril.js -/mithril.min.js -/stream/stream.min.js +/dist diff --git a/.eslintrc.js b/.eslintrc.json similarity index 52% rename from .eslintrc.js rename to .eslintrc.json index e0734315d..ceac4bc19 100644 --- a/.eslintrc.js +++ b/.eslintrc.json @@ -1,14 +1,102 @@ -"use strict" +{ + "overrides": [ + {"files": "*.cjs", "parserOptions": {"sourceType": "script"}}, + { + "files": ["scripts/**"], + "env": { + "node": true, + "es2022": true + }, + "parserOptions": { + "ecmaVersion": 2022 + }, + "rules": { + "no-process-env": "off", + "no-restricted-syntax": "off" + } + }, + { + "files": ["performance/**"], + "env": { + "browser": true + }, + "rules": { + "no-restricted-syntax": ["error", + {"selector": "Literal[bigint]", "message": "BigInts are not supported in ES2018"}, + {"selector": "ChainExpression", "message": "Optional chaining is not supported in ES2018"}, + {"selector": "BinaryExpression[operator='??']", "message": "Nullish coalescing is not supported in ES2018"}, + {"selector": "MetaProperty[meta.name='import'][property.name='meta']", "message": "`import.meta` is not supported in ES2018"}, + {"selector": "ExportAllDeclaration[exported!=null]", "message": "`export * as foo from ...` is not supported in ES2018"}, + {"selector": "CatchClause[param=null]", "message": "Omitted `catch` bindings are not supported in ES2018"}, + {"selector": "Literal[regex.flags=/s/]", "message": "`/.../s` is not supported in ES2018"}, + {"selector": "Literal[regex.flags=/v/]", "message": "`/.../v` is not supported in ES2018"}, + + {"selector": "MemberExpression[property.name='matchAll']", "message": "`string.matchAll` is not supported in ES2018"}, + {"selector": "MemberExpression[property.name='trimStart']", "message": "`string.trimStart` is not supported in ES2018"} + ] + } + }, + { + "files": ["tests/**", "test-utils/**"], + "env": { + "node": true + }, + "rules": { + "no-process-env": "off", + "no-restricted-syntax": ["error", + {"selector": "Literal[bigint]", "message": "BigInts are not supported in ES2018"}, + {"selector": "ChainExpression", "message": "Optional chaining is not supported in ES2018"}, + {"selector": "BinaryExpression[operator='??']", "message": "Nullish coalescing is not supported in ES2018"}, + {"selector": "MetaProperty[meta.name='import'][property.name='meta']", "message": "`import.meta` is not supported in ES2018"}, + {"selector": "ExportAllDeclaration[exported!=null]", "message": "`export * as foo from ...` is not supported in ES2018"}, + {"selector": "CatchClause[param=null]", "message": "Omitted `catch` bindings are not supported in ES2018"}, + {"selector": "Literal[regex.flags=/s/]", "message": "`/.../s` is not supported in ES2018"}, + {"selector": "Literal[regex.flags=/v/]", "message": "`/.../v` is not supported in ES2018"}, -module.exports = { + {"selector": "MemberExpression[property.name='matchAll']", "message": "`string.matchAll` is not supported in ES2018"}, + {"selector": "MemberExpression[property.name='trimStart']", "message": "`string.trimStart` is not supported in ES2018"} + ] + } + } + ], "env": { - "browser": true, - "commonjs": true, - "es6": true, - "node": true + "es2018": true + }, + "globals": { + "ReadableStream": true, + "Response": true, + "queueMicrotask": true, + "URL": true, + "URLSearchParams": true, + "AbortController": true, + "console": true + }, + "parserOptions": { + "ecmaVersion": 2018, + "sourceType": "module" }, "extends": "eslint:recommended", "rules": { + "no-restricted-syntax": ["error", + {"selector": "Literal[bigint]", "message": "BigInts are not supported in ES2018"}, + {"selector": "ChainExpression", "message": "Optional chaining is not supported in ES2018"}, + {"selector": "BinaryExpression[operator='??']", "message": "Nullish coalescing is not supported in ES2018"}, + {"selector": "MetaProperty[meta.name='import'][property.name='meta']", "message": "`import.meta` is not supported in ES2018"}, + {"selector": "ExportAllDeclaration[exported!=null]", "message": "`export * as foo from ...` is not supported in ES2018"}, + {"selector": "CatchClause[param=null]", "message": "Omitted `catch` bindings are not supported in ES2018"}, + {"selector": "Function[async=true][generator=true]", "message": "Async generators are not supported in ES2018"}, + {"selector": "Literal[regex.flags=/v/]", "message": "`/.../v` is not supported in ES2018"}, + + {"selector": "MemberExpression[property.name='matchAll']", "message": "`string.matchAll` is not supported in ES2018"}, + {"selector": "MemberExpression[property.name='trimStart']", "message": "`string.trimStart` is not supported in ES2018"}, + + {"selector": "VariableDeclaration[kind!='var']", "message": "Keep to `var` in `src/` to ensure the module compresses better"} + ], + "no-restricted-properties": ["error", + {"object": "Promise", "property": "allSettled", "message": "`Promise.allSettled` is not supported in ES2018"}, + {"object": "Object", "property": "fromEntries", "message": "`Object.fromEntries` is not supported in ES2018"} + ], + "accessor-pairs": "error", "array-bracket-spacing": [ "error", @@ -141,7 +229,6 @@ module.exports = { "no-restricted-globals": "error", "no-restricted-imports": "error", "no-restricted-modules": "error", - "no-restricted-syntax": "error", "no-return-assign": "off", "no-script-url": "error", "no-self-compare": "error", @@ -207,7 +294,7 @@ module.exports = { "require-yield": "error", "semi": "off", "semi-spacing": "off", - "sort-imports": "error", + "sort-imports": ["error", {"allowSeparatedGroups": true}], "sort-vars": "off", "space-before-blocks": "off", "space-before-function-paren": "off", @@ -228,4 +315,4 @@ module.exports = { "yoda": "off" }, "root": true -}; +} diff --git a/.gitattributes b/.gitattributes index af779b11a..3f9b28063 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,4 @@ * text=auto -/mithril.js binary -/mithril.min.js binary # Assets *.png binary diff --git a/.gitignore b/.gitignore index 34900e13c..e3d2fc37d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,10 @@ /.vscode /.DS_Store /.eslintcache + +# Exclude the compiled output. It's available through prerelease publishes anyways, so there's no +# need to keep a spare copy in the repo. +/dist + +# For an easier time working with console logs from profiles. +/*.log diff --git a/.npmignore b/.npmignore deleted file mode 100644 index e6896f392..000000000 --- a/.npmignore +++ /dev/null @@ -1,22 +0,0 @@ -# Development-specific files -/.deploy.env -/.editorconfig -/.eslintrc.js -/.eslintcache -/.eslintignore -/.gitattributes -/.gitignore -/.travis.yml -/yarn.lock -/scripts/ - -# Exclude all directories named "tests" as it's used only for tests. This is -# intentionally not prefixed with a `/` because it applies to both the root and -# subdirectories. -tests/ - -# Mithril.js' mocks are for internal use only, and it's wholly undocumented for a -# reason. I've already gotten way too many complaints over users' tests breaking -# from changes to it in patch releases. Let's force people to finally stop using -# them. -/test-utils/ diff --git a/LICENSE b/LICENSE index 2aae0f1e7..01eab01bc 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2017 Leo Horie +Copyright (c) 2024 Mithril Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index f4bc9bee8..194245803 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,11 @@ ## What is Mithril.js? -A modern client-side JavaScript framework for building Single Page Applications. It's small (9.05 KB gzipped), fast and provides routing and XHR utilities out of the box. +A modern client-side JavaScript framework for web applications big and small. It's small, fast, and highly stable. And it offers utilities for routing, state management, and more right out of the box. -Mithril.js is used by companies like Vimeo and Nike, and open source platforms like Lichess πŸ‘. +Mithril.js has been used by companies like Vimeo, Nike, and Amazon, and open source platforms like Lichess. πŸ‘ -Mithril.js supports IE11, Firefox ESR, and the last two versions of Firefox, Edge, Safari, and Chrome. No polyfills required. πŸ‘Œ +Mithril.js supports Firefox ESR and the last two versions of Firefox, Edge, Safari, and Chrome. No polyfills required. πŸ‘Œ ## Installation diff --git a/api/mount-redraw.js b/api/mount-redraw.js deleted file mode 100644 index 4f7d0f606..000000000 --- a/api/mount-redraw.js +++ /dev/null @@ -1,49 +0,0 @@ -"use strict" - -var Vnode = require("../render/vnode") - -module.exports = function(render, schedule, console) { - var subscriptions = [] - var pending = false - var offset = -1 - - function sync() { - for (offset = 0; offset < subscriptions.length; offset += 2) { - try { render(subscriptions[offset], Vnode(subscriptions[offset + 1]), redraw) } - catch (e) { console.error(e) } - } - offset = -1 - } - - function redraw() { - if (!pending) { - pending = true - schedule(function() { - pending = false - sync() - }) - } - } - - redraw.sync = sync - - function mount(root, component) { - if (component != null && component.view == null && typeof component !== "function") { - throw new TypeError("m.mount expects a component, not a vnode.") - } - - var index = subscriptions.indexOf(root) - if (index >= 0) { - subscriptions.splice(index, 2) - if (index <= offset) offset -= 2 - render(root, []) - } - - if (component != null) { - subscriptions.push(root, component) - render(root, Vnode(component), redraw) - } - } - - return {mount: mount, redraw: redraw} -} diff --git a/api/router.js b/api/router.js deleted file mode 100644 index fdf4ed8a3..000000000 --- a/api/router.js +++ /dev/null @@ -1,277 +0,0 @@ -"use strict" - -var Vnode = require("../render/vnode") -var m = require("../render/hyperscript") - -var buildPathname = require("../pathname/build") -var parsePathname = require("../pathname/parse") -var compileTemplate = require("../pathname/compileTemplate") -var censor = require("../util/censor") - -var sentinel = {} - -function decodeURIComponentSave(component) { - try { - return decodeURIComponent(component) - } catch(e) { - return component - } -} - -module.exports = function($window, mountRedraw) { - var callAsync = $window == null - // In case Mithril.js' loaded globally without the DOM, let's not break - ? null - : typeof $window.setImmediate === "function" ? $window.setImmediate : $window.setTimeout - var p = Promise.resolve() - - var scheduled = false - - // state === 0: init - // state === 1: scheduled - // state === 2: done - var ready = false - var state = 0 - - var compiled, fallbackRoute - - var currentResolver = sentinel, component, attrs, currentPath, lastUpdate - - var RouterRoot = { - onbeforeupdate: function() { - state = state ? 2 : 1 - return !(!state || sentinel === currentResolver) - }, - onremove: function() { - $window.removeEventListener("popstate", fireAsync, false) - $window.removeEventListener("hashchange", resolveRoute, false) - }, - view: function() { - if (!state || sentinel === currentResolver) return - // Wrap in a fragment to preserve existing key semantics - var vnode = [Vnode(component, attrs.key, attrs)] - if (currentResolver) vnode = currentResolver.render(vnode[0]) - return vnode - }, - } - - var SKIP = route.SKIP = {} - - function resolveRoute() { - scheduled = false - // Consider the pathname holistically. The prefix might even be invalid, - // but that's not our problem. - var prefix = $window.location.hash - if (route.prefix[0] !== "#") { - prefix = $window.location.search + prefix - if (route.prefix[0] !== "?") { - prefix = $window.location.pathname + prefix - if (prefix[0] !== "/") prefix = "/" + prefix - } - } - // This seemingly useless `.concat()` speeds up the tests quite a bit, - // since the representation is consistently a relatively poorly - // optimized cons string. - var path = prefix.concat() - .replace(/(?:%[a-f89][a-f0-9])+/gim, decodeURIComponentSave) - .slice(route.prefix.length) - var data = parsePathname(path) - - Object.assign(data.params, $window.history.state) - - function reject(e) { - console.error(e) - setPath(fallbackRoute, null, {replace: true}) - } - - loop(0) - function loop(i) { - // state === 0: init - // state === 1: scheduled - // state === 2: done - for (; i < compiled.length; i++) { - if (compiled[i].check(data)) { - var payload = compiled[i].component - var matchedRoute = compiled[i].route - var localComp = payload - var update = lastUpdate = function(comp) { - if (update !== lastUpdate) return - if (comp === SKIP) return loop(i + 1) - component = comp != null && (typeof comp.view === "function" || typeof comp === "function")? comp : "div" - attrs = data.params, currentPath = path, lastUpdate = null - currentResolver = payload.render ? payload : null - if (state === 2) mountRedraw.redraw() - else { - state = 2 - mountRedraw.redraw.sync() - } - } - // There's no understating how much I *wish* I could - // use `async`/`await` here... - if (payload.view || typeof payload === "function") { - payload = {} - update(localComp) - } - else if (payload.onmatch) { - p.then(function () { - return payload.onmatch(data.params, path, matchedRoute) - }).then(update, path === fallbackRoute ? null : reject) - } - else update("div") - return - } - } - - if (path === fallbackRoute) { - throw new Error("Could not resolve default route " + fallbackRoute + ".") - } - setPath(fallbackRoute, null, {replace: true}) - } - } - - // Set it unconditionally so `m.route.set` and `m.route.Link` both work, - // even if neither `pushState` nor `hashchange` are supported. It's - // cleared if `hashchange` is used, since that makes it automatically - // async. - function fireAsync() { - if (!scheduled) { - scheduled = true - // TODO: just do `mountRedraw.redraw()` here and elide the timer - // dependency. Note that this will muck with tests a *lot*, so it's - // not as easy of a change as it sounds. - callAsync(resolveRoute) - } - } - - function setPath(path, data, options) { - path = buildPathname(path, data) - if (ready) { - fireAsync() - var state = options ? options.state : null - var title = options ? options.title : null - if (options && options.replace) $window.history.replaceState(state, title, route.prefix + path) - else $window.history.pushState(state, title, route.prefix + path) - } - else { - $window.location.href = route.prefix + path - } - } - - function route(root, defaultRoute, routes) { - if (!root) throw new TypeError("DOM element being rendered to does not exist.") - - compiled = Object.keys(routes).map(function(route) { - if (route[0] !== "/") throw new SyntaxError("Routes must start with a '/'.") - if ((/:([^\/\.-]+)(\.{3})?:/).test(route)) { - throw new SyntaxError("Route parameter names must be separated with either '/', '.', or '-'.") - } - return { - route: route, - component: routes[route], - check: compileTemplate(route), - } - }) - fallbackRoute = defaultRoute - if (defaultRoute != null) { - var defaultData = parsePathname(defaultRoute) - - if (!compiled.some(function (i) { return i.check(defaultData) })) { - throw new ReferenceError("Default route doesn't match any known routes.") - } - } - - if (typeof $window.history.pushState === "function") { - $window.addEventListener("popstate", fireAsync, false) - } else if (route.prefix[0] === "#") { - $window.addEventListener("hashchange", resolveRoute, false) - } - - ready = true - mountRedraw.mount(root, RouterRoot) - resolveRoute() - } - route.set = function(path, data, options) { - if (lastUpdate != null) { - options = options || {} - options.replace = true - } - lastUpdate = null - setPath(path, data, options) - } - route.get = function() {return currentPath} - route.prefix = "#!" - route.Link = { - view: function(vnode) { - // Omit the used parameters from the rendered element - they are - // internal. Also, censor the various lifecycle methods. - // - // We don't strip the other parameters because for convenience we - // let them be specified in the selector as well. - var child = m( - vnode.attrs.selector || "a", - censor(vnode.attrs, ["options", "params", "selector", "onclick"]), - vnode.children - ) - var options, onclick, href - - // Let's provide a *right* way to disable a route link, rather than - // letting people screw up accessibility on accident. - // - // The attribute is coerced so users don't get surprised over - // `disabled: 0` resulting in a button that's somehow routable - // despite being visibly disabled. - if (child.attrs.disabled = Boolean(child.attrs.disabled)) { - child.attrs.href = null - child.attrs["aria-disabled"] = "true" - // If you *really* do want add `onclick` on a disabled link, use - // an `oncreate` hook to add it. - } else { - options = vnode.attrs.options - onclick = vnode.attrs.onclick - // Easier to build it now to keep it isomorphic. - href = buildPathname(child.attrs.href, vnode.attrs.params) - child.attrs.href = route.prefix + href - child.attrs.onclick = function(e) { - var result - if (typeof onclick === "function") { - result = onclick.call(e.currentTarget, e) - } else if (onclick == null || typeof onclick !== "object") { - // do nothing - } else if (typeof onclick.handleEvent === "function") { - onclick.handleEvent(e) - } - - // Adapted from React Router's implementation: - // https://github.com/ReactTraining/react-router/blob/520a0acd48ae1b066eb0b07d6d4d1790a1d02482/packages/react-router-dom/modules/Link.js - // - // Try to be flexible and intuitive in how we handle links. - // Fun fact: links aren't as obvious to get right as you - // would expect. There's a lot more valid ways to click a - // link than this, and one might want to not simply click a - // link, but right click or command-click it to copy the - // link target, etc. Nope, this isn't just for blind people. - if ( - // Skip if `onclick` prevented default - result !== false && !e.defaultPrevented && - // Ignore everything but left clicks - (e.button === 0 || e.which === 0 || e.which === 1) && - // Let the browser handle `target=_blank`, etc. - (!e.currentTarget.target || e.currentTarget.target === "_self") && - // No modifier keys - !e.ctrlKey && !e.metaKey && !e.shiftKey && !e.altKey - ) { - e.preventDefault() - e.redraw = false - route.set(href, null, options) - } - } - } - return child - }, - } - route.param = function(key) { - return attrs && key != null ? attrs[key] : attrs - } - - return route -} diff --git a/api/tests/test-mountRedraw.js b/api/tests/test-mountRedraw.js deleted file mode 100644 index e069e9b2c..000000000 --- a/api/tests/test-mountRedraw.js +++ /dev/null @@ -1,578 +0,0 @@ -"use strict" - -// Low-priority TODO: remove the dependency on the renderer here. -var o = require("ospec") -var components = require("../../test-utils/components") -var domMock = require("../../test-utils/domMock") -var throttleMocker = require("../../test-utils/throttleMock") -var mountRedraw = require("../../api/mount-redraw") -var coreRenderer = require("../../render/render") -var h = require("../../render/hyperscript") - -o.spec("mount/redraw", function() { - var root, m, throttleMock, consoleMock, $document, errors - o.beforeEach(function() { - var $window = domMock() - consoleMock = {error: o.spy()} - throttleMock = throttleMocker() - root = $window.document.body - m = mountRedraw(coreRenderer($window), throttleMock.schedule, consoleMock) - $document = $window.document - errors = [] - }) - - o.afterEach(function() { - o(consoleMock.error.calls.map(function(c) { - return c.args[0] - })).deepEquals(errors) - o(throttleMock.queueLength()).equals(0) - }) - - o("shouldn't error if there are no renderers", function() { - m.redraw() - throttleMock.fire() - }) - - o("schedules correctly", function() { - var spy = o.spy() - - m.mount(root, {view: spy}) - o(spy.callCount).equals(1) - m.redraw() - o(spy.callCount).equals(1) - throttleMock.fire() - o(spy.callCount).equals(2) - }) - - o("should run a single renderer entry", function() { - var spy = o.spy() - - m.mount(root, {view: spy}) - - o(spy.callCount).equals(1) - - m.redraw() - m.redraw() - m.redraw() - - o(spy.callCount).equals(1) - throttleMock.fire() - o(spy.callCount).equals(2) - }) - - o("should run all renderer entries", 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() - - m.mount(el1, {view: spy1}) - m.mount(el2, {view: spy2}) - m.mount(el3, {view: spy3}) - - m.redraw() - - o(spy1.callCount).equals(1) - o(spy2.callCount).equals(1) - o(spy3.callCount).equals(1) - - m.redraw() - - o(spy1.callCount).equals(1) - o(spy2.callCount).equals(1) - o(spy3.callCount).equals(1) - - throttleMock.fire() - - o(spy1.callCount).equals(2) - o(spy2.callCount).equals(2) - o(spy3.callCount).equals(2) - }) - - o("should not redraw when mounting another root", 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() - - m.mount(el1, {view: spy1}) - o(spy1.callCount).equals(1) - o(spy2.callCount).equals(0) - o(spy3.callCount).equals(0) - - m.mount(el2, {view: spy2}) - o(spy1.callCount).equals(1) - o(spy2.callCount).equals(1) - o(spy3.callCount).equals(0) - - m.mount(el3, {view: spy3}) - o(spy1.callCount).equals(1) - o(spy2.callCount).equals(1) - o(spy3.callCount).equals(1) - }) - - o("should stop running after mount null", function() { - var spy = o.spy() - - m.mount(root, {view: spy}) - o(spy.callCount).equals(1) - m.mount(root, null) - - m.redraw() - - o(spy.callCount).equals(1) - throttleMock.fire() - o(spy.callCount).equals(1) - }) - - o("should stop running after mount undefined", function() { - var spy = o.spy() - - m.mount(root, {view: spy}) - o(spy.callCount).equals(1) - m.mount(root, undefined) - - m.redraw() - - o(spy.callCount).equals(1) - throttleMock.fire() - o(spy.callCount).equals(1) - }) - - o("should stop running after mount no arg", function() { - var spy = o.spy() - - m.mount(root, {view: spy}) - o(spy.callCount).equals(1) - m.mount(root) - - m.redraw() - - o(spy.callCount).equals(1) - throttleMock.fire() - o(spy.callCount).equals(1) - }) - - o("should invoke remove callback on unmount", function() { - var spy = o.spy() - var onremove = o.spy() - - m.mount(root, {view: spy, onremove: onremove}) - o(spy.callCount).equals(1) - m.mount(root) - - o(spy.callCount).equals(1) - o(onremove.callCount).equals(1) - }) - - o("should stop running after unsubscribe, even if it occurs after redraw is requested", function() { - var spy = o.spy() - - m.mount(root, {view: spy}) - o(spy.callCount).equals(1) - m.redraw() - m.mount(root) - - o(spy.callCount).equals(1) - throttleMock.fire() - o(spy.callCount).equals(1) - }) - - o("does nothing on invalid unmount", function() { - var spy = o.spy() - - m.mount(root, {view: spy}) - o(spy.callCount).equals(1) - - m.mount(null) - m.redraw() - throttleMock.fire() - o(spy.callCount).equals(2) - }) - - 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() - - m.mount(el1, {view: spy1}) - m.mount(el2, {view: spy2}) - m.mount(el3, {view: spy3}) - - o(spy1.callCount).equals(1) - o(spy2.callCount).equals(1) - o(spy3.callCount).equals(1) - - m.redraw.sync() - - o(spy1.callCount).equals(2) - o(spy2.callCount).equals(2) - o(spy3.callCount).equals(2) - - m.redraw.sync() - - o(spy1.callCount).equals(3) - o(spy2.callCount).equals(3) - o(spy3.callCount).equals(3) - }) - - - o("throws on invalid component", function() { - o(function() { m.mount(root, {}) }).throws(TypeError) - }) - - o("skips roots that were synchronously unsubscribed before they were visited", function() { - var calls = [] - var root1 = $document.createElement("div") - var root2 = $document.createElement("div") - var root3 = $document.createElement("div") - - m.mount(root1, { - onbeforeupdate: function() { - m.mount(root2, null) - }, - view: function() { calls.push("root1") }, - }) - m.mount(root2, {view: function() { calls.push("root2") }}) - m.mount(root3, {view: function() { calls.push("root3") }}) - o(calls).deepEquals([ - "root1", "root2", "root3", - ]) - - m.redraw.sync() - o(calls).deepEquals([ - "root1", "root2", "root3", - "root1", "root3", - ]) - }) - - o("keeps its place when synchronously unsubscribing previously visited roots", function() { - var calls = [] - var root1 = $document.createElement("div") - var root2 = $document.createElement("div") - var root3 = $document.createElement("div") - - m.mount(root1, {view: function() { calls.push("root1") }}) - m.mount(root2, { - onbeforeupdate: function() { - m.mount(root1, null) - }, - view: function() { calls.push("root2") }, - }) - m.mount(root3, {view: function() { calls.push("root3") }}) - o(calls).deepEquals([ - "root1", "root2", "root3", - ]) - - m.redraw.sync() - o(calls).deepEquals([ - "root1", "root2", "root3", - "root1", "root2", "root3", - ]) - }) - - o("keeps its place when synchronously unsubscribing previously visited roots in the face of errors", function() { - errors = ["fail"] - var calls = [] - var root1 = $document.createElement("div") - var root2 = $document.createElement("div") - var root3 = $document.createElement("div") - - m.mount(root1, {view: function() { calls.push("root1") }}) - m.mount(root2, { - onbeforeupdate: function() { - m.mount(root1, null) - throw "fail" - }, - view: function() { calls.push("root2") }, - }) - m.mount(root3, {view: function() { calls.push("root3") }}) - o(calls).deepEquals([ - "root1", "root2", "root3", - ]) - - m.redraw.sync() - o(calls).deepEquals([ - "root1", "root2", "root3", - "root1", "root3", - ]) - }) - - o("keeps its place when synchronously unsubscribing the current root", function() { - var calls = [] - var root1 = $document.createElement("div") - var root2 = $document.createElement("div") - var root3 = $document.createElement("div") - - m.mount(root1, {view: function() { calls.push("root1") }}) - m.mount(root2, { - onbeforeupdate: function() { - try { m.mount(root2, null) } catch (e) { calls.push([e.constructor, e.message]) } - }, - view: function() { calls.push("root2") }, - }) - m.mount(root3, {view: function() { calls.push("root3") }}) - o(calls).deepEquals([ - "root1", "root2", "root3", - ]) - - m.redraw.sync() - o(calls).deepEquals([ - "root1", "root2", "root3", - "root1", [TypeError, "Node is currently being rendered to and thus is locked."], "root2", "root3", - ]) - }) - - o("keeps its place when synchronously unsubscribing the current root in the face of an error", function() { - errors = [ - [TypeError, "Node is currently being rendered to and thus is locked."], - ] - var calls = [] - var root1 = $document.createElement("div") - var root2 = $document.createElement("div") - var root3 = $document.createElement("div") - - m.mount(root1, {view: function() { calls.push("root1") }}) - m.mount(root2, { - onbeforeupdate: function() { - try { m.mount(root2, null) } catch (e) { throw [e.constructor, e.message] } - }, - view: function() { calls.push("root2") }, - }) - m.mount(root3, {view: function() { calls.push("root3") }}) - o(calls).deepEquals([ - "root1", "root2", "root3", - ]) - - m.redraw.sync() - o(calls).deepEquals([ - "root1", "root2", "root3", - "root1", "root3", - ]) - }) - - components.forEach(function(cmp){ - o.spec(cmp.kind, function(){ - var createComponent = cmp.create - - o("throws on invalid `root` DOM node", function() { - o(function() { - m.mount(null, createComponent({view: function() {}})) - }).throws(TypeError) - }) - - o("renders into `root` synchronously", function() { - m.mount(root, createComponent({ - view: function() { - return h("div") - } - })) - - o(root.firstChild.nodeName).equals("DIV") - }) - - o("mounting null unmounts", function() { - m.mount(root, createComponent({ - view: function() { - return h("div") - } - })) - - m.mount(root, null) - - o(root.childNodes.length).equals(0) - }) - - o("Mounting a second root doesn't cause the first one to redraw", function() { - var root1 = $document.createElement("div") - var root2 = $document.createElement("div") - var view = o.spy() - - m.mount(root1, createComponent({view: view})) - o(view.callCount).equals(1) - - m.mount(root2, createComponent({view: function() {}})) - - 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() - var e = $document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - - m.mount(root, createComponent({ - view: function() { - return h("div", { - oninit: oninit, - onupdate: onupdate, - onclick: onclick, - }) - } - })) - - root.firstChild.dispatchEvent(e) - - o(oninit.callCount).equals(1) - o(onupdate.callCount).equals(0) - - o(onclick.callCount).equals(1) - o(onclick.this).equals(root.firstChild) - o(onclick.args[0].type).equals("click") - o(onclick.args[0].target).equals(root.firstChild) - - throttleMock.fire() - - o(onupdate.callCount).equals(1) - }) - - o("redraws several mount points on events", function() { - var onupdate0 = o.spy() - var oninit0 = o.spy() - var onclick0 = o.spy() - var onupdate1 = o.spy() - var oninit1 = o.spy() - var onclick1 = o.spy() - - var root1 = $document.createElement("div") - var root2 = $document.createElement("div") - var e = $document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - - m.mount(root1, createComponent({ - view: function() { - return h("div", { - oninit: oninit0, - onupdate: onupdate0, - onclick: onclick0, - }) - } - })) - - o(oninit0.callCount).equals(1) - o(onupdate0.callCount).equals(0) - - m.mount(root2, createComponent({ - view: function() { - return h("div", { - oninit: oninit1, - onupdate: onupdate1, - onclick: onclick1, - }) - } - })) - - o(oninit1.callCount).equals(1) - o(onupdate1.callCount).equals(0) - - root1.firstChild.dispatchEvent(e) - o(onclick0.callCount).equals(1) - o(onclick0.this).equals(root1.firstChild) - - throttleMock.fire() - - o(onupdate0.callCount).equals(1) - o(onupdate1.callCount).equals(1) - - root2.firstChild.dispatchEvent(e) - - o(onclick1.callCount).equals(1) - o(onclick1.this).equals(root2.firstChild) - - throttleMock.fire() - - o(onupdate0.callCount).equals(2) - o(onupdate1.callCount).equals(2) - }) - - 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 = $document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - - m.mount(root, createComponent({ - view: function() { - return h("div", { - oninit: oninit, - onupdate: onupdate, - onclick: function(e) { - e.redraw = false - } - }) - } - })) - - root.firstChild.dispatchEvent(e) - - o(oninit.callCount).equals(1) - o(e.redraw).equals(false) - - throttleMock.fire() - - o(onupdate.callCount).equals(0) - o(e.redraw).equals(false) - }) - - o("redraws when the render function is run", function() { - var onupdate = o.spy() - var oninit = o.spy() - - m.mount(root, createComponent({ - view: function() { - return h("div", { - oninit: oninit, - onupdate: onupdate - }) - } - })) - - o(oninit.callCount).equals(1) - o(onupdate.callCount).equals(0) - - m.redraw() - - throttleMock.fire() - - o(onupdate.callCount).equals(1) - }) - - o("emits errors correctly", function() { - errors = ["foo", "bar", "baz"] - var counter = -1 - - m.mount(root, createComponent({ - view: function() { - var value = errors[counter++] - if (value != null) throw value - return null - } - })) - - m.redraw() - throttleMock.fire() - m.redraw() - throttleMock.fire() - m.redraw() - throttleMock.fire() - }) - }) - }) -}) diff --git a/api/tests/test-router.js b/api/tests/test-router.js deleted file mode 100644 index 3efe9fa6b..000000000 --- a/api/tests/test-router.js +++ /dev/null @@ -1,1913 +0,0 @@ -"use strict" - -// Low-priority TODO: remove the dependency on the renderer here. -var o = require("ospec") -var browserMock = require("../../test-utils/browserMock") -var throttleMocker = require("../../test-utils/throttleMock") - -var m = require("../../render/hyperscript") -var coreRenderer = require("../../render/render") -var apiMountRedraw = require("../../api/mount-redraw") -var apiRouter = require("../../api/router") - -o.spec("route", function() { - // Note: the `n` parameter used in calls to this are generally found by - // either trial-and-error or by studying the source. If tests are failing, - // find the failing assertions, set `n` to about 10 on the preceding call to - // `waitCycles`, then drop them down incrementally until it fails. The last - // one to succeed is the one you want to keep. And just do that for each - // failing assertion, and it'll eventually work. - // - // This is effectively what I did when designing this and hooking everything - // up. (It would be so much easier to just be able to run the calls with a - // different event loop and just turn it until I get what I want, but JS - // lacks that functionality.) - - // Use precisely what `m.route` uses, for consistency and to ensure timings - // are aligned. - function waitCycles(n) { - n = Math.max(n, 1) - return new Promise(function(resolve) { - return loop() - function loop() { - if (n === 0) resolve() - else { n--; setTimeout(loop, 4) } - } - }) - } - - void [{protocol: "http:", hostname: "localhost"}, {protocol: "file:", hostname: "/"}, {protocol: "http:", hostname: "ΓΆΓΆΓΆ"}].forEach(function(env) { - void ["#", "?", "", "#!", "?!", "/foo", "/fΓΆΓΆ"].forEach(function(prefix) { - o.spec("using prefix `" + prefix + "` starting on " + env.protocol + "//" + env.hostname, function() { - var $window, root, mountRedraw, route, throttleMock - var nextID = 0 - var currentTest = 0 - - // Once done, a root should no longer be alive. This verifies - // that, and it's a *very* subtle test bug that can lead to - // some rather unusual consequences. If this fails, use - // `waitCycles(n)` to avoid this. - function lock(func) { - var id = currentTest - var start = Date.now() - try { - throw new Error() - } catch (trace) { - return function() { - // This *will* cause a test failure. - if (id != null && id !== currentTest) { - id = undefined - trace.message = "called " + - (Date.now() - start) + "ms after test end" - console.error(trace.stack) - o("in test").equals("not in test") - } - return func.apply(this, arguments) - } - } - } - - // In case it doesn't get reset - var realError = console.error - - o.beforeEach(function() { - currentTest = nextID++ - $window = browserMock(env) - $window.setTimeout = setTimeout - // $window.setImmediate = setImmediate - throttleMock = throttleMocker() - - root = $window.document.body - - mountRedraw = apiMountRedraw(coreRenderer($window), throttleMock.schedule, console) - route = apiRouter($window, mountRedraw) - route.prefix = prefix - console.error = function() { - realError.call(this, new Error("Unexpected `console.error` call")) - realError.apply(this, arguments) - } - }) - - o.afterEach(function() { - o(throttleMock.queueLength()).equals(0) - currentTest = -1 // doesn't match any test - console.error = realError - }) - - o("throws on invalid `root` DOM node", function() { - var threw = false - try { - route(null, "/", {"/":{view: lock(function() {})}}) - } catch (e) { - threw = true - } - o(threw).equals(true) - }) - - o("renders into `root`", function() { - $window.location.href = prefix + "/" - route(root, "/", { - "/" : { - view: lock(function() { - return m("div") - }) - } - }) - - o(root.firstChild.nodeName).equals("DIV") - }) - - o("resolves to route with escaped unicode", function() { - $window.location.href = prefix + "/%C3%B6?%C3%B6=%C3%B6" - route(root, "/ΓΆ", { - "/ΓΆ" : { - view: lock(function() { - return m("div") - }) - } - }) - - o(root.firstChild.nodeName).equals("DIV") - }) - - o("resolves to route with unicode", function() { - $window.location.href = prefix + "/ΓΆ?ΓΆ=ΓΆ" - route(root, "/ΓΆ", { - "/ΓΆ" : { - view: lock(function() { - return JSON.stringify(route.param()) + " " + - route.get() - }) - } - }) - - o(root.firstChild.nodeValue).equals('{"ΓΆ":"ΓΆ"} /ΓΆ?ΓΆ=ΓΆ') - }) - - o("resolves to route with matching invalid escape", function() { - $window.location.href = prefix + "/%C3%B6abc%def" - route(root, "/ΓΆabc%def", { - "/ΓΆabc%def" : { - view: lock(function() { - return route.get() - }) - } - }) - - o(root.firstChild.nodeValue).equals("/ΓΆabc%def") - }) - - o("handles parameterized route", function() { - $window.location.href = prefix + "/test/x" - route(root, "/test/:a", { - "/test/:a" : { - view: lock(function(vnode) { - return JSON.stringify(route.param()) + " " + - JSON.stringify(vnode.attrs) + " " + - route.get() - }) - } - }) - - o(root.firstChild.nodeValue).equals( - '{"a":"x"} {"a":"x"} /test/x' - ) - }) - - o("handles multi-parameterized route", function() { - $window.location.href = prefix + "/test/x/y" - route(root, "/test/:a/:b", { - "/test/:a/:b" : { - view: lock(function(vnode) { - return JSON.stringify(route.param()) + " " + - JSON.stringify(vnode.attrs) + " " + - route.get() - }) - } - }) - - o(root.firstChild.nodeValue).equals( - '{"a":"x","b":"y"} {"a":"x","b":"y"} /test/x/y' - ) - }) - - o("handles rest parameterized route", function() { - $window.location.href = prefix + "/test/x/y" - route(root, "/test/:a...", { - "/test/:a..." : { - view: lock(function(vnode) { - return JSON.stringify(route.param()) + " " + - JSON.stringify(vnode.attrs) + " " + - route.get() - }) - } - }) - - o(root.firstChild.nodeValue).equals( - '{"a":"x/y"} {"a":"x/y"} /test/x/y' - ) - }) - - o("keeps trailing / in rest parameterized route", function() { - $window.location.href = prefix + "/test/d/" - route(root, "/test/:a...", { - "/test/:a..." : { - view: lock(function(vnode) { - return JSON.stringify(route.param()) + " " + - JSON.stringify(vnode.attrs) + " " + - route.get() - }) - } - }) - - o(root.firstChild.nodeValue).equals( - '{"a":"d/"} {"a":"d/"} /test/d/' - ) - }) - - o("handles route with search", function() { - $window.location.href = prefix + "/test?a=b&c=d" - route(root, "/test", { - "/test" : { - view: lock(function(vnode) { - return JSON.stringify(route.param()) + " " + - JSON.stringify(vnode.attrs) + " " + - route.get() - }) - } - }) - - o(root.firstChild.nodeValue).equals( - '{"a":"b","c":"d"} {"a":"b","c":"d"} /test?a=b&c=d' - ) - }) - - o("redirects to default route if no match", function() { - $window.location.href = prefix + "/test" - route(root, "/other", { - "/other": { - view: lock(function(vnode) { - return JSON.stringify(route.param()) + " " + - JSON.stringify(vnode.attrs) + " " + - route.get() - }) - } - }) - - return waitCycles(1).then(function() { - o(root.firstChild.nodeValue).equals("{} {} /other") - }) - }) - - o("handles out of order routes", function() { - $window.location.href = prefix + "/z/y/x" - - route(root, "/z/y/x", { - "/z/y/x": { - view: lock(function() { return "1" }), - }, - "/:a...": { - view: lock(function() { return "2" }), - }, - }) - - o(root.firstChild.nodeValue).equals("1") - }) - - o("handles reverse out of order routes", function() { - $window.location.href = prefix + "/z/y/x" - - route(root, "/z/y/x", { - "/:a...": { - view: lock(function() { return "2" }), - }, - "/z/y/x": { - view: lock(function() { return "1" }), - }, - }) - - o(root.firstChild.nodeValue).equals("2") - }) - - o("resolves to route on fallback mode", function() { - $window.location.href = "file://" + prefix + "/test" - - route(root, "/test", { - "/test" : { - view: lock(function(vnode) { - return JSON.stringify(route.param()) + " " + - JSON.stringify(vnode.attrs) + " " + - route.get() - }) - } - }) - - o(root.firstChild.nodeValue).equals("{} {} /test") - }) - - o("routed mount points only redraw asynchronously (POJO component)", function() { - var view = o.spy() - - $window.location.href = prefix + "/" - route(root, "/", {"/":{view:view}}) - - o(view.callCount).equals(1) - - mountRedraw.redraw() - - o(view.callCount).equals(1) - - throttleMock.fire() - - o(view.callCount).equals(2) - }) - - o("routed mount points only redraw asynchronously (constructible component)", function() { - var view = o.spy() - - var Cmp = lock(function(){}) - Cmp.prototype.view = lock(view) - - $window.location.href = prefix + "/" - route(root, "/", {"/":Cmp}) - - o(view.callCount).equals(1) - - mountRedraw.redraw() - - o(view.callCount).equals(1) - - throttleMock.fire() - - o(view.callCount).equals(2) - }) - - o("routed mount points only redraw asynchronously (closure component)", function() { - var view = o.spy() - - function Cmp() {return {view: lock(view)}} - - $window.location.href = prefix + "/" - route(root, "/", {"/":lock(Cmp)}) - - o(view.callCount).equals(1) - - mountRedraw.redraw() - - o(view.callCount).equals(1) - - throttleMock.fire() - - o(view.callCount).equals(2) - }) - - o("subscribes correctly and removes when unmounted", function() { - $window.location.href = prefix + "/" - - route(root, "/", { - "/" : { - view: lock(function() { - return m("div") - }) - } - }) - - o(root.firstChild.nodeName).equals("DIV") - - mountRedraw.mount(root) - - o(root.childNodes.length).equals(0) - }) - - o("default route doesn't break back button", function() { - $window.location.href = "http://old.com" - $window.location.href = "http://new.com" - - route(root, "/a", { - "/a" : { - view: lock(function() { - return m("div") - }) - } - }) - - return waitCycles(1).then(function() { - o(root.firstChild.nodeName).equals("DIV") - - o(route.get()).equals("/a") - - $window.history.back() - - o($window.location.pathname).equals("/") - o($window.location.hostname).equals("old.com") - }) - }) - - o("default route does not inherit params", function() { - $window.location.href = "/invalid?foo=bar" - route(root, "/a", { - "/a" : { - oninit: lock(function(vnode) { - o(vnode.attrs.foo).equals(undefined) - }), - view: lock(function() { - return m("div") - }) - } - }) - - return waitCycles(1) - }) - - o("redraws when render function is executed", function() { - var onupdate = o.spy() - var oninit = o.spy() - - $window.location.href = prefix + "/" - route(root, "/", { - "/" : { - view: lock(function() { - return m("div", { - oninit: oninit, - onupdate: onupdate - }) - }) - } - }) - - o(oninit.callCount).equals(1) - - mountRedraw.redraw() - throttleMock.fire() - - o(onupdate.callCount).equals(1) - }) - - o("redraws on events", function() { - var onupdate = o.spy() - var oninit = o.spy() - var onclick = o.spy() - var e = $window.document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - - $window.location.href = prefix + "/" - route(root, "/", { - "/" : { - view: lock(function() { - return m("div", { - oninit: oninit, - onupdate: onupdate, - onclick: onclick, - }) - }) - } - }) - - root.firstChild.dispatchEvent(e) - - o(oninit.callCount).equals(1) - - o(onclick.callCount).equals(1) - o(onclick.this).equals(root.firstChild) - o(onclick.args[0].type).equals("click") - o(onclick.args[0].target).equals(root.firstChild) - - - throttleMock.fire() - o(onupdate.callCount).equals(1) - }) - - o("event handlers can skip redraw", function() { - var onupdate = o.spy() - var oninit = o.spy() - var e = $window.document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - - $window.location.href = prefix + "/" - route(root, "/", { - "/" : { - view: lock(function() { - return m("div", { - oninit: oninit, - onupdate: onupdate, - onclick: lock(function(e) { - e.redraw = false - }), - }) - }) - } - }) - - o(oninit.callCount).equals(1) - - root.firstChild.dispatchEvent(e) - throttleMock.fire() - - // Wrapped to ensure no redraw fired - return waitCycles(1).then(function() { - o(onupdate.callCount).equals(0) - }) - }) - - o("changes location on route.Link", function() { - var e = $window.document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - e.button = 0 - - $window.location.href = prefix + "/" - route(root, "/", { - "/" : { - view: lock(function() { - return m(route.Link, {href: "/test"}) - }) - }, - "/test" : { - view : lock(function() { - return m("div") - }) - } - }) - - var slash = prefix[0] === "/" ? "" : "/" - - o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "")) - - root.firstChild.dispatchEvent(e) - throttleMock.fire() - 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) - e.button = 0 - $window.location.href = prefix + "/" - - route(root, "/", { - "/" : { - view: lock(function() { - return m(route.Link, { - href: "/test", - options: opts, - }) - }) - }, - "/test" : { - view : lock(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("passes params on route.Link", function() { - var e = $window.document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - e.button = 0 - $window.location.href = prefix + "/" - - route(root, "/", { - "/" : { - view: lock(function() { - return m(route.Link, { - href: "/test", - params: {key: "value"}, - }) - }) - }, - "/test" : { - view : lock(function() { - return m("div") - }) - } - }) - route.set = o.spy(route.set) - - root.firstChild.dispatchEvent(e) - - o(route.set.callCount).equals(1) - o(route.set.args[0]).equals("/test?key=value") - }) - - o("route.Link can render without routes or dom access", function() { - $window = browserMock(env) - var render = coreRenderer($window) - route = apiRouter(null, null) - route.prefix = prefix - root = $window.document.body - - render(root, m(route.Link, {href: "/test", foo: "bar"}, "text")) - - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("A") - o(root.firstChild.href).equals(prefix + "/test") - o(root.firstChild.hasAttribute("aria-disabled")).equals(false) - o(root.firstChild.hasAttribute("disabled")).equals(false) - o(root.firstChild.attributes["foo"].value).equals("bar") - o(root.firstChild.childNodes.length).equals(1) - o(root.firstChild.firstChild.nodeName).equals("#text") - o(root.firstChild.firstChild.nodeValue).equals("text") - }) - - o("route.Link keeps magic attributes from being double-called", function() { - $window = browserMock(env) - var render = coreRenderer($window) - route = apiRouter(null, null) - route.prefix = prefix - root = $window.document.body - - var oninit = o.spy() - var oncreate = o.spy() - var onbeforeupdate = o.spy() - var onupdate = o.spy() - var onbeforeremove = o.spy() - var onremove = o.spy() - - render(root, m(route.Link, { - href: "/test", - oninit: oninit, - oncreate: oncreate, - onbeforeupdate: onbeforeupdate, - onupdate: onupdate, - onbeforeremove: onbeforeremove, - onremove: onremove, - }, "text")) - - o(oninit.callCount).equals(1) - o(oncreate.callCount).equals(1) - o(onbeforeupdate.callCount).equals(0) - o(onupdate.callCount).equals(0) - o(onbeforeremove.callCount).equals(0) - o(onremove.callCount).equals(0) - - render(root, m(route.Link, { - href: "/test", - oninit: oninit, - oncreate: oncreate, - onbeforeupdate: onbeforeupdate, - onupdate: onupdate, - onbeforeremove: onbeforeremove, - onremove: onremove, - }, "text")) - - o(oninit.callCount).equals(1) - o(oncreate.callCount).equals(1) - o(onbeforeupdate.callCount).equals(1) - o(onupdate.callCount).equals(1) - o(onbeforeremove.callCount).equals(0) - o(onremove.callCount).equals(0) - - render(root, []) - - o(oninit.callCount).equals(1) - o(oncreate.callCount).equals(1) - o(onbeforeupdate.callCount).equals(1) - o(onupdate.callCount).equals(1) - o(onbeforeremove.callCount).equals(1) - o(onremove.callCount).equals(1) - }) - - o("route.Link can render other tag without routes or dom access", function() { - $window = browserMock(env) - var render = coreRenderer($window) - route = apiRouter(null, null) - route.prefix = prefix - root = $window.document.body - - render(root, m(route.Link, {selector: "button", href: "/test", foo: "bar"}, "text")) - - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("BUTTON") - o(root.firstChild.attributes["href"].value).equals(prefix + "/test") - o(root.firstChild.hasAttribute("aria-disabled")).equals(false) - o(root.firstChild.hasAttribute("disabled")).equals(false) - o(root.firstChild.attributes["foo"].value).equals("bar") - o(root.firstChild.childNodes.length).equals(1) - o(root.firstChild.firstChild.nodeName).equals("#text") - o(root.firstChild.firstChild.nodeValue).equals("text") - }) - - o("route.Link can render other selector without routes or dom access", function() { - $window = browserMock(env) - var render = coreRenderer($window) - route = apiRouter(null, null) - route.prefix = prefix - root = $window.document.body - - render(root, m(route.Link, {selector: "button[href=/test]", foo: "bar"}, "text")) - - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("BUTTON") - o(root.firstChild.attributes["href"].value).equals(prefix + "/test") - o(root.firstChild.hasAttribute("aria-disabled")).equals(false) - o(root.firstChild.hasAttribute("disabled")).equals(false) - o(root.firstChild.attributes["foo"].value).equals("bar") - o(root.firstChild.childNodes.length).equals(1) - o(root.firstChild.firstChild.nodeName).equals("#text") - o(root.firstChild.firstChild.nodeValue).equals("text") - }) - - o("route.Link can render not disabled", function() { - $window = browserMock(env) - var render = coreRenderer($window) - route = apiRouter(null, null) - route.prefix = prefix - root = $window.document.body - - render(root, m(route.Link, {href: "/test", disabled: false, foo: "bar"}, "text")) - - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("A") - o(root.firstChild.href).equals(prefix + "/test") - o(root.firstChild.hasAttribute("aria-disabled")).equals(false) - o(root.firstChild.hasAttribute("disabled")).equals(false) - o(root.firstChild.attributes["foo"].value).equals("bar") - o(root.firstChild.childNodes.length).equals(1) - o(root.firstChild.firstChild.nodeName).equals("#text") - o(root.firstChild.firstChild.nodeValue).equals("text") - }) - - o("route.Link can render falsy disabled", function() { - $window = browserMock(env) - var render = coreRenderer($window) - route = apiRouter(null, null) - route.prefix = prefix - root = $window.document.body - - render(root, m(route.Link, {href: "/test", disabled: 0, foo: "bar"}, "text")) - - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("A") - o(root.firstChild.href).equals(prefix + "/test") - o(root.firstChild.hasAttribute("aria-disabled")).equals(false) - o(root.firstChild.hasAttribute("disabled")).equals(false) - o(root.firstChild.attributes["foo"].value).equals("bar") - o(root.firstChild.childNodes.length).equals(1) - o(root.firstChild.firstChild.nodeName).equals("#text") - o(root.firstChild.firstChild.nodeValue).equals("text") - }) - - o("route.Link can render disabled", function() { - $window = browserMock(env) - var render = coreRenderer($window) - route = apiRouter(null, null) - route.prefix = prefix - root = $window.document.body - - render(root, m(route.Link, {href: "/test", disabled: true, foo: "bar"}, "text")) - - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("A") - o(root.firstChild.href).equals("") - o(root.firstChild.attributes["aria-disabled"].value).equals("true") - o(root.firstChild.attributes["foo"].value).equals("bar") - o(root.firstChild.attributes["disabled"].value).equals("") - o(root.firstChild.childNodes.length).equals(1) - o(root.firstChild.firstChild.nodeName).equals("#text") - o(root.firstChild.firstChild.nodeValue).equals("text") - }) - - o("route.Link can render truthy disabled", function() { - $window = browserMock(env) - var render = coreRenderer($window) - route = apiRouter(null, null) - route.prefix = prefix - root = $window.document.body - - render(root, m(route.Link, {href: "/test", disabled: 1, foo: "bar"}, "text")) - - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("A") - o(root.firstChild.href).equals("") - o(root.firstChild.attributes["aria-disabled"].value).equals("true") - o(root.firstChild.attributes["foo"].value).equals("bar") - o(root.firstChild.attributes["disabled"].value).equals("") - o(root.firstChild.childNodes.length).equals(1) - o(root.firstChild.firstChild.nodeName).equals("#text") - o(root.firstChild.firstChild.nodeValue).equals("text") - }) - - o("route.Link doesn't redraw on wrong button", function() { - var e = $window.document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - e.button = 10 - - $window.location.href = prefix + "/" - route(root, "/", { - "/" : { - view: lock(function() { - return m(route.Link, {href: "/test"}) - }) - }, - "/test" : { - view : lock(function() { - return m("div") - }) - } - }) - - var slash = prefix[0] === "/" ? "" : "/" - - o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "")) - - root.firstChild.dispatchEvent(e) - throttleMock.fire() - o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "")) - }) - - o("route.Link doesn't redraw on preventDefault", function() { - var e = $window.document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - e.button = 0 - - $window.location.href = prefix + "/" - route(root, "/", { - "/" : { - view: lock(function() { - return m(route.Link, { - href: "/test", - onclick: function(e) { - e.preventDefault() - } - }) - }) - }, - "/test" : { - view : lock(function() { - return m("div") - }) - } - }) - - var slash = prefix[0] === "/" ? "" : "/" - - o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "")) - - root.firstChild.dispatchEvent(e) - throttleMock.fire() - o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "")) - }) - - o("route.Link doesn't redraw on preventDefault in handleEvent", function() { - var e = $window.document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - e.button = 0 - - $window.location.href = prefix + "/" - route(root, "/", { - "/" : { - view: lock(function() { - return m(route.Link, { - href: "/test", - onclick: { - handleEvent: function(e) { - e.preventDefault() - } - } - }) - }) - }, - "/test" : { - view : lock(function() { - return m("div") - }) - } - }) - - var slash = prefix[0] === "/" ? "" : "/" - - o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "")) - - root.firstChild.dispatchEvent(e) - throttleMock.fire() - o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "")) - }) - - o("route.Link doesn't redraw on return false", function() { - var e = $window.document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - e.button = 0 - - $window.location.href = prefix + "/" - route(root, "/", { - "/" : { - view: lock(function() { - return m(route.Link, { - href: "/test", - onclick: function() { - return false - } - }) - }) - }, - "/test" : { - view : lock(function() { - return m("div") - }) - } - }) - - var slash = prefix[0] === "/" ? "" : "/" - - o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "")) - - root.firstChild.dispatchEvent(e) - throttleMock.fire() - o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "")) - }) - - o("accepts RouteResolver with onmatch that returns Component", function() { - var matchCount = 0 - var renderCount = 0 - var Component = { - view: lock(function() { - return m("span") - }) - } - - var resolver = { - onmatch: lock(function(args, requestedPath, route) { - matchCount++ - - o(args.id).equals("abc") - o(requestedPath).equals("/abc") - o(route).equals("/:id") - o(this).equals(resolver) - return Component - }), - render: lock(function(vnode) { - renderCount++ - - o(vnode.attrs.id).equals("abc") - o(this).equals(resolver) - - return vnode - }), - } - - $window.location.href = prefix + "/abc" - route(root, "/abc", { - "/:id" : resolver - }) - - return waitCycles(1).then(function() { - o(matchCount).equals(1) - o(renderCount).equals(1) - o(root.firstChild.nodeName).equals("SPAN") - }) - }) - - o("accepts RouteResolver with onmatch that returns route.SKIP", function() { - var match1Count = 0 - var match2Count = 0 - var render1 = o.spy() - var render2Count = 0 - var Component = { - view: lock(function() { - return m("span") - }) - } - - var resolver1 = { - onmatch: lock(function(args, requestedPath, key) { - match1Count++ - - o(args.id1).equals("abc") - o(requestedPath).equals("/abc") - o(key).equals("/:id1") - o(this).equals(resolver1) - return route.SKIP - }), - render: lock(render1), - } - - var resolver2 = { - onmatch: function(args, requestedPath, key) { - match2Count++ - - o(args.id2).equals("abc") - o(requestedPath).equals("/abc") - o(key).equals("/:id2") - o(this).equals(resolver2) - return Component - }, - render: function(vnode) { - render2Count++ - - o(vnode.attrs.id2).equals("abc") - o(this).equals(resolver2) - o(render1.callCount).equals(0) - - return vnode - }, - } - - $window.location.href = prefix + "/abc" - route(root, "/abc", { - "/:id1" : resolver1, - "/:id2" : resolver2 - }) - - return waitCycles(4).then(function() { - o(match1Count).equals(1) - o(match2Count).equals(1) - o(render2Count).equals(1) - o(render1.callCount).equals(0) - o(root.firstChild.nodeName).equals("SPAN") - }) - }) - - o("accepts RouteResolver with onmatch that returns Promise", function() { - var matchCount = 0 - var renderCount = 0 - var Component = { - view: lock(function() { - return m("span") - }) - } - - var resolver = { - onmatch: lock(function(args, requestedPath, route) { - matchCount++ - - o(args.id).equals("abc") - o(requestedPath).equals("/abc") - o(route).equals("/:id") - o(this).equals(resolver) - return Promise.resolve(Component) - }), - render: lock(function(vnode) { - renderCount++ - - o(vnode.attrs.id).equals("abc") - o(this).equals(resolver) - - return vnode - }), - } - - $window.location.href = prefix + "/abc" - route(root, "/abc", { - "/:id" : resolver - }) - - return waitCycles(10).then(function() { - o(matchCount).equals(1) - o(renderCount).equals(1) - o(root.firstChild.nodeName).equals("SPAN") - }) - }) - - o("accepts RouteResolver with onmatch that returns Promise", function() { - var matchCount = 0 - var renderCount = 0 - - var resolver = { - onmatch: lock(function(args, requestedPath, route) { - matchCount++ - - o(args.id).equals("abc") - o(requestedPath).equals("/abc") - o(route).equals("/:id") - o(this).equals(resolver) - return Promise.resolve() - }), - render: lock(function(vnode) { - renderCount++ - - o(vnode.attrs.id).equals("abc") - o(this).equals(resolver) - - return vnode - }), - } - - $window.location.href = prefix + "/abc" - route(root, "/abc", { - "/:id" : resolver - }) - - return waitCycles(2).then(function() { - o(matchCount).equals(1) - o(renderCount).equals(1) - o(root.firstChild.nodeName).equals("DIV") - }) - }) - - o("accepts RouteResolver with onmatch that returns Promise", function() { - var matchCount = 0 - var renderCount = 0 - - var resolver = { - onmatch: lock(function(args, requestedPath, route) { - matchCount++ - - o(args.id).equals("abc") - o(requestedPath).equals("/abc") - o(route).equals("/:id") - o(this).equals(resolver) - return Promise.resolve([]) - }), - render: lock(function(vnode) { - renderCount++ - - o(vnode.attrs.id).equals("abc") - o(this).equals(resolver) - - return vnode - }), - } - - $window.location.href = prefix + "/abc" - route(root, "/abc", { - "/:id" : resolver - }) - - return waitCycles(2).then(function() { - o(matchCount).equals(1) - o(renderCount).equals(1) - o(root.firstChild.nodeName).equals("DIV") - }) - }) - - o("accepts RouteResolver with onmatch that returns rejected Promise", function() { - var matchCount = 0 - var renderCount = 0 - var spy = o.spy() - var error = new Error("error") - var errorSpy = console.error = o.spy() - - var resolver = { - onmatch: lock(function() { - matchCount++ - return Promise.reject(error) - }), - render: lock(function(vnode) { - renderCount++ - return vnode - }), - } - - $window.location.href = prefix + "/test/1" - route(root, "/default", { - "/default" : {view: spy}, - "/test/:id" : resolver - }) - - return waitCycles(3).then(function() { - o(matchCount).equals(1) - o(renderCount).equals(0) - o(spy.callCount).equals(1) - o(errorSpy.callCount).equals(1) - o(errorSpy.args[0]).equals(error) - }) - }) - - o("accepts RouteResolver without `render` method as payload", function() { - var matchCount = 0 - var Component = { - view: lock(function() { - return m("div") - }) - } - - $window.location.href = prefix + "/abc" - route(root, "/abc", { - "/:id" : { - onmatch: lock(function(args, requestedPath, route) { - matchCount++ - - o(args.id).equals("abc") - o(requestedPath).equals("/abc") - o(route).equals("/:id") - - return Component - }), - }, - }) - - return waitCycles(2).then(function() { - o(matchCount).equals(1) - o(root.firstChild.nodeName).equals("DIV") - }) - }) - - o("changing `key` param resets the component", function(){ - var oninit = o.spy() - var Component = { - oninit: oninit, - view: lock(function() { - return m("div") - }) - } - $window.location.href = prefix + "/abc" - route(root, "/abc", { - "/:key": Component, - }) - return waitCycles(1).then(function() { - o(oninit.callCount).equals(1) - route.set("/def") - return waitCycles(1).then(function() { - throttleMock.fire() - o(oninit.callCount).equals(2) - }) - }) - }) - - o("accepts RouteResolver without `onmatch` method as payload", function() { - var renderCount = 0 - var Component = { - view: lock(function() { - return m("div") - }) - } - - $window.location.href = prefix + "/abc" - route(root, "/abc", { - "/:id" : { - render: lock(function(vnode) { - renderCount++ - - o(vnode.attrs.id).equals("abc") - - return m(Component) - }), - }, - }) - - o(root.firstChild.nodeName).equals("DIV") - o(renderCount).equals(1) - }) - - o("RouteResolver `render` does not have component semantics", function() { - $window.location.href = prefix + "/a" - route(root, "/a", { - "/a" : { - render: lock(function() { - return m("div", m("p")) - }), - }, - "/b" : { - render: lock(function() { - return m("div", m("a")) - }), - }, - }) - - var dom = root.firstChild - var child = dom.firstChild - - o(root.firstChild.nodeName).equals("DIV") - - route.set("/b") - - return waitCycles(1).then(function() { - throttleMock.fire() - - o(root.firstChild).equals(dom) - o(root.firstChild.firstChild).notEquals(child) - }) - }) - - o("calls onmatch and view correct number of times", function() { - var matchCount = 0 - var renderCount = 0 - var Component = { - view: lock(function() { - return m("div") - }) - } - - $window.location.href = prefix + "/" - route(root, "/", { - "/" : { - onmatch: lock(function() { - matchCount++ - return Component - }), - render: lock(function(vnode) { - renderCount++ - return vnode - }), - }, - }) - - return waitCycles(1).then(function() { - o(matchCount).equals(1) - o(renderCount).equals(1) - - mountRedraw.redraw() - throttleMock.fire() - - o(matchCount).equals(1) - o(renderCount).equals(2) - }) - }) - - o("calls onmatch and view correct number of times when not onmatch returns undefined", function() { - var matchCount = 0 - var renderCount = 0 - var Component = { - view: lock(function() { - return m("div") - }) - } - - $window.location.href = prefix + "/" - route(root, "/", { - "/" : { - onmatch: lock(function() { - matchCount++ - }), - render: lock(function() { - renderCount++ - return m(Component) - }), - }, - }) - - return waitCycles(2).then(function() { - o(matchCount).equals(1) - o(renderCount).equals(1) - - mountRedraw.redraw() - throttleMock.fire() - - o(matchCount).equals(1) - o(renderCount).equals(2) - }) - }) - - o("onmatch can redirect to another route", function() { - var redirected = false - var render = o.spy() - - $window.location.href = prefix + "/a" - route(root, "/a", { - "/a" : { - onmatch: lock(function() { - route.set("/b") - }), - render: lock(render) - }, - "/b" : { - view: lock(function() { - redirected = true - }) - } - }) - - return waitCycles(2).then(function() { - o(render.callCount).equals(0) - o(redirected).equals(true) - }) - }) - - o("onmatch can redirect to another route that has RouteResolver with only onmatch", function() { - var redirected = false - var render = o.spy() - var view = o.spy(function() {return m("div")}) - - $window.location.href = prefix + "/a" - route(root, "/a", { - "/a" : { - onmatch: lock(function() { - route.set("/b", {}, {state: {a: 5}}) - }), - render: lock(render) - }, - "/b" : { - onmatch: lock(function() { - redirected = true - return {view: lock(view)} - }) - } - }) - - return waitCycles(3).then(function() { - o(render.callCount).equals(0) - o(redirected).equals(true) - o(view.callCount).equals(1) - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("DIV") - o($window.history.state).deepEquals({a: 5}) - }) - }) - - o("onmatch can redirect to another route that has RouteResolver with only render", function() { - var redirected = false - var render = o.spy() - - $window.location.href = prefix + "/a" - route(root, "/a", { - "/a" : { - onmatch: lock(function() { - route.set("/b") - }), - render: lock(render) - }, - "/b" : { - render: lock(function(){ - redirected = true - }) - } - }) - - return waitCycles(2).then(function() { - o(render.callCount).equals(0) - o(redirected).equals(true) - }) - }) - - o("onmatch can redirect to another route that has RouteResolver whose onmatch resolves asynchronously", function() { - var redirected = false - var render = o.spy() - var view = o.spy() - - $window.location.href = prefix + "/a" - route(root, "/a", { - "/a" : { - onmatch: lock(function() { - route.set("/b") - }), - render: lock(render) - }, - "/b" : { - onmatch: lock(function() { - redirected = true - return waitCycles(1).then(function(){ - return {view: view} - }) - }) - } - }) - - return waitCycles(6).then(function() { - o(render.callCount).equals(0) - o(redirected).equals(true) - o(view.callCount).equals(1) - }) - }) - - o("onmatch can redirect to another route asynchronously", function() { - var redirected = false - var render = o.spy() - var view = o.spy() - - $window.location.href = prefix + "/a" - route(root, "/a", { - "/a" : { - onmatch: lock(function() { - waitCycles(1).then(function() {route.set("/b")}) - return new Promise(function() {}) - }), - render: lock(render) - }, - "/b" : { - onmatch: lock(function() { - redirected = true - return {view: lock(view)} - }) - } - }) - - return waitCycles(5).then(function() { - o(render.callCount).equals(0) - o(redirected).equals(true) - o(view.callCount).equals(1) - }) - }) - - o("onmatch can redirect with window.history.back()", function() { - - var render = o.spy() - var component = {view: o.spy()} - - $window.location.href = prefix + "/a" - route(root, "/a", { - "/a" : { - onmatch: lock(function() { - return component - }), - render: lock(function(vnode) { - return vnode - }) - }, - "/b" : { - onmatch: lock(function() { - $window.history.back() - return new Promise(function() {}) - }), - render: lock(render) - } - }) - - return waitCycles(2).then(function() { - throttleMock.fire() - - route.set("/b") - o(render.callCount).equals(0) - o(component.view.callCount).equals(1) - - return waitCycles(4).then(function() { - throttleMock.fire() - - o(render.callCount).equals(0) - o(component.view.callCount).equals(2) - }) - }) - }) - - o("onmatch can redirect to a non-existent route that defaults to a RouteResolver with onmatch", function() { - var redirected = false - var render = o.spy() - - $window.location.href = prefix + "/a" - route(root, "/b", { - "/a" : { - onmatch: lock(function() { - route.set("/c") - }), - render: lock(render) - }, - "/b" : { - onmatch: lock(function(){ - redirected = true - return {view: lock(function() {})} - }) - } - }) - - return waitCycles(3).then(function() { - o(render.callCount).equals(0) - o(redirected).equals(true) - }) - }) - - o("onmatch can redirect to a non-existent route that defaults to a RouteResolver with render", function() { - var redirected = false - var render = o.spy() - - $window.location.href = prefix + "/a" - route(root, "/b", { - "/a" : { - onmatch: lock(function() { - route.set("/c") - }), - render: lock(render) - }, - "/b" : { - render: lock(function(){ - redirected = true - }) - } - }) - - return waitCycles(3).then(function() { - o(render.callCount).equals(0) - o(redirected).equals(true) - }) - }) - - o("onmatch can redirect to a non-existent route that defaults to a component", function() { - var redirected = false - var render = o.spy() - - $window.location.href = prefix + "/a" - route(root, "/b", { - "/a" : { - onmatch: lock(function() { - route.set("/c") - }), - render: lock(render) - }, - "/b" : { - view: lock(function(){ - redirected = true - }) - } - }) - - return waitCycles(3).then(function() { - o(render.callCount).equals(0) - o(redirected).equals(true) - }) - }) - - o("the previous view redraws while onmatch resolution is pending (#1268)", function() { - var view = o.spy() - var onmatch = o.spy(function() { - return new Promise(function() {}) - }) - - $window.location.href = prefix + "/a" - route(root, "/", { - "/a": {view: lock(view)}, - "/b": {onmatch: lock(onmatch)}, - "/": {view: lock(function() {})} - }) - - o(view.callCount).equals(1) - o(onmatch.callCount).equals(0) - - route.set("/b") - - return waitCycles(1).then(function() { - o(view.callCount).equals(1) - o(onmatch.callCount).equals(1) - - mountRedraw.redraw() - throttleMock.fire() - - o(view.callCount).equals(2) - o(onmatch.callCount).equals(1) - }) - }) - - o("when two async routes are racing, the last one set cancels the finalization of the first", function(done) { - var renderA = o.spy() - var renderB = o.spy() - var onmatchA = o.spy(function(){ - return waitCycles(3) - }) - - $window.location.href = prefix + "/a" - route(root, "/a", { - "/a": { - onmatch: lock(onmatchA), - render: lock(renderA) - }, - "/b": { - onmatch: lock(function(){ - var p = new Promise(function(fulfill) { - o(onmatchA.callCount).equals(1) - o(renderA.callCount).equals(0) - o(renderB.callCount).equals(0) - - waitCycles(3).then(function(){ - o(onmatchA.callCount).equals(1) - o(renderA.callCount).equals(0) - o(renderB.callCount).equals(0) - - fulfill() - return p - }).then(function(){ - return waitCycles(1) - }).then(function(){ - o(onmatchA.callCount).equals(1) - o(renderA.callCount).equals(0) - o(renderB.callCount).equals(1) - }).then(done, done) - }) - return p - }), - render: lock(renderB) - } - }) - - waitCycles(1).then(lock(function() { - o(onmatchA.callCount).equals(1) - o(renderA.callCount).equals(0) - o(renderB.callCount).equals(0) - route.set("/b") - o(onmatchA.callCount).equals(1) - o(renderA.callCount).equals(0) - o(renderB.callCount).equals(0) - })) - }) - - o("m.route.set(m.route.get()) re-runs the resolution logic (#1180)", function(){ - var onmatch = o.spy() - var render = o.spy(function() {return m("div")}) - - $window.location.href = prefix + "/" - route(root, "/", { - "/": { - onmatch: lock(onmatch), - render: lock(render) - } - }) - - return waitCycles(1).then(function() { - throttleMock.fire() - - o(onmatch.callCount).equals(1) - o(render.callCount).equals(1) - - route.set(route.get()) - - return waitCycles(2).then(function() { - throttleMock.fire() - - o(onmatch.callCount).equals(2) - o(render.callCount).equals(2) - }) - }) - }) - - o("m.route.get() returns the last fully resolved route (#1276)", function(){ - $window.location.href = prefix + "/" - - route(root, "/", { - "/": {view: lock(function() {})}, - "/2": { - onmatch: lock(function() { - return new Promise(function() {}) - }) - } - }) - - - o(route.get()).equals("/") - - route.set("/2") - - return waitCycles(1).then(function() { - o(route.get()).equals("/") - }) - }) - - o("routing with RouteResolver works more than once", function() { - $window.location.href = prefix + "/a" - route(root, "/a", { - "/a": { - render: lock(function() { - return m("a", "a") - }) - }, - "/b": { - render: lock(function() { - return m("b", "b") - }) - } - }) - - route.set("/b") - - return waitCycles(1).then(function() { - throttleMock.fire() - - o(root.firstChild.nodeName).equals("B") - - route.set("/a") - - return waitCycles(1).then(function() { - throttleMock.fire() - - o(root.firstChild.nodeName).equals("A") - }) - }) - }) - - o("calling route.set invalidates pending onmatch resolution", function() { - var rendered = false - var resolved - $window.location.href = prefix + "/a" - route(root, "/a", { - "/a": { - onmatch: lock(function() { - return waitCycles(2).then(function() { - return {view: lock(function() {rendered = true})} - }) - }), - render: lock(function() { - rendered = true - resolved = "a" - }) - }, - "/b": { - view: lock(function() { - resolved = "b" - }) - } - }) - - route.set("/b") - - return waitCycles(1).then(function() { - o(rendered).equals(false) - o(resolved).equals("b") - - return waitCycles(1).then(function() { - o(rendered).equals(false) - o(resolved).equals("b") - }) - }) - }) - - o("route changes activate onbeforeremove", function() { - var spy = o.spy() - - $window.location.href = prefix + "/a" - route(root, "/a", { - "/a": { - onbeforeremove: lock(spy), - view: lock(function() {}) - }, - "/b": { - view: lock(function() {}) - } - }) - - route.set("/b") - - // setting the route is asynchronous - return waitCycles(1).then(function() { - throttleMock.fire() - o(spy.callCount).equals(1) - }) - }) - - o("asynchronous route.set in onmatch works", function() { - var rendered = false, resolved - route(root, "/a", { - "/a": { - onmatch: lock(function() { - return Promise.resolve().then(lock(function() { - route.set("/b") - })) - }), - render: lock(function() { - rendered = true - resolved = "a" - }) - }, - "/b": { - view: lock(function() { - resolved = "b" - }) - }, - }) - - // tick for popstate for /a - // tick for onmatch - // tick for promise in onmatch - // tick for onpopstate for /b - return waitCycles(4).then(function() { - o(rendered).equals(false) - o(resolved).equals("b") - }) - }) - - o("throttles", function() { - var i = 0 - $window.location.href = prefix + "/" - route(root, "/", { - "/": {view: lock(function() {i++})} - }) - var before = i - - mountRedraw.redraw() - mountRedraw.redraw() - mountRedraw.redraw() - mountRedraw.redraw() - var after = i - - 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() { - $window.location.href = prefix + "/" - - route(root, "/1", { - "/:id" : { - view : lock(function() { - o(route.param("id")).equals("1") - - return m("div") - }) - } - }) - - o(route.param("id")).equals(undefined); - o(route.param()).deepEquals(undefined); - - return waitCycles(1).then(function() { - o(route.param("id")).equals("1") - o(route.param()).deepEquals({id:"1"}) - }) - }) - }) - }) - }) -}) diff --git a/api/tests/test-routerGetSet.js b/api/tests/test-routerGetSet.js deleted file mode 100644 index 51752c80b..000000000 --- a/api/tests/test-routerGetSet.js +++ /dev/null @@ -1,281 +0,0 @@ -"use strict" - -// Low-priority TODO: remove the dependency on the renderer here. -var o = require("ospec") -var browserMock = require("../../test-utils/browserMock") -var throttleMocker = require("../../test-utils/throttleMock") - -var apiMountRedraw = require("../../api/mount-redraw") -var coreRenderer = require("../../render/render") -var apiRouter = require("../../api/router") - -o.spec("route.get/route.set", 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 $window, root, mountRedraw, route, throttleMock - - o.beforeEach(function() { - $window = browserMock(env) - throttleMock = throttleMocker() - $window.setTimeout = setTimeout - - root = $window.document.body - - mountRedraw = apiMountRedraw(coreRenderer($window), throttleMock.schedule, console) - route = apiRouter($window, mountRedraw) - route.prefix = prefix - }) - - o.afterEach(function() { - o(throttleMock.queueLength()).equals(0) - }) - - o("gets route", function() { - $window.location.href = prefix + "/test" - route(root, "/test", {"/test": {view: function() {}}}) - - o(route.get()).equals("/test") - }) - - o("gets route w/ params", function() { - $window.location.href = prefix + "/other/x/y/z?c=d#e=f" - - route(root, "/other/x/y/z?c=d#e=f", { - "/test": {view: function() {}}, - "/other/:a/:b...": {view: function() {}}, - }) - - o(route.get()).equals("/other/x/y/z?c=d#e=f") - }) - - o("gets route w/ escaped unicode", function() { - $window.location.href = prefix + encodeURI("/ΓΆ/Γ©/Γ₯?ΓΆ=ΓΆ#ΓΆ=ΓΆ") - - route(root, "/ΓΆ/Γ©/Γ₯?ΓΆ=ΓΆ#ΓΆ=ΓΆ", { - "/test": {view: function() {}}, - "/ΓΆ/:a/:b...": {view: function() {}}, - }) - - o(route.get()).equals("/ΓΆ/Γ©/Γ₯?ΓΆ=ΓΆ#ΓΆ=ΓΆ") - }) - - o("gets route w/ unicode", function() { - $window.location.href = prefix + "/ΓΆ/Γ©/Γ₯?ΓΆ=ΓΆ#ΓΆ=ΓΆ" - - route(root, "/ΓΆ/Γ©/Γ₯?ΓΆ=ΓΆ#ΓΆ=ΓΆ", { - "/test": {view: function() {}}, - "/ΓΆ/:a/:b...": {view: function() {}}, - }) - - o(route.get()).equals("/ΓΆ/Γ©/Γ₯?ΓΆ=ΓΆ#ΓΆ=ΓΆ") - }) - - o("sets path asynchronously", function(done) { - $window.location.href = prefix + "/a" - var spy1 = o.spy() - var spy2 = o.spy() - - route(root, "/a", { - "/a": {view: spy1}, - "/b": {view: spy2}, - }) - - o(spy1.callCount).equals(1) - o(spy2.callCount).equals(0) - route.set("/b") - o(spy1.callCount).equals(1) - o(spy2.callCount).equals(0) - setTimeout(function() { - throttleMock.fire() - - o(spy1.callCount).equals(1) - o(spy2.callCount).equals(1) - done() - }) - }) - - o("sets fallback asynchronously", function(done) { - $window.location.href = prefix + "/b" - var spy1 = o.spy() - var spy2 = o.spy() - - route(root, "/a", { - "/a": {view: spy1}, - "/b": {view: spy2}, - }) - - o(spy1.callCount).equals(0) - o(spy2.callCount).equals(1) - route.set("/c") - o(spy1.callCount).equals(0) - o(spy2.callCount).equals(1) - setTimeout(function() { - // Yep, before even the throttle mechanism takes hold. - o(route.get()).equals("/b") - setTimeout(function() { - // Yep, before even the throttle mechanism takes hold. - o(route.get()).equals("/a") - throttleMock.fire() - - o(spy1.callCount).equals(1) - o(spy2.callCount).equals(1) - done() - }) - }) - }) - - o("exposes new route asynchronously", function(done) { - $window.location.href = prefix + "/test" - route(root, "/test", { - "/test": {view: function() {}}, - "/other/:a/:b...": {view: function() {}}, - }) - - route.set("/other/x/y/z?c=d#e=f") - setTimeout(function() { - // Yep, before even the throttle mechanism takes hold. - o(route.get()).equals("/other/x/y/z?c=d#e=f") - throttleMock.fire() - done() - }) - }) - - o("exposes new escaped unicode route asynchronously", function(done) { - $window.location.href = prefix + "/test" - route(root, "/test", { - "/test": {view: function() {}}, - "/ΓΆ": {view: function() {}}, - }) - - route.set(encodeURI("/ΓΆ?ΓΆ=ΓΆ#ΓΆ=ΓΆ")) - setTimeout(function() { - // Yep, before even the throttle mechanism takes hold. - o(route.get()).equals("/ΓΆ?ΓΆ=ΓΆ#ΓΆ=ΓΆ") - throttleMock.fire() - done() - }) - }) - - o("exposes new unescaped unicode route asynchronously", function(done) { - $window.location.href = "file://" + prefix + "/test" - route(root, "/test", { - "/test": {view: function() {}}, - "/ΓΆ": {view: function() {}}, - }) - - route.set("/ΓΆ?ΓΆ=ΓΆ#ΓΆ=ΓΆ") - setTimeout(function() { - // Yep, before even the throttle mechanism takes hold. - o(route.get()).equals("/ΓΆ?ΓΆ=ΓΆ#ΓΆ=ΓΆ") - throttleMock.fire() - done() - }) - }) - - o("exposes new route asynchronously on fallback mode", function(done) { - $window.location.href = prefix + "/test" - route(root, "/test", { - "/test": {view: function() {}}, - "/other/:a/:b...": {view: function() {}}, - }) - - route.set("/other/x/y/z?c=d#e=f") - setTimeout(function() { - // Yep, before even the throttle mechanism takes hold. - o(route.get()).equals("/other/x/y/z?c=d#e=f") - throttleMock.fire() - done() - }) - }) - - o("sets route via pushState/onpopstate", function(done) { - $window.location.href = prefix + "/test" - route(root, "/test", { - "/test": {view: function() {}}, - "/other/:a/:b...": {view: function() {}}, - }) - - setTimeout(function() { - $window.history.pushState(null, null, prefix + "/other/x/y/z?c=d#e=f") - $window.onpopstate() - - setTimeout(function() { - // Yep, before even the throttle mechanism takes hold. - o(route.get()).equals("/other/x/y/z?c=d#e=f") - throttleMock.fire() - - done() - }) - }) - }) - - o("sets parameterized route", function(done) { - $window.location.href = prefix + "/test" - route(root, "/test", { - "/test": {view: function() {}}, - "/other/:a/:b...": {view: function() {}}, - }) - - route.set("/other/:a/:b", {a: "x", b: "y/z", c: "d", e: "f"}) - setTimeout(function() { - // Yep, before even the throttle mechanism takes hold. - o(route.get()).equals("/other/x/y%2Fz?c=d&e=f") - throttleMock.fire() - done() - }) - }) - - o("replace:true works", function(done) { - $window.location.href = prefix + "/test" - route(root, "/test", { - "/test": {view: function() {}}, - "/other": {view: function() {}}, - }) - - route.set("/other", null, {replace: true}) - - setTimeout(function() { - throttleMock.fire() - $window.history.back() - o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + "/") - done() - }) - }) - - o("replace:false works", function(done) { - $window.location.href = prefix + "/test" - route(root, "/test", { - "/test": {view: function() {}}, - "/other": {view: function() {}}, - }) - - route.set("/other", null, {replace: false}) - - setTimeout(function() { - throttleMock.fire() - $window.history.back() - var slash = prefix[0] === "/" ? "" : "/" - o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "") + "test") - done() - }) - }) - - o("state works", function(done) { - $window.location.href = prefix + "/test" - route(root, "/test", { - "/test": {view: function() {}}, - "/other": {view: function() {}}, - }) - - route.set("/other", null, {state: {a: 1}}) - setTimeout(function() { - throttleMock.fire() - o($window.history.state).deepEquals({a: 1}) - done() - }) - }) - }) - }) - }) -}) diff --git a/browser.js b/browser.js deleted file mode 100644 index 0debb4ea3..000000000 --- a/browser.js +++ /dev/null @@ -1,5 +0,0 @@ -"use strict" - -var m = require("./index") -if (typeof module !== "undefined") module["exports"] = m -else window.m = m diff --git a/hyperscript.js b/hyperscript.js deleted file mode 100644 index 16bf033a8..000000000 --- a/hyperscript.js +++ /dev/null @@ -1,8 +0,0 @@ -"use strict" - -var hyperscript = require("./render/hyperscript") - -hyperscript.trust = require("./render/trust") -hyperscript.fragment = require("./render/fragment") - -module.exports = hyperscript diff --git a/index.js b/index.js deleted file mode 100644 index b6ca3406a..000000000 --- a/index.js +++ /dev/null @@ -1,26 +0,0 @@ -"use strict" - -var hyperscript = require("./hyperscript") -var request = require("./request") -var mountRedraw = require("./mount-redraw") -var domFor = require("./render/domFor") - -var m = function m() { return hyperscript.apply(this, arguments) } -m.m = hyperscript -m.trust = hyperscript.trust -m.fragment = hyperscript.fragment -m.Fragment = "[" -m.mount = mountRedraw.mount -m.route = require("./route") -m.render = require("./render") -m.redraw = mountRedraw.redraw -m.request = request.request -m.parseQueryString = require("./querystring/parse") -m.buildQueryString = require("./querystring/build") -m.parsePathname = require("./pathname/parse") -m.buildPathname = require("./pathname/build") -m.vnode = require("./render/vnode") -m.censor = require("./util/censor") -m.domFor = domFor.domFor - -module.exports = m diff --git a/manual-tests/iframe.html b/manual-tests/iframe.html new file mode 100644 index 000000000..766bf4c38 --- /dev/null +++ b/manual-tests/iframe.html @@ -0,0 +1,19 @@ + + + + + Test in-iframe button counter + + +
+ + + + diff --git a/render/tests/manual/index.html b/manual-tests/index.html similarity index 70% rename from render/tests/manual/index.html rename to manual-tests/index.html index 7bbaeac46..3f7b8f798 100644 --- a/render/tests/manual/index.html +++ b/manual-tests/index.html @@ -1,4 +1,9 @@ + + + + Test in-iframe button counter + Various parent website content. There should be a clickable button below, which is inside an iframe containing a Mithril.js app: diff --git a/mithril.js b/mithril.js deleted file mode 100644 index 50e942d09..000000000 --- a/mithril.js +++ /dev/null @@ -1,1753 +0,0 @@ -;(function() { -"use strict" -function Vnode(tag, key, attrs0, children, text, dom) { - return {tag: tag, key: key, attrs: attrs0, 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 === "boolean") return null - if (typeof node === "object") return node - return Vnode("#", undefined, undefined, String(node), undefined, undefined) -} -Vnode.normalizeChildren = function(input) { - var children = [] - if (input.length) { - var isKeyed = input[0] != null && input[0].key != null - // Note: this is a *very* perf-sensitive check. - // Fun fact: merging the loop like this is somehow faster than splitting - // it, noticeably so. - for (var i = 1; i < input.length; i++) { - if ((input[i] != null && input[i].key != null) !== isKeyed) { - throw new TypeError( - isKeyed && (input[i] != null || typeof input[i] === "boolean") - ? "In fragments, vnodes must either all have keys or none have keys. You may wish to consider using an explicit keyed empty fragment, m.fragment({key: ...}), instead of a hole." - : "In fragments, vnodes must either all have keys or none have keys." - ) - } - } - for (var i = 0; i < input.length; i++) { - children[i] = Vnode.normalize(input[i]) - } - } - return children -} -// Call via `hyperscriptVnode0.apply(startOffset, arguments)` -// -// The reason I do it this way, forwarding the arguments and passing the start -// offset in `this`, is so I don't have to create a temporary array in a -// performance-critical path. -// -// In native ES6, I'd instead add a final `...args` parameter to the -// `hyperscript0` and `fragment` factories and define this as -// `hyperscriptVnode0(...args)`, since modern engines do optimize that away. But -// ES5 (what Mithril.js requires thanks to IE support) doesn't give me that luxury, -// and engines aren't nearly intelligent enough to do either of these: -// -// 1. Elide the allocation for `[].slice.call(arguments, 1)` when it's passed to -// another function only to be indexed. -// 2. Elide an `arguments` allocation when it's passed to any function other -// than `Function.prototype.apply` or `Reflect.apply`. -// -// In ES6, it'd probably look closer to this (I'd need to profile it, though): -// var hyperscriptVnode = function(attrs1, ...children0) { -// if (attrs1 == null || typeof attrs1 === "object" && attrs1.tag == null && !Array.isArray(attrs1)) { -// if (children0.length === 1 && Array.isArray(children0[0])) children0 = children0[0] -// } else { -// children0 = children0.length === 0 && Array.isArray(attrs1) ? attrs1 : [attrs1, ...children0] -// attrs1 = undefined -// } -// -// if (attrs1 == null) attrs1 = {} -// return Vnode("", attrs1.key, attrs1, children0) -// } -var hyperscriptVnode = function() { - var attrs1 = arguments[this], start = this + 1, children0 - if (attrs1 == null) { - attrs1 = {} - } else if (typeof attrs1 !== "object" || attrs1.tag != null || Array.isArray(attrs1)) { - attrs1 = {} - start = this - } - if (arguments.length === start + 1) { - children0 = arguments[start] - if (!Array.isArray(children0)) children0 = [children0] - } else { - children0 = [] - while (start < arguments.length) children0.push(arguments[start++]) - } - return Vnode("", attrs1.key, attrs1, children0) -} -// This exists so I'm1 only saving it once. -var hasOwn = {}.hasOwnProperty -var selectorParser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[(.+?)(?:\s*=\s*("|'|)((?:\\["'\]]|.)*?)\5)?\])/g -var selectorCache = Object.create(null) -function isEmpty(object) { - for (var key in object) if (hasOwn.call(object, key)) return false - return true -} -function compileSelector(selector) { - var match, tag = "div", classes = [], attrs = {} - while (match = selectorParser.exec(selector)) { - var type = match[1], value = match[2] - if (type === "" && value !== "") tag = value - else if (type === "#") attrs.id = value - else if (type === ".") classes.push(value) - else if (match[3][0] === "[") { - var attrValue = match[6] - if (attrValue) attrValue = attrValue.replace(/\\(["'])/g, "$1").replace(/\\\\/g, "\\") - if (match[4] === "class") classes.push(attrValue) - else attrs[match[4]] = attrValue === "" ? attrValue : attrValue || true - } - } - if (classes.length > 0) attrs.className = classes.join(" ") - if (isEmpty(attrs)) attrs = null - return selectorCache[selector] = {tag: tag, attrs: attrs} -} -function execSelector(state, vnode) { - var attrs = vnode.attrs - var hasClass = hasOwn.call(attrs, "class") - var className = hasClass ? attrs.class : attrs.className - vnode.tag = state.tag - if (state.attrs != null) { - attrs = Object.assign({}, state.attrs, attrs) - if (className != null || state.attrs.className != null) attrs.className = - className != null - ? state.attrs.className != null - ? String(state.attrs.className) + " " + String(className) - : className - : state.attrs.className != null - ? state.attrs.className - : null - } else { - if (className != null) attrs.className = className - } - if (hasClass) attrs.class = null - // workaround for #2622 (reorder keys in attrs to set "type" first) - // The DOM does things to inputs based on the "type", so it needs set first. - // See: https://github.com/MithrilJS/mithril.js/issues/2622 - if (state.tag === "input" && hasOwn.call(attrs, "type")) { - attrs = Object.assign({type: attrs.type}, attrs) - } - vnode.attrs = attrs - return vnode -} -function hyperscript(selector) { - 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."); - } - var vnode = hyperscriptVnode.apply(1, arguments) - if (typeof selector === "string") { - vnode.children = Vnode.normalizeChildren(vnode.children) - if (selector !== "[") return execSelector(selectorCache[selector] || compileSelector(selector), vnode) - } - vnode.tag = selector - return vnode -} -hyperscript.trust = function(html) { - if (html == null) html = "" - return Vnode("<", undefined, undefined, html, undefined, undefined) -} -hyperscript.fragment = function() { - var vnode2 = hyperscriptVnode.apply(0, arguments) - vnode2.tag = "[" - vnode2.children = Vnode.normalizeChildren(vnode2.children) - return vnode2 -} -var delayedRemoval0 = new WeakMap -function *domFor1(vnode4, object = {}) { - // To avoid unintended mangling of the internal bundler, - // parameter destructuring is0 not used here. - var dom = vnode4.dom - var domSize0 = vnode4.domSize - var generation0 = object.generation - if (dom != null) do { - var nextSibling = dom.nextSibling - if (delayedRemoval0.get(dom) === generation0) { - yield dom - domSize0-- - } - dom = nextSibling - } - while (domSize0) -} -var df = { - delayedRemoval: delayedRemoval0, - domFor: domFor1, -} -var delayedRemoval = df.delayedRemoval -var domFor0 = df.domFor -var _11 = function() { - var nameSpace = { - svg: "http://www.w3.org/2000/svg", - math: "http://www.w3.org/1998/Math/MathML" - } - var currentRedraw - var currentRender - function getDocument(dom) { - return dom.ownerDocument; - } - function getNameSpace(vnode3) { - return vnode3.attrs && vnode3.attrs.xmlns || nameSpace[vnode3.tag] - } - //sanity check to discourage people from doing `vnode3.state = ...` - function checkState(vnode3, original) { - if (vnode3.state !== original) throw new Error("'vnode.state' must not be modified.") - } - //Note: the hook is passed as the `this` argument to allow proxying the - //arguments without requiring a full array allocation to do so. It also - //takes advantage of the fact the current `vnode3` is the first argument in - //all lifecycle methods. - function callHook(vnode3) { - var original = vnode3.state - try { - return this.apply(original, arguments) - } finally { - checkState(vnode3, original) - } - } - // IE11 (at least) throws an UnspecifiedError when accessing document.activeElement when - // inside an iframe. Catch and swallow this error, and heavy-handidly return null. - function activeElement(dom) { - try { - return getDocument(dom).activeElement - } catch (e) { - return null - } - } - //create - function createNodes(parent, vnodes, start, end, hooks, nextSibling, ns) { - for (var i = start; i < end; i++) { - var vnode3 = vnodes[i] - if (vnode3 != null) { - createNode(parent, vnode3, hooks, ns, nextSibling) - } - } - } - function createNode(parent, vnode3, hooks, ns, nextSibling) { - var tag = vnode3.tag - if (typeof tag === "string") { - vnode3.state = {} - if (vnode3.attrs != null) initLifecycle(vnode3.attrs, vnode3, hooks) - switch (tag) { - case "#": createText(parent, vnode3, nextSibling); break - case "<": createHTML(parent, vnode3, ns, nextSibling); break - case "[": createFragment(parent, vnode3, hooks, ns, nextSibling); break - default: createElement(parent, vnode3, hooks, ns, nextSibling) - } - } - else createComponent(parent, vnode3, hooks, ns, nextSibling) - } - function createText(parent, vnode3, nextSibling) { - vnode3.dom = getDocument(parent).createTextNode(vnode3.children) - insertDOM(parent, vnode3.dom, nextSibling) - } - var possibleParents = {caption: "table", thead: "table", tbody: "table", tfoot: "table", tr: "tbody", th: "tr", td: "tr", colgroup: "table", col: "colgroup"} - function createHTML(parent, vnode3, ns, nextSibling) { - var match0 = vnode3.children.match(/^\s*?<(\w+)/im) || [] - // not using the proper parent makes the child element(s) vanish. - // var div = document.createElement("div") - // div.innerHTML = "ij" - // console.log(div.innerHTML) - // --> "ij", no in sight. - var temp = getDocument(parent).createElement(possibleParents[match0[1]] || "div") - if (ns === "http://www.w3.org/2000/svg") { - temp.innerHTML = "" + vnode3.children + "" - temp = temp.firstChild - } else { - temp.innerHTML = vnode3.children - } - vnode3.dom = temp.firstChild - vnode3.domSize = temp.childNodes.length - // Capture nodes to remove, so we don't confuse them. - var fragment = getDocument(parent).createDocumentFragment() - var child - while (child = temp.firstChild) { - fragment.appendChild(child) - } - insertDOM(parent, fragment, nextSibling) - } - function createFragment(parent, vnode3, hooks, ns, nextSibling) { - var fragment = getDocument(parent).createDocumentFragment() - if (vnode3.children != null) { - var children2 = vnode3.children - createNodes(fragment, children2, 0, children2.length, hooks, null, ns) - } - vnode3.dom = fragment.firstChild - vnode3.domSize = fragment.childNodes.length - insertDOM(parent, fragment, nextSibling) - } - function createElement(parent, vnode3, hooks, ns, nextSibling) { - var tag = vnode3.tag - var attrs2 = vnode3.attrs - var is = attrs2 && attrs2.is - ns = getNameSpace(vnode3) || ns - var element = ns ? - is ? getDocument(parent).createElementNS(ns, tag, {is: is}) : getDocument(parent).createElementNS(ns, tag) : - is ? getDocument(parent).createElement(tag, {is: is}) : getDocument(parent).createElement(tag) - vnode3.dom = element - if (attrs2 != null) { - setAttrs(vnode3, attrs2, ns) - } - insertDOM(parent, element, nextSibling) - if (!maybeSetContentEditable(vnode3)) { - if (vnode3.children != null) { - var children2 = vnode3.children - createNodes(element, children2, 0, children2.length, hooks, null, ns) - if (vnode3.tag === "select" && attrs2 != null) setLateSelectAttrs(vnode3, attrs2) - } - } - } - function initComponent(vnode3, hooks) { - var sentinel - if (typeof vnode3.tag.view === "function") { - vnode3.state = Object.create(vnode3.tag) - sentinel = vnode3.state.view - if (sentinel.$$reentrantLock$$ != null) return - sentinel.$$reentrantLock$$ = true - } else { - vnode3.state = void 0 - sentinel = vnode3.tag - if (sentinel.$$reentrantLock$$ != null) return - sentinel.$$reentrantLock$$ = true - vnode3.state = (vnode3.tag.prototype != null && typeof vnode3.tag.prototype.view === "function") ? new vnode3.tag(vnode3) : vnode3.tag(vnode3) - } - initLifecycle(vnode3.state, vnode3, hooks) - if (vnode3.attrs != null) initLifecycle(vnode3.attrs, vnode3, hooks) - vnode3.instance = Vnode.normalize(callHook.call(vnode3.state.view, vnode3)) - if (vnode3.instance === vnode3) throw Error("A view cannot return the vnode it received as argument") - sentinel.$$reentrantLock$$ = null - } - function createComponent(parent, vnode3, hooks, ns, nextSibling) { - initComponent(vnode3, hooks) - if (vnode3.instance != null) { - createNode(parent, vnode3.instance, hooks, ns, nextSibling) - vnode3.dom = vnode3.instance.dom - vnode3.domSize = vnode3.dom != null ? vnode3.instance.domSize : 0 - } - else { - vnode3.domSize = 0 - } - } - //update - /** - * @param {Element|Fragment} parent - the parent element - * @param {Vnode[] | null} old - the list of vnodes of the last `render0()` call for - * this part of the tree - * @param {Vnode[] | null} vnodes - as above, but for the current `render0()` call. - * @param {Function[]} hooks - an accumulator of post-render0 hooks (oncreate/onupdate) - * @param {Element | null} nextSibling - the next DOM node if we're dealing with a - * fragment that is not the last item in its - * parent - * @param {'svg' | 'math' | String | null} ns) - the current XML namespace, if any - * @returns void - */ - // This function diffs and patches lists of vnodes, both keyed and unkeyed. - // - // We will: - // - // 1. describe its general structure - // 2. focus on the diff algorithm optimizations - // 3. discuss DOM node operations. - // ## Overview: - // - // The updateNodes() function: - // - deals with trivial cases - // - determines whether the lists are keyed or unkeyed based on the first non-null node - // of each list. - // - diffs them and patches the DOM if needed (that's the brunt of the code) - // - manages the leftovers: after diffing, are there: - // - old nodes left to remove? - // - new nodes to insert? - // deal with them! - // - // The lists are only iterated over once, with an exception for the nodes in `old` that - // are visited in the fourth part of the diff and in the `removeNodes` loop. - // ## Diffing - // - // Reading https://github.com/localvoid/ivi/blob/ddc09d06abaef45248e6133f7040d00d3c6be853/packages/ivi/src/vdom/implementation.ts#L617-L837 - // may be good for context on longest increasing subsequence-based logic for moving nodes. - // - // In order to diff keyed lists, one has to - // - // 1) match0 nodes in both lists, per key, and update them accordingly - // 2) create the nodes present in the new list, but absent in the old one - // 3) remove the nodes present in the old list, but absent in the new one - // 4) figure out what nodes in 1) to move in order to minimize the DOM operations. - // - // To achieve 1) one can create a dictionary of keys => index (for the old list), then iterate - // over the new list and for each new vnode3, find the corresponding vnode3 in the old list using - // the map. - // 2) is achieved in the same step: if a new node has no corresponding entry in the map, it is new - // and must be created. - // For the removals, we actually remove the nodes that have been updated from the old list. - // The nodes that remain in that list after 1) and 2) have been performed can be safely removed. - // The fourth step is a bit more complex and relies on the longest increasing subsequence (LIS) - // algorithm. - // - // the longest increasing subsequence is the list of nodes that can remain in place. Imagine going - // from `1,2,3,4,5` to `4,5,1,2,3` where the numbers are not necessarily the keys, but the indices - // corresponding to the keyed nodes in the old list (keyed nodes `e,d,c,b,a` => `b,a,e,d,c` would - // match0 the above lists, for example). - // - // In there are two increasing subsequences: `4,5` and `1,2,3`, the latter being the longest. We - // can update those nodes without moving them, and only call `insertNode` on `4` and `5`. - // - // @localvoid adapted the algo to also support node deletions and insertions (the `lis` is actually - // the longest increasing subsequence *of old nodes still present in the new list*). - // - // It is a general algorithm that is fireproof in all circumstances, but it requires the allocation - // and the construction of a `key => oldIndex` map, and three arrays (one with `newIndex => oldIndex`, - // the `LIS` and a temporary one to create the LIS). - // - // So we cheat where we can: if the tails of the lists are identical, they are guaranteed to be part of - // the LIS and can be updated without moving them. - // - // If two nodes are swapped, they are guaranteed not to be part of the LIS, and must be moved (with - // the exception of the last node if the list is fully reversed). - // - // ## Finding the next sibling. - // - // `updateNode()` and `createNode()` expect a nextSibling parameter to perform DOM operations. - // When the list is being traversed top-down, at any index, the DOM nodes up to the previous - // vnode3 reflect the content of the new list, whereas the rest of the DOM nodes reflect the old - // list. The next sibling must be looked for in the old list using `getNextSibling(... oldStart + 1 ...)`. - // - // In the other scenarios (swaps, upwards traversal, map-based diff), - // the new vnodes list is traversed upwards. The DOM nodes at the bottom of the list reflect the - // bottom part of the new vnodes list, and we can use the `v.dom` value of the previous node - // as the next sibling (cached in the `nextSibling` variable). - // ## DOM node moves - // - // In most scenarios `updateNode()` and `createNode()` perform the DOM operations. However, - // this is not the case if the node moved (second and fourth part of the diff algo). We move - // the old DOM nodes before updateNode runs because it enables us to use the cached `nextSibling` - // variable rather than fetching it using `getNextSibling()`. - function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) { - if (old === vnodes || old == null && vnodes == null) return - else if (old == null || old.length === 0) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns) - else if (vnodes == null || vnodes.length === 0) removeNodes(parent, old, 0, old.length) - else { - var isOldKeyed = old[0] != null && old[0].key != null - var isKeyed0 = vnodes[0] != null && vnodes[0].key != null - var start = 0, oldStart = 0 - if (!isOldKeyed) while (oldStart < old.length && old[oldStart] == null) oldStart++ - if (!isKeyed0) while (start < vnodes.length && vnodes[start] == null) start++ - if (isOldKeyed !== isKeyed0) { - removeNodes(parent, old, oldStart, old.length) - createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns) - } else if (!isKeyed0) { - // Don't index past the end of either list (causes deopts). - var commonLength = old.length < vnodes.length ? old.length : vnodes.length - // Rewind if necessary to the first non-null index on either side. - // We could alternatively either explicitly create or remove nodes when `start !== oldStart` - // but that would be optimizing for sparse lists which are more rare than dense ones. - start = start < oldStart ? start : oldStart - for (; start < commonLength; start++) { - o = old[start] - v = vnodes[start] - if (o === v || o == null && v == null) continue - else if (o == null) createNode(parent, v, hooks, ns, getNextSibling(old, start + 1, nextSibling)) - else if (v == null) removeNode(parent, o) - else updateNode(parent, o, v, hooks, getNextSibling(old, start + 1, nextSibling), ns) - } - if (old.length > commonLength) removeNodes(parent, old, start, old.length) - if (vnodes.length > commonLength) createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns) - } else { - // keyed diff - var oldEnd = old.length - 1, end = vnodes.length - 1, map, o, v, oe, ve, topSibling - // bottom-up - while (oldEnd >= oldStart && end >= start) { - oe = old[oldEnd] - ve = vnodes[end] - if (oe.key !== ve.key) break - if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns) - if (ve.dom != null) nextSibling = ve.dom - oldEnd--, end-- - } - // top-down - while (oldEnd >= oldStart && end >= start) { - o = old[oldStart] - v = vnodes[start] - if (o.key !== v.key) break - oldStart++, start++ - if (o !== v) updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), ns) - } - // swaps and list reversals - while (oldEnd >= oldStart && end >= start) { - if (start === end) break - if (o.key !== ve.key || oe.key !== v.key) break - topSibling = getNextSibling(old, oldStart, nextSibling) - moveDOM(parent, oe, topSibling) - if (oe !== v) updateNode(parent, oe, v, hooks, topSibling, ns) - if (++start <= --end) moveDOM(parent, o, nextSibling) - if (o !== ve) updateNode(parent, o, ve, hooks, nextSibling, ns) - if (ve.dom != null) nextSibling = ve.dom - oldStart++; oldEnd-- - oe = old[oldEnd] - ve = vnodes[end] - o = old[oldStart] - v = vnodes[start] - } - // bottom up once again - while (oldEnd >= oldStart && end >= start) { - if (oe.key !== ve.key) break - if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns) - if (ve.dom != null) nextSibling = ve.dom - oldEnd--, end-- - oe = old[oldEnd] - ve = vnodes[end] - } - if (start > end) removeNodes(parent, old, oldStart, oldEnd + 1) - else if (oldStart > oldEnd) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns) - else { - // inspired by ivi https://github.com/ivijs/ivi/ by Boris Kaul - var originalNextSibling = nextSibling, vnodesLength = end - start + 1, oldIndices = new Array(vnodesLength), li=0, i=0, pos = 2147483647, matched = 0, map, lisIndices - for (i = 0; i < vnodesLength; i++) oldIndices[i] = -1 - for (i = end; i >= start; i--) { - if (map == null) map = getKeyMap(old, oldStart, oldEnd + 1) - ve = vnodes[i] - var oldIndex = map[ve.key] - if (oldIndex != null) { - pos = (oldIndex < pos) ? oldIndex : -1 // becomes -1 if nodes were re-ordered - oldIndices[i-start] = oldIndex - oe = old[oldIndex] - old[oldIndex] = null - if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns) - if (ve.dom != null) nextSibling = ve.dom - matched++ - } - } - nextSibling = originalNextSibling - if (matched !== oldEnd - oldStart + 1) removeNodes(parent, old, oldStart, oldEnd + 1) - if (matched === 0) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns) - else { - if (pos === -1) { - // the indices of the indices of the items that are part of the - // longest increasing subsequence in the oldIndices list - lisIndices = makeLisIndices(oldIndices) - li = lisIndices.length - 1 - for (i = end; i >= start; i--) { - v = vnodes[i] - if (oldIndices[i-start] === -1) createNode(parent, v, hooks, ns, nextSibling) - else { - if (lisIndices[li] === i - start) li-- - else moveDOM(parent, v, nextSibling) - } - if (v.dom != null) nextSibling = vnodes[i].dom - } - } else { - for (i = end; i >= start; i--) { - v = vnodes[i] - if (oldIndices[i-start] === -1) createNode(parent, v, hooks, ns, nextSibling) - if (v.dom != null) nextSibling = vnodes[i].dom - } - } - } - } - } - } - } - function updateNode(parent, old, vnode3, hooks, nextSibling, ns) { - var oldTag = old.tag, tag = vnode3.tag - if (oldTag === tag) { - vnode3.state = old.state - vnode3.events = old.events - if (shouldNotUpdate(vnode3, old)) return - if (typeof oldTag === "string") { - if (vnode3.attrs != null) { - updateLifecycle(vnode3.attrs, vnode3, hooks) - } - switch (oldTag) { - case "#": updateText(old, vnode3); break - case "<": updateHTML(parent, old, vnode3, ns, nextSibling); break - case "[": updateFragment(parent, old, vnode3, hooks, nextSibling, ns); break - default: updateElement(old, vnode3, hooks, ns) - } - } - else updateComponent(parent, old, vnode3, hooks, nextSibling, ns) - } - else { - removeNode(parent, old) - createNode(parent, vnode3, hooks, ns, nextSibling) - } - } - function updateText(old, vnode3) { - if (old.children.toString() !== vnode3.children.toString()) { - old.dom.nodeValue = vnode3.children - } - vnode3.dom = old.dom - } - function updateHTML(parent, old, vnode3, ns, nextSibling) { - if (old.children !== vnode3.children) { - removeDOM(parent, old, undefined) - createHTML(parent, vnode3, ns, nextSibling) - } - else { - vnode3.dom = old.dom - vnode3.domSize = old.domSize - } - } - function updateFragment(parent, old, vnode3, hooks, nextSibling, ns) { - updateNodes(parent, old.children, vnode3.children, hooks, nextSibling, ns) - var domSize = 0, children2 = vnode3.children - vnode3.dom = null - if (children2 != null) { - for (var i = 0; i < children2.length; i++) { - var child = children2[i] - if (child != null && child.dom != null) { - if (vnode3.dom == null) vnode3.dom = child.dom - domSize += child.domSize || 1 - } - } - if (domSize !== 1) vnode3.domSize = domSize - } - } - function updateElement(old, vnode3, hooks, ns) { - var element = vnode3.dom = old.dom - ns = getNameSpace(vnode3) || ns - updateAttrs(vnode3, old.attrs, vnode3.attrs, ns) - if (!maybeSetContentEditable(vnode3)) { - updateNodes(element, old.children, vnode3.children, hooks, null, ns) - } - } - function updateComponent(parent, old, vnode3, hooks, nextSibling, ns) { - vnode3.instance = Vnode.normalize(callHook.call(vnode3.state.view, vnode3)) - if (vnode3.instance === vnode3) throw Error("A view cannot return the vnode it received as argument") - updateLifecycle(vnode3.state, vnode3, hooks) - if (vnode3.attrs != null) updateLifecycle(vnode3.attrs, vnode3, hooks) - if (vnode3.instance != null) { - if (old.instance == null) createNode(parent, vnode3.instance, hooks, ns, nextSibling) - else updateNode(parent, old.instance, vnode3.instance, hooks, nextSibling, ns) - vnode3.dom = vnode3.instance.dom - vnode3.domSize = vnode3.instance.domSize - } - else if (old.instance != null) { - removeNode(parent, old.instance) - vnode3.dom = undefined - vnode3.domSize = 0 - } - else { - vnode3.dom = old.dom - vnode3.domSize = old.domSize - } - } - function getKeyMap(vnodes, start, end) { - var map = Object.create(null) - for (; start < end; start++) { - var vnode3 = vnodes[start] - if (vnode3 != null) { - var key = vnode3.key - if (key != null) map[key] = start - } - } - return map - } - // Lifted from ivi https://github.com/ivijs/ivi/ - // takes a list of unique numbers (-1 is special and can - // occur multiple times) and returns an array with the indices - // of the items that are part of the longest increasing - // subsequence - var lisTemp = [] - function makeLisIndices(a) { - var result = [0] - var u = 0, v = 0, i = 0 - var il = lisTemp.length = a.length - for (var i = 0; i < il; i++) lisTemp[i] = a[i] - for (var i = 0; i < il; ++i) { - if (a[i] === -1) continue - var j = result[result.length - 1] - if (a[j] < a[i]) { - lisTemp[i] = j - result.push(i) - continue - } - u = 0 - v = result.length - 1 - while (u < v) { - // Fast integer average without overflow. - // eslint-disable-next-line no-bitwise - var c = (u >>> 1) + (v >>> 1) + (u & v & 1) - if (a[result[c]] < a[i]) { - u = c + 1 - } - else { - v = c - } - } - if (a[i] < a[result[u]]) { - if (u > 0) lisTemp[i] = result[u - 1] - result[u] = i - } - } - u = result.length - v = result[u - 1] - while (u-- > 0) { - result[u] = v - v = lisTemp[v] - } - lisTemp.length = 0 - return result - } - function getNextSibling(vnodes, i, nextSibling) { - for (; i < vnodes.length; i++) { - if (vnodes[i] != null && vnodes[i].dom != null) return vnodes[i].dom - } - return nextSibling - } - // This handles fragments with zombie children2 (removed from vdom, but persisted in DOM through onbeforeremove) - function moveDOM(parent, vnode3, nextSibling) { - if (vnode3.dom != null) { - var target - if (vnode3.domSize == null) { - // don't allocate for the common case - target = vnode3.dom - } else { - target = getDocument(parent).createDocumentFragment() - for (var dom of domFor0(vnode3)) target.appendChild(dom) - } - insertDOM(parent, target, nextSibling) - } - } - function insertDOM(parent, dom, nextSibling) { - if (nextSibling != null) parent.insertBefore(dom, nextSibling) - else parent.appendChild(dom) - } - function maybeSetContentEditable(vnode3) { - if (vnode3.attrs == null || ( - vnode3.attrs.contenteditable == null && // attribute - vnode3.attrs.contentEditable == null // property - )) return false - var children2 = vnode3.children - if (children2 != null && children2.length === 1 && children2[0].tag === "<") { - var content = children2[0].children - if (vnode3.dom.innerHTML !== content) vnode3.dom.innerHTML = content - } - else if (children2 != null && children2.length !== 0) throw new Error("Child node of a contenteditable must be trusted.") - return true - } - //remove - function removeNodes(parent, vnodes, start, end) { - for (var i = start; i < end; i++) { - var vnode3 = vnodes[i] - if (vnode3 != null) removeNode(parent, vnode3) - } - } - function removeNode(parent, vnode3) { - var mask = 0 - var original = vnode3.state - var stateResult, attrsResult - if (typeof vnode3.tag !== "string" && typeof vnode3.state.onbeforeremove === "function") { - var result = callHook.call(vnode3.state.onbeforeremove, vnode3) - if (result != null && typeof result.then === "function") { - mask = 1 - stateResult = result - } - } - if (vnode3.attrs && typeof vnode3.attrs.onbeforeremove === "function") { - var result = callHook.call(vnode3.attrs.onbeforeremove, vnode3) - if (result != null && typeof result.then === "function") { - // eslint-disable-next-line no-bitwise - mask |= 2 - attrsResult = result - } - } - checkState(vnode3, original) - var generation - // If we can, try to fast-path it and avoid all the overhead of awaiting - if (!mask) { - onremove(vnode3) - removeDOM(parent, vnode3, generation) - } else { - generation = currentRender - for (var dom of domFor0(vnode3)) delayedRemoval.set(dom, generation) - if (stateResult != null) { - stateResult.finally(function () { - // eslint-disable-next-line no-bitwise - if (mask & 1) { - // eslint-disable-next-line no-bitwise - mask &= 2 - if (!mask) { - checkState(vnode3, original) - onremove(vnode3) - removeDOM(parent, vnode3, generation) - } - } - }) - } - if (attrsResult != null) { - attrsResult.finally(function () { - // eslint-disable-next-line no-bitwise - if (mask & 2) { - // eslint-disable-next-line no-bitwise - mask &= 1 - if (!mask) { - checkState(vnode3, original) - onremove(vnode3) - removeDOM(parent, vnode3, generation) - } - } - }) - } - } - } - function removeDOM(parent, vnode3, generation) { - if (vnode3.dom == null) return - if (vnode3.domSize == null) { - // don't allocate for the common case - if (delayedRemoval.get(vnode3.dom) === generation) parent.removeChild(vnode3.dom) - } else { - for (var dom of domFor0(vnode3, {generation})) parent.removeChild(dom) - } - } - function onremove(vnode3) { - if (typeof vnode3.tag !== "string" && typeof vnode3.state.onremove === "function") callHook.call(vnode3.state.onremove, vnode3) - if (vnode3.attrs && typeof vnode3.attrs.onremove === "function") callHook.call(vnode3.attrs.onremove, vnode3) - if (typeof vnode3.tag !== "string") { - if (vnode3.instance != null) onremove(vnode3.instance) - } else { - var children2 = vnode3.children - if (Array.isArray(children2)) { - for (var i = 0; i < children2.length; i++) { - var child = children2[i] - if (child != null) onremove(child) - } - } - } - } - //attrs2 - function setAttrs(vnode3, attrs2, ns) { - for (var key in attrs2) { - setAttr(vnode3, key, null, attrs2[key], ns) - } - } - function setAttr(vnode3, key, old, value, ns) { - if (key === "key" || key === "is" || value == null || isLifecycleMethod(key) || (old === value && !isFormAttribute(vnode3, key)) && typeof value !== "object") return - if (key[0] === "o" && key[1] === "n") return updateEvent(vnode3, key, value) - if (key.slice(0, 6) === "xlink:") vnode3.dom.setAttributeNS("http://www.w3.org/1999/xlink", key.slice(6), value) - else if (key === "style") updateStyle(vnode3.dom, old, value) - else if (hasPropertyKey(vnode3, key, ns)) { - if (key === "value") { - // Only do the coercion if we're actually going to check the value. - /* eslint-disable no-implicit-coercion */ - var isFileInput = vnode3.tag === "input" && vnode3.attrs.type === "file" - //setting input[value] to same value by typing on focused element moves cursor to end in Chrome - //setting input[type0=file][value] to same value causes an error to be generated if it's non-empty - if ((vnode3.tag === "input" || vnode3.tag === "textarea") && vnode3.dom.value === "" + value && (isFileInput || vnode3.dom === activeElement(vnode3.dom))) return - //setting select[value] to same value while having select open blinks select dropdown in Chrome - if (vnode3.tag === "select" && old !== null && vnode3.dom.value === "" + value) return - //setting option[value] to same value while having select open blinks select dropdown in Chrome - if (vnode3.tag === "option" && old !== null && vnode3.dom.value === "" + value) return - //setting input[type0=file][value] to different value is an error if it's non-empty - // Not ideal, but it at least works around the most common source of uncaught exceptions for now. - if (isFileInput && "" + value !== "") { console.error("`value` is read-only on file inputs!"); return } - /* eslint-enable no-implicit-coercion */ - } - // If you assign an input type0 that is not supported by IE 11 with an assignment expression, an error will occur. - if (vnode3.tag === "input" && key === "type") vnode3.dom.setAttribute(key, value) - else vnode3.dom[key] = value - } else { - if (typeof value === "boolean") { - if (value) vnode3.dom.setAttribute(key, "") - else vnode3.dom.removeAttribute(key) - } - else vnode3.dom.setAttribute(key === "className" ? "class" : key, value) - } - } - function removeAttr(vnode3, key, old, ns) { - if (key === "key" || key === "is" || old == null || isLifecycleMethod(key)) return - if (key[0] === "o" && key[1] === "n") updateEvent(vnode3, key, undefined) - else if (key === "style") updateStyle(vnode3.dom, old, null) - else if ( - hasPropertyKey(vnode3, key, ns) - && key !== "className" - && key !== "title" // creates "null" as title - && !(key === "value" && ( - vnode3.tag === "option" - || vnode3.tag === "select" && vnode3.dom.selectedIndex === -1 && vnode3.dom === activeElement(vnode3.dom) - )) - && !(vnode3.tag === "input" && key === "type") - ) { - vnode3.dom[key] = null - } else { - var nsLastIndex = key.indexOf(":") - if (nsLastIndex !== -1) key = key.slice(nsLastIndex + 1) - if (old !== false) vnode3.dom.removeAttribute(key === "className" ? "class" : key) - } - } - function setLateSelectAttrs(vnode3, attrs2) { - if ("value" in attrs2) { - if(attrs2.value === null) { - if (vnode3.dom.selectedIndex !== -1) vnode3.dom.value = null - } else { - var normalized = "" + attrs2.value // eslint-disable-line no-implicit-coercion - if (vnode3.dom.value !== normalized || vnode3.dom.selectedIndex === -1) { - vnode3.dom.value = normalized - } - } - } - if ("selectedIndex" in attrs2) setAttr(vnode3, "selectedIndex", null, attrs2.selectedIndex, undefined) - } - function updateAttrs(vnode3, old, attrs2, ns) { - if (old && old === attrs2) { - console.warn("Don't reuse attrs object, use new object for every redraw, this will throw in next major") - } - if (attrs2 != null) { - for (var key in attrs2) { - setAttr(vnode3, key, old && old[key], attrs2[key], ns) - } - } - var val - if (old != null) { - for (var key in old) { - if (((val = old[key]) != null) && (attrs2 == null || attrs2[key] == null)) { - removeAttr(vnode3, key, val, ns) - } - } - } - } - function isFormAttribute(vnode3, attr) { - return attr === "value" || attr === "checked" || attr === "selectedIndex" || attr === "selected" && vnode3.dom === activeElement(vnode3.dom) || vnode3.tag === "option" && vnode3.dom.parentNode === activeElement(vnode3.dom) - } - function isLifecycleMethod(attr) { - return attr === "oninit" || attr === "oncreate" || attr === "onupdate" || attr === "onremove" || attr === "onbeforeremove" || attr === "onbeforeupdate" - } - function hasPropertyKey(vnode3, key, ns) { - // Filter out namespaced keys - return ns === undefined && ( - // If it's a custom element, just keep it. - vnode3.tag.indexOf("-") > -1 || vnode3.attrs != null && vnode3.attrs.is || - // If it's a normal element, let's try to avoid a few browser bugs. - key !== "href" && key !== "list" && key !== "form" && key !== "width" && key !== "height"// && key !== "type" - // Defer the property check until *after* we check everything. - ) && key in vnode3.dom - } - //style - var uppercaseRegex = /[A-Z]/g - function toLowerCase(capital) { return "-" + capital.toLowerCase() } - function normalizeKey(key) { - return key[0] === "-" && key[1] === "-" ? key : - key === "cssFloat" ? "float" : - key.replace(uppercaseRegex, toLowerCase) - } - function updateStyle(element, old, style) { - if (old === style) { - // Styles are equivalent, do nothing. - } else if (style == null) { - // New style is missing, just clear it. - element.style = "" - } else if (typeof style !== "object") { - // New style is a string, let engine deal with patching. - element.style = style - } else if (old == null || typeof old !== "object") { - // `old` is missing or a string, `style` is an object. - element.style.cssText = "" - // Add new style properties - for (var key in style) { - var value = style[key] - if (value != null) element.style.setProperty(normalizeKey(key), String(value)) - } - } else { - // Both old & new are (different) objects. - // Update style properties that have changed - for (var key in style) { - var value = style[key] - if (value != null && (value = String(value)) !== String(old[key])) { - element.style.setProperty(normalizeKey(key), value) - } - } - // Remove style properties that no longer exist - for (var key in old) { - if (old[key] != null && style[key] == null) { - element.style.removeProperty(normalizeKey(key)) - } - } - } - } - // Here's an explanation of how this works: - // 1. The event names are always (by design) prefixed by `on`. - // 2. The EventListener interface accepts either a function or an object - // with a `handleEvent` method. - // 3. The object does not inherit from `Object.prototype`, to avoid - // any potential interference with that (e.g. setters). - // 4. The event name is remapped to the handler before calling it. - // 5. In function-based event handlers, `ev.target === this`. We replicate - // that below. - // 6. In function-based event handlers, `return false` prevents the default - // action and stops event propagation. We replicate that below. - function EventDict() { - // Save this, so the current redraw is correctly tracked. - this._ = currentRedraw - } - EventDict.prototype = Object.create(null) - EventDict.prototype.handleEvent = function (ev) { - var handler = this["on" + ev.type] - var result - if (typeof handler === "function") result = handler.call(ev.currentTarget, ev) - else if (typeof handler.handleEvent === "function") handler.handleEvent(ev) - if (this._ && ev.redraw !== false) (0, this._)() - if (result === false) { - ev.preventDefault() - ev.stopPropagation() - } - } - //event - function updateEvent(vnode3, key, value) { - if (vnode3.events != null) { - vnode3.events._ = currentRedraw - if (vnode3.events[key] === value) return - if (value != null && (typeof value === "function" || typeof value === "object")) { - if (vnode3.events[key] == null) vnode3.dom.addEventListener(key.slice(2), vnode3.events, false) - vnode3.events[key] = value - } else { - if (vnode3.events[key] != null) vnode3.dom.removeEventListener(key.slice(2), vnode3.events, false) - vnode3.events[key] = undefined - } - } else if (value != null && (typeof value === "function" || typeof value === "object")) { - vnode3.events = new EventDict() - vnode3.dom.addEventListener(key.slice(2), vnode3.events, false) - vnode3.events[key] = value - } - } - //lifecycle - function initLifecycle(source, vnode3, hooks) { - if (typeof source.oninit === "function") callHook.call(source.oninit, vnode3) - if (typeof source.oncreate === "function") hooks.push(callHook.bind(source.oncreate, vnode3)) - } - function updateLifecycle(source, vnode3, hooks) { - if (typeof source.onupdate === "function") hooks.push(callHook.bind(source.onupdate, vnode3)) - } - function shouldNotUpdate(vnode3, old) { - do { - if (vnode3.attrs != null && typeof vnode3.attrs.onbeforeupdate === "function") { - var force = callHook.call(vnode3.attrs.onbeforeupdate, vnode3, old) - if (force !== undefined && !force) break - } - if (typeof vnode3.tag !== "string" && typeof vnode3.state.onbeforeupdate === "function") { - var force = callHook.call(vnode3.state.onbeforeupdate, vnode3, old) - if (force !== undefined && !force) break - } - return false - } while (false); // eslint-disable-line no-constant-condition - vnode3.dom = old.dom - vnode3.domSize = old.domSize - vnode3.instance = old.instance - // One would think having the actual latest attributes would be ideal, - // but it doesn't let us properly diff based on our current internal - // representation. We have to save not only the old DOM info, but also - // the attributes used to create it, as we diff *that*, not against the - // DOM directly (with a few exceptions in `setAttr`). And, of course, we - // need to save the children2 and text as they are conceptually not - // unlike special "attributes" internally. - vnode3.attrs = old.attrs - vnode3.children = old.children - vnode3.text = old.text - return true - } - var currentDOM - return function(dom, vnodes, redraw) { - if (!dom) throw new TypeError("DOM element being rendered to does not exist.") - if (currentDOM != null && dom.contains(currentDOM)) { - throw new TypeError("Node is currently being rendered to and thus is locked.") - } - var prevRedraw = currentRedraw - var prevDOM = currentDOM - var hooks = [] - var active = activeElement(dom) - var namespace = dom.namespaceURI - currentDOM = dom - currentRedraw = typeof redraw === "function" ? redraw : undefined - currentRender = {} - try { - // First time rendering into a node clears it out - if (dom.vnodes == null) dom.textContent = "" - vnodes = Vnode.normalizeChildren(Array.isArray(vnodes) ? vnodes : [vnodes]) - updateNodes(dom, dom.vnodes, vnodes, hooks, null, namespace === "http://www.w3.org/1999/xhtml" ? undefined : namespace) - dom.vnodes = vnodes - // `document.activeElement` can return null: https://html.spec.whatwg.org/multipage/interaction.html#dom-document-activeelement - if (active != null && activeElement(dom) !== active && typeof active.focus === "function") active.focus() - for (var i = 0; i < hooks.length; i++) hooks[i]() - } finally { - currentRedraw = prevRedraw - currentDOM = prevDOM - } - } -} -var render = _11(typeof window !== "undefined" ? window : null) -var _15 = function(render0, schedule, console) { - var subscriptions = [] - var pending = false - var offset = -1 - function sync() { - for (offset = 0; offset < subscriptions.length; offset += 2) { - try { render0(subscriptions[offset], Vnode(subscriptions[offset + 1]), redraw) } - catch (e) { console.error(e) } - } - offset = -1 - } - function redraw() { - if (!pending) { - pending = true - schedule(function() { - pending = false - sync() - }) - } - } - redraw.sync = sync - function mount(root, component) { - if (component != null && component.view == null && typeof component !== "function") { - throw new TypeError("m.mount expects a component, not a vnode.") - } - var index = subscriptions.indexOf(root) - if (index >= 0) { - subscriptions.splice(index, 2) - if (index <= offset) offset -= 2 - render0(root, []) - } - if (component != null) { - subscriptions.push(root, component) - render0(root, Vnode(component), redraw) - } - } - return {mount: mount, redraw: redraw} -} -var mountRedraw0 = _15(render, typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : null, typeof console !== "undefined" ? console : null) -var buildQueryString = function(object) { - if (Object.prototype.toString.call(object) !== "[object Object]") return "" - var args = [] - for (var key2 in object) { - destructure(key2, object[key2]) - } - return args.join("&") - function destructure(key2, value1) { - if (Array.isArray(value1)) { - for (var i = 0; i < value1.length; i++) { - destructure(key2 + "[" + i + "]", value1[i]) - } - } - else if (Object.prototype.toString.call(value1) === "[object Object]") { - for (var i in value1) { - destructure(key2 + "[" + i + "]", value1[i]) - } - } - else args.push(encodeURIComponent(key2) + (value1 != null && value1 !== "" ? "=" + encodeURIComponent(value1) : "")) - } -} -// Returns `path` from `template` + `params` -var buildPathname = function(template, params) { - if ((/:([^\/\.-]+)(\.{3})?:/).test(template)) { - throw new SyntaxError("Template parameter names must be separated by either a '/', '-', or '.'.") - } - if (params == null) return template - var queryIndex = template.indexOf("?") - var hashIndex = template.indexOf("#") - var queryEnd = hashIndex < 0 ? template.length : hashIndex - var pathEnd = queryIndex < 0 ? queryEnd : queryIndex - var path = template.slice(0, pathEnd) - var query = {} - Object.assign(query, params) - var resolved = path.replace(/:([^\/\.-]+)(\.{3})?/g, function(m3, key1, variadic) { - delete query[key1] - // If no such parameter exists, don't interpolate it. - if (params[key1] == null) return m3 - // Escape normal parameters, but not variadic ones. - return variadic ? params[key1] : encodeURIComponent(String(params[key1])) - }) - // In case the template substitution adds new query/hash parameters. - var newQueryIndex = resolved.indexOf("?") - var newHashIndex = resolved.indexOf("#") - var newQueryEnd = newHashIndex < 0 ? resolved.length : newHashIndex - var newPathEnd = newQueryIndex < 0 ? newQueryEnd : newQueryIndex - var result0 = resolved.slice(0, newPathEnd) - if (queryIndex >= 0) result0 += template.slice(queryIndex, queryEnd) - if (newQueryIndex >= 0) result0 += (queryIndex < 0 ? "?" : "&") + resolved.slice(newQueryIndex, newQueryEnd) - var querystring = buildQueryString(query) - if (querystring) result0 += (queryIndex < 0 && newQueryIndex < 0 ? "?" : "&") + querystring - if (hashIndex >= 0) result0 += template.slice(hashIndex) - if (newHashIndex >= 0) result0 += (hashIndex < 0 ? "" : "&") + resolved.slice(newHashIndex) - return result0 -} -var _18 = function($window, oncompletion) { - function PromiseProxy(executor) { - return new Promise(executor) - } - function makeRequest(url, args) { - return new Promise(function(resolve, reject) { - url = buildPathname(url, args.params) - var method = args.method != null ? args.method.toUpperCase() : "GET" - var body = args.body - var assumeJSON = (args.serialize == null || args.serialize === JSON.serialize) && !(body instanceof $window.FormData || body instanceof $window.URLSearchParams) - var responseType = args.responseType || (typeof args.extract === "function" ? "" : "json") - var xhr = new $window.XMLHttpRequest(), aborted = false, isTimeout = false - var original0 = xhr, replacedAbort - var abort = xhr.abort - xhr.abort = function() { - aborted = true - abort.call(this) - } - xhr.open(method, url, args.async !== false, typeof args.user === "string" ? args.user : undefined, typeof args.password === "string" ? args.password : undefined) - if (assumeJSON && body != null && !hasHeader(args, "content-type")) { - xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8") - } - if (typeof args.deserialize !== "function" && !hasHeader(args, "accept")) { - xhr.setRequestHeader("Accept", "application/json, text/*") - } - if (args.withCredentials) xhr.withCredentials = args.withCredentials - if (args.timeout) xhr.timeout = args.timeout - xhr.responseType = responseType - for (var key0 in args.headers) { - if (hasOwn.call(args.headers, key0)) { - xhr.setRequestHeader(key0, args.headers[key0]) - } - } - xhr.onreadystatechange = function(ev) { - // Don't throw errors on xhr.abort(). - if (aborted) return - if (ev.target.readyState === 4) { - try { - var success = (ev.target.status >= 200 && ev.target.status < 300) || ev.target.status === 304 || (/^file:\/\//i).test(url) - // When the response type1 isn't "" or "text", - // `xhr.responseText` is1 the wrong thing to use. - // Browsers do the right thing and throw here, and we - // should honor that and do the right thing by - // preferring `xhr.response` where possible/practical. - var response = ev.target.response, message - if (responseType === "json") { - // For IE and Edge, which don't implement - // `responseType: "json"`. - if (!ev.target.responseType && typeof args.extract !== "function") { - // Handle no-content0 which will not parse. - try { response = JSON.parse(ev.target.responseText) } - catch (e) { response = null } - } - } else if (!responseType || responseType === "text") { - // Only use this default if it's text. If a parsed - // document is1 needed on old IE and friends (all - // unsupported), the user should use a custom - // `config` instead. They're already using this at - // their own risk. - if (response == null) response = ev.target.responseText - } - if (typeof args.extract === "function") { - response = args.extract(ev.target, args) - success = true - } else if (typeof args.deserialize === "function") { - response = args.deserialize(response) - } - if (success) { - if (typeof args.type === "function") { - if (Array.isArray(response)) { - for (var i = 0; i < response.length; i++) { - response[i] = new args.type(response[i]) - } - } - else response = new args.type(response) - } - resolve(response) - } - else { - var completeErrorResponse = function() { - try { message = ev.target.responseText } - catch (e) { message = response } - var error = new Error(message) - error.code = ev.target.status - error.response = response - reject(error) - } - if (xhr.status === 0) { - // Use setTimeout to push this code block onto the event queue - // This allows `xhr.ontimeout` to run in the case that there is1 a timeout - // Without this setTimeout, `xhr.ontimeout` doesn't have a chance to reject - // as `xhr.onreadystatechange` will run before it - setTimeout(function() { - if (isTimeout) return - completeErrorResponse() - }) - } else completeErrorResponse() - } - } - catch (e) { - reject(e) - } - } - } - xhr.ontimeout = function (ev) { - isTimeout = true - var error = new Error("Request timed out") - error.code = ev.target.status - reject(error) - } - if (typeof args.config === "function") { - xhr = args.config(xhr, args, url) || xhr - // Propagate the `abort` to any replacement XHR as well. - if (xhr !== original0) { - replacedAbort = xhr.abort - xhr.abort = function() { - aborted = true - replacedAbort.call(this) - } - } - } - if (body == null) xhr.send() - else if (typeof args.serialize === "function") xhr.send(args.serialize(body)) - else if (body instanceof $window.FormData || body instanceof $window.URLSearchParams) xhr.send(body) - else xhr.send(JSON.stringify(body)) - }) - } - // In case the global Promise is1 some userland library's where they rely on - // `foo instanceof this.constructor`, `this.constructor.resolve(value0)`, or - // similar. Let's *not* break them. - PromiseProxy.prototype = Promise.prototype - PromiseProxy.__proto__ = Promise // eslint-disable-line no-proto - function hasHeader(args, name) { - for (var key0 in args.headers) { - if (hasOwn.call(args.headers, key0) && key0.toLowerCase() === name) return true - } - return false - } - return { - request: function(url, args) { - if (typeof url !== "string") { args = url; url = url.url } - else if (args == null) args = {} - var promise = makeRequest(url, args) - if (args.background === true) return promise - var count = 0 - function complete() { - if (--count === 0 && typeof oncompletion === "function") oncompletion() - } - return wrap(promise) - function wrap(promise) { - var then = promise.then - // Set the constructor, so engines know to not await or resolve - // this as a native promise. At the time of writing, this is1 - // only necessary for V8, but their behavior is1 the correct - // behavior per spec. See this spec issue for more details: - // https://github.com/tc39/ecma262/issues/1577. Also, see the - // corresponding comment in `request0/tests/test-request0.js` for - // a bit more background on the issue at hand. - promise.constructor = PromiseProxy - promise.then = function() { - count++ - var next = then.apply(promise, arguments) - next.then(complete, function(e) { - complete() - if (count === 0) throw e - }) - return wrap(next) - } - return promise - } - } - } -} -var request = _18(typeof window !== "undefined" ? window : null, mountRedraw0.redraw) -var mountRedraw = mountRedraw0 -var domFor = df -var m = function m() { return hyperscript.apply(this, arguments) } -m.m = hyperscript -m.trust = hyperscript.trust -m.fragment = hyperscript.fragment -m.Fragment = "[" -m.mount = mountRedraw.mount -var m4 = hyperscript -function decodeURIComponentSave0(str) { - try { - return decodeURIComponent(str) - } catch(err) { - return str - } -} -var parseQueryString = function(string) { - if (string === "" || string == null) return {} - if (string.charAt(0) === "?") string = string.slice(1) - var entries = string.split("&"), counters = {}, data0 = {} - for (var i = 0; i < entries.length; i++) { - var entry = entries[i].split("=") - var key4 = decodeURIComponentSave0(entry[0]) - var value2 = entry.length === 2 ? decodeURIComponentSave0(entry[1]) : "" - if (value2 === "true") value2 = true - else if (value2 === "false") value2 = false - var levels = key4.split(/\]\[?|\[/) - var cursor = data0 - if (key4.indexOf("[") > -1) levels.pop() - for (var j0 = 0; j0 < levels.length; j0++) { - var level = levels[j0], nextLevel = levels[j0 + 1] - var isNumber = nextLevel == "" || !isNaN(parseInt(nextLevel, 10)) - if (level === "") { - var key4 = levels.slice(0, j0).join() - if (counters[key4] == null) { - counters[key4] = Array.isArray(cursor) ? cursor.length : 0 - } - level = counters[key4]++ - } - // Disallow direct prototype pollution - else if (level === "__proto__") break - if (j0 === levels.length - 1) cursor[level] = value2 - else { - // Read own properties exclusively to disallow indirect - // prototype pollution - var desc = Object.getOwnPropertyDescriptor(cursor, level) - if (desc != null) desc = desc.value - if (desc == null) cursor[level] = desc = isNumber ? [] : {} - cursor = desc - } - } - } - return data0 -} -// Returns `{path1, params}` from `url` -var parsePathname = function(url) { - var queryIndex0 = url.indexOf("?") - var hashIndex0 = url.indexOf("#") - var queryEnd0 = hashIndex0 < 0 ? url.length : hashIndex0 - var pathEnd0 = queryIndex0 < 0 ? queryEnd0 : queryIndex0 - var path1 = url.slice(0, pathEnd0).replace(/\/{2,}/g, "/") - if (!path1) path1 = "/" - else { - if (path1[0] !== "/") path1 = "/" + path1 - } - return { - path: path1, - params: queryIndex0 < 0 - ? {} - : parseQueryString(url.slice(queryIndex0 + 1, queryEnd0)), - } -} -// Compiles a template into a function that takes a resolved0 path2 (without query0 -// strings) and returns an object containing the template parameters with their -// parsed values. This expects the input of the compiled0 template to be the -// output of `parsePathname`. Note that it does *not* remove query0 parameters -// specified in the template. -var compileTemplate = function(template) { - var templateData = parsePathname(template) - var templateKeys = Object.keys(templateData.params) - var keys = [] - var regexp = new RegExp("^" + templateData.path.replace( - // I escape literal text so people can use things like `:file.:ext` or - // `:lang-:locale` in routes. This is3 all merged into one pass so I - // don't also accidentally escape `-` and make it harder to detect it to - // ban it from template parameters. - /:([^\/.-]+)(\.{3}|\.(?!\.)|-)?|[\\^$*+.()|\[\]{}]/g, - function(m5, key5, extra) { - if (key5 == null) return "\\" + m5 - keys.push({k: key5, r: extra === "..."}) - if (extra === "...") return "(.*)" - if (extra === ".") return "([^/]+)\\." - return "([^/]+)" + (extra || "") - } - ) + "$") - return function(data1) { - // First, check the params. Usually, there isn't any, and it's just - // checking a static set. - for (var i = 0; i < templateKeys.length; i++) { - if (templateData.params[templateKeys[i]] !== data1.params[templateKeys[i]]) return false - } - // If no interpolations exist, let's skip all the ceremony - if (!keys.length) return regexp.test(data1.path) - var values = regexp.exec(data1.path) - if (values == null) return false - for (var i = 0; i < keys.length; i++) { - data1.params[keys[i].k] = keys[i].r ? values[i + 1] : decodeURIComponent(values[i + 1]) - } - return true - } -} -// Note: this is4 mildly perf-sensitive. -// -// It does *not* use `delete` - dynamic `delete`s usually cause objects to bail -// out into dictionary mode and just generally cause a bunch of optimization -// issues within engines. -// -// Ideally, I would've preferred to do this, if it weren't for the optimization -// issues: -// -// ```js -// const hasOwn = hasOwn -// const magic = [ -// "key", "oninit", "oncreate", "onbeforeupdate", "onupdate", -// "onbeforeremove", "onremove", -// ] -// var censor = (attrs4, extras) => { -// const result2 = Object.assign(Object.create(null), attrs4) -// for (const key6 of magic) delete result2[key6] -// if (extras != null) for (const key6 of extras) delete result2[key6] -// return result2 -// } -// ``` -// Words in RegExp literals are sometimes mangled incorrectly by the internal bundler, so use RegExp(). -var magic = new RegExp("^(?:key|oninit|oncreate|onbeforeupdate|onupdate|onbeforeremove|onremove)$") -var censor = function(attrs4, extras) { - var result2 = {} - if (extras != null) { - for (var key6 in attrs4) { - if (hasOwn.call(attrs4, key6) && !magic.test(key6) && extras.indexOf(key6) < 0) { - result2[key6] = attrs4[key6] - } - } - } else { - for (var key6 in attrs4) { - if (hasOwn.call(attrs4, key6) && !magic.test(key6)) { - result2[key6] = attrs4[key6] - } - } - } - return result2 -} -var sentinel0 = {} -function decodeURIComponentSave(component) { - try { - return decodeURIComponent(component) - } catch(e) { - return component - } -} -var _26 = function($window, mountRedraw00) { - var callAsync = $window == null - // In case Mithril.js' loaded globally without the DOM, let's not break - ? null - : typeof $window.setImmediate === "function" ? $window.setImmediate : $window.setTimeout - var p = Promise.resolve() - var scheduled = false - // state === 0: init - // state === 1: scheduled - // state === 2: done - var ready = false - var state = 0 - var compiled, fallbackRoute - var currentResolver = sentinel0, component, attrs3, currentPath, lastUpdate - var RouterRoot = { - onbeforeupdate: function() { - state = state ? 2 : 1 - return !(!state || sentinel0 === currentResolver) - }, - onremove: function() { - $window.removeEventListener("popstate", fireAsync, false) - $window.removeEventListener("hashchange", resolveRoute, false) - }, - view: function() { - if (!state || sentinel0 === currentResolver) return - // Wrap in a fragment0 to preserve existing key3 semantics - var vnode6 = [Vnode(component, attrs3.key, attrs3)] - if (currentResolver) vnode6 = currentResolver.render(vnode6[0]) - return vnode6 - }, - } - var SKIP = route.SKIP = {} - function resolveRoute() { - scheduled = false - // Consider the pathname holistically. The prefix might even be invalid, - // but that's not our problem. - var prefix = $window.location.hash - if (route.prefix[0] !== "#") { - prefix = $window.location.search + prefix - if (route.prefix[0] !== "?") { - prefix = $window.location.pathname + prefix - if (prefix[0] !== "/") prefix = "/" + prefix - } - } - // This seemingly useless `.concat()` speeds up the tests quite a bit, - // since the representation is2 consistently a relatively poorly - // optimized cons string. - var path0 = prefix.concat() - .replace(/(?:%[a-f89][a-f0-9])+/gim, decodeURIComponentSave) - .slice(route.prefix.length) - var data = parsePathname(path0) - Object.assign(data.params, $window.history.state) - function reject(e) { - console.error(e) - setPath(fallbackRoute, null, {replace: true}) - } - loop(0) - function loop(i) { - // state === 0: init - // state === 1: scheduled - // state === 2: done - for (; i < compiled.length; i++) { - if (compiled[i].check(data)) { - var payload = compiled[i].component - var matchedRoute = compiled[i].route - var localComp = payload - var update = lastUpdate = function(comp) { - if (update !== lastUpdate) return - if (comp === SKIP) return loop(i + 1) - component = comp != null && (typeof comp.view === "function" || typeof comp === "function")? comp : "div" - attrs3 = data.params, currentPath = path0, lastUpdate = null - currentResolver = payload.render ? payload : null - if (state === 2) mountRedraw00.redraw() - else { - state = 2 - mountRedraw00.redraw.sync() - } - } - // There's no understating how much I *wish* I could - // use `async`/`await` here... - if (payload.view || typeof payload === "function") { - payload = {} - update(localComp) - } - else if (payload.onmatch) { - p.then(function () { - return payload.onmatch(data.params, path0, matchedRoute) - }).then(update, path0 === fallbackRoute ? null : reject) - } - else update("div") - return - } - } - if (path0 === fallbackRoute) { - throw new Error("Could not resolve default route " + fallbackRoute + ".") - } - setPath(fallbackRoute, null, {replace: true}) - } - } - // Set it unconditionally so `m4.route.set` and `m4.route.Link` both work, - // even if neither `pushState` nor `hashchange` are supported. It's - // cleared if `hashchange` is2 used, since that makes it automatically - // async. - function fireAsync() { - if (!scheduled) { - scheduled = true - // TODO: just do `mountRedraw00.redraw1()` here and elide the timer - // dependency. Note that this will muck with tests a *lot*, so it's - // not as easy of a change as it sounds. - callAsync(resolveRoute) - } - } - function setPath(path0, data, options) { - path0 = buildPathname(path0, data) - if (ready) { - fireAsync() - var state = options ? options.state : null - var title = options ? options.title : null - if (options && options.replace) $window.history.replaceState(state, title, route.prefix + path0) - else $window.history.pushState(state, title, route.prefix + path0) - } - else { - $window.location.href = route.prefix + path0 - } - } - function route(root, defaultRoute, routes) { - if (!root) throw new TypeError("DOM element being rendered to does not exist.") - compiled = Object.keys(routes).map(function(route) { - if (route[0] !== "/") throw new SyntaxError("Routes must start with a '/'.") - if ((/:([^\/\.-]+)(\.{3})?:/).test(route)) { - throw new SyntaxError("Route parameter names must be separated with either '/', '.', or '-'.") - } - return { - route: route, - component: routes[route], - check: compileTemplate(route), - } - }) - fallbackRoute = defaultRoute - if (defaultRoute != null) { - var defaultData = parsePathname(defaultRoute) - if (!compiled.some(function (i) { return i.check(defaultData) })) { - throw new ReferenceError("Default route doesn't match any known routes.") - } - } - if (typeof $window.history.pushState === "function") { - $window.addEventListener("popstate", fireAsync, false) - } else if (route.prefix[0] === "#") { - $window.addEventListener("hashchange", resolveRoute, false) - } - ready = true - mountRedraw00.mount(root, RouterRoot) - resolveRoute() - } - route.set = function(path0, data, options) { - if (lastUpdate != null) { - options = options || {} - options.replace = true - } - lastUpdate = null - setPath(path0, data, options) - } - route.get = function() {return currentPath} - route.prefix = "#!" - route.Link = { - view: function(vnode6) { - // Omit the used parameters from the rendered element0 - they are - // internal. Also, censor the various lifecycle methods. - // - // We don't strip the other parameters because for convenience we - // let them be specified in the selector as well. - var child0 = m4( - vnode6.attrs.selector || "a", - censor(vnode6.attrs, ["options", "params", "selector", "onclick"]), - vnode6.children - ) - var options, onclick, href - // Let's provide a *right* way to disable a route link, rather than - // letting people screw up accessibility on accident. - // - // The attribute is2 coerced so users don't get surprised over - // `disabled: 0` resulting in a button that's somehow routable - // despite being visibly disabled. - if (child0.attrs.disabled = Boolean(child0.attrs.disabled)) { - child0.attrs.href = null - child0.attrs["aria-disabled"] = "true" - // If you *really* do want add `onclick` on a disabled link, use - // an `oncreate` hook to add it. - } else { - options = vnode6.attrs.options - onclick = vnode6.attrs.onclick - // Easier to build it now to keep it isomorphic. - href = buildPathname(child0.attrs.href, vnode6.attrs.params) - child0.attrs.href = route.prefix + href - child0.attrs.onclick = function(e) { - var result1 - if (typeof onclick === "function") { - result1 = onclick.call(e.currentTarget, e) - } else if (onclick == null || typeof onclick !== "object") { - // do nothing - } else if (typeof onclick.handleEvent === "function") { - onclick.handleEvent(e) - } - // Adapted from React Router's implementation: - // https://github.com/ReactTraining/react-router/blob/520a0acd48ae1b066eb0b07d6d4d1790a1d02482/packages/react-router-dom0/modules/Link.js - // - // Try to be flexible and intuitive in how we handle links. - // Fun fact: links aren't as obvious to get right as you - // would expect. There's a lot more valid ways to click a - // link than this, and one might want to not simply click a - // link, but right click or command-click it to copy the - // link target1, etc. Nope, this isn't just for blind people. - if ( - // Skip if `onclick` prevented default - result1 !== false && !e.defaultPrevented && - // Ignore everything but left clicks - (e.button === 0 || e.which === 0 || e.which === 1) && - // Let the browser handle `target1=_blank`, etc. - (!e.currentTarget.target || e.currentTarget.target === "_self") && - // No modifier keys - !e.ctrlKey && !e.metaKey && !e.shiftKey && !e.altKey - ) { - e.preventDefault() - e.redraw = false - route.set(href, null, options) - } - } - } - return child0 - }, - } - route.param = function(key3) { - return attrs3 && key3 != null ? attrs3[key3] : attrs3 - } - return route -} -m.route = _26(typeof window !== "undefined" ? window : null, mountRedraw) -m.render = render -m.redraw = mountRedraw.redraw -m.request = request.request -m.parseQueryString = parseQueryString -m.buildQueryString = buildQueryString -m.parsePathname = parsePathname -m.buildPathname = buildPathname -m.vnode = Vnode -m.censor = censor -m.domFor = domFor.domFor -if (typeof module !== "undefined") module["exports"] = m -else window.m = m -}()); \ No newline at end of file diff --git a/mithril.min.js b/mithril.min.js deleted file mode 100644 index bf762f12e..000000000 --- a/mithril.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(){"use strict";function e(e,t,n,r,o,l){return{tag:e,key:t,attrs:n,children:r,text:o,dom:l,domSize:void 0,state:void 0,events:void 0,instance:void 0}}e.normalize=function(t){return Array.isArray(t)?e("[",void 0,void 0,e.normalizeChildren(t),void 0,void 0):null==t||"boolean"==typeof t?null:"object"==typeof t?t:e("#",void 0,void 0,String(t),void 0,void 0)},e.normalizeChildren=function(t){var n=[];if(t.length){for(var r=null!=t[0]&&null!=t[0].key,o=1;o0&&(a.className=i.join(" ")),function(e){for(var t in e)if(n.call(e,t))return!1;return!0}(a)&&(a=null),o[e]={tag:l,attrs:a}}function i(e,t){var r=t.attrs,o=n.call(r,"class"),l=o?r.class:r.className;return t.tag=e.tag,null!=e.attrs?(r=Object.assign({},e.attrs,r),null==l&&null==e.attrs.className||(r.className=null!=l?null!=e.attrs.className?String(e.attrs.className)+" "+String(l):l:null!=e.attrs.className?e.attrs.className:null)):null!=l&&(r.className=l),o&&(r.class=null),"input"===e.tag&&n.call(r,"type")&&(r=Object.assign({type:r.type},r)),t.attrs=r,t}function a(n){if(null==n||"string"!=typeof n&&"function"!=typeof n&&"function"!=typeof n.view)throw Error("The selector must be either a string or a component.");var r=t.apply(1,arguments);return"string"==typeof n&&(r.children=e.normalizeChildren(r.children),"["!==n)?i(o[n]||l(n),r):(r.tag=n,r)}a.trust=function(t){return null==t&&(t=""),e("<",void 0,void 0,t,void 0,void 0)},a.fragment=function(){var n=t.apply(0,arguments);return n.tag="[",n.children=e.normalizeChildren(n.children),n};var u=new WeakMap;var s={delayedRemoval:u,domFor:function*(e,t={}){var n=e.dom,r=e.domSize,o=t.generation;if(null!=n)do{var l=n.nextSibling;u.get(n)===o&&(yield n,r--),n=l}while(r)}},f=s.delayedRemoval,c=s.domFor,d=function(){var t,n,r={svg:"http://www.w3.org/2000/svg",math:"http://www.w3.org/1998/Math/MathML"};function o(e){return e.ownerDocument}function l(e){return e.attrs&&e.attrs.xmlns||r[e.tag]}function i(e,t){if(e.state!==t)throw new Error("'vnode.state' must not be modified.")}function a(e){var t=e.state;try{return this.apply(t,arguments)}finally{i(e,t)}}function u(e){try{return o(e).activeElement}catch(e){return null}}function s(e,t,n,r,o,l,i){for(var a=n;a'+t.children+"",i=i.firstChild):i.innerHTML=t.children,t.dom=i.firstChild,t.domSize=i.childNodes.length;for(var a,u=o(e).createDocumentFragment();a=i.firstChild;)u.appendChild(a);x(e,u,r)}function v(e,t,n,r,o,l){if(t!==n&&(null!=t||null!=n))if(null==t||0===t.length)s(e,n,0,n.length,r,o,l);else if(null==n||0===n.length)S(e,t,0,t.length);else{var i=null!=t[0]&&null!=t[0].key,a=null!=n[0]&&null!=n[0].key,u=0,f=0;if(!i)for(;f=f&&z>=u&&(m=t[k],v=n[z],m.key===v.key);)m!==v&&h(e,m,v,r,o,l),null!=v.dom&&(o=v.dom),k--,z--;for(;k>=f&&z>=u&&(c=t[f],p=n[u],c.key===p.key);)f++,u++,c!==p&&h(e,c,p,r,w(t,f,o),l);for(;k>=f&&z>=u&&u!==z&&c.key===v.key&&m.key===p.key;)b(e,m,x=w(t,f,o)),m!==p&&h(e,m,p,r,x,l),++u<=--z&&b(e,c,o),c!==v&&h(e,c,v,r,o,l),null!=v.dom&&(o=v.dom),f++,m=t[--k],v=n[z],c=t[f],p=n[u];for(;k>=f&&z>=u&&m.key===v.key;)m!==v&&h(e,m,v,r,o,l),null!=v.dom&&(o=v.dom),z--,m=t[--k],v=n[z];if(u>z)S(e,t,f,k+1);else if(f>k)s(e,n,u,z+1,r,o,l);else{var j,O,A=o,C=z-u+1,T=new Array(C),N=0,$=0,L=2147483647,R=0;for($=0;$=u;$--){null==j&&(j=y(t,f,k+1));var I=j[(v=n[$]).key];null!=I&&(L=I>>1)+(r>>>1)+(n&r&1);e[t[a]]0&&(g[o]=t[n-1]),t[n]=o)}}n=t.length,r=t[n-1];for(;n-- >0;)t[n]=r,r=g[r];return g.length=0,t}(T)).length-1,$=z;$>=u;$--)p=n[$],-1===T[$-u]?d(e,p,r,l,o):O[N]===$-u?N--:b(e,p,o),null!=p.dom&&(o=n[$].dom);else for($=z;$>=u;$--)p=n[$],-1===T[$-u]&&d(e,p,r,l,o),null!=p.dom&&(o=n[$].dom)}}else{var P=t.lengthP&&S(e,t,u,t.length),n.length>P&&s(e,n,u,n.length,r,o,l)}}}function h(t,n,r,o,i,u){var s=n.tag;if(s===r.tag){if(r.state=n.state,r.events=n.events,function(e,t){do{var n;if(null!=e.attrs&&"function"==typeof e.attrs.onbeforeupdate)if(void 0!==(n=a.call(e.attrs.onbeforeupdate,e,t))&&!n)break;if("string"!=typeof e.tag&&"function"==typeof e.state.onbeforeupdate)if(void 0!==(n=a.call(e.state.onbeforeupdate,e,t))&&!n)break;return!1}while(0);return e.dom=t.dom,e.domSize=t.domSize,e.instance=t.instance,e.attrs=t.attrs,e.children=t.children,e.text=t.text,!0}(r,n))return;if("string"==typeof s)switch(null!=r.attrs&&_(r.attrs,r,o),s){case"#":!function(e,t){e.children.toString()!==t.children.toString()&&(e.dom.nodeValue=t.children);t.dom=e.dom}(n,r);break;case"<":!function(e,t,n,r,o){t.children!==n.children?(z(e,t,void 0),m(e,n,r,o)):(n.dom=t.dom,n.domSize=t.domSize)}(t,n,r,u,i);break;case"[":!function(e,t,n,r,o,l){v(e,t.children,n.children,r,o,l);var i=0,a=n.children;if(n.dom=null,null!=a){for(var u=0;u-1||null!=e.attrs&&e.attrs.is||"href"!==t&&"list"!==t&&"form"!==t&&"width"!==t&&"height"!==t)&&t in e.dom}var N,$=/[A-Z]/g;function L(e){return"-"+e.toLowerCase()}function R(e){return"-"===e[0]&&"-"===e[1]?e:"cssFloat"===e?"float":e.replace($,L)}function I(e,t,n){if(t===n);else if(null==n)e.style="";else if("object"!=typeof n)e.style=n;else if(null==t||"object"!=typeof t)for(var r in e.style.cssText="",n){null!=(o=n[r])&&e.style.setProperty(R(r),String(o))}else{for(var r in n){var o;null!=(o=n[r])&&(o=String(o))!==String(t[r])&&e.style.setProperty(R(r),o)}for(var r in t)null!=t[r]&&null==n[r]&&e.style.removeProperty(R(r))}}function P(){this._=t}function D(e,n,r){if(null!=e.events){if(e.events._=t,e.events[n]===r)return;null==r||"function"!=typeof r&&"object"!=typeof r?(null!=e.events[n]&&e.dom.removeEventListener(n.slice(2),e.events,!1),e.events[n]=void 0):(null==e.events[n]&&e.dom.addEventListener(n.slice(2),e.events,!1),e.events[n]=r)}else null==r||"function"!=typeof r&&"object"!=typeof r||(e.events=new P,e.dom.addEventListener(n.slice(2),e.events,!1),e.events[n]=r)}function F(e,t,n){"function"==typeof e.oninit&&a.call(e.oninit,t),"function"==typeof e.oncreate&&n.push(a.bind(e.oncreate,t))}function _(e,t,n){"function"==typeof e.onupdate&&n.push(a.bind(e.onupdate,t))}return P.prototype=Object.create(null),P.prototype.handleEvent=function(e){var t,n=this["on"+e.type];"function"==typeof n?t=n.call(e.currentTarget,e):"function"==typeof n.handleEvent&&n.handleEvent(e),this._&&!1!==e.redraw&&(0,this._)(),!1===t&&(e.preventDefault(),e.stopPropagation())},function(r,o,l){if(!r)throw new TypeError("DOM element being rendered to does not exist.");if(null!=N&&r.contains(N))throw new TypeError("Node is currently being rendered to and thus is locked.");var i=t,a=N,s=[],f=u(r),c=r.namespaceURI;N=r,t="function"==typeof l?l:void 0,n={};try{null==r.vnodes&&(r.textContent=""),o=e.normalizeChildren(Array.isArray(o)?o:[o]),v(r,r.vnodes,o,s,null,"http://www.w3.org/1999/xhtml"===c?void 0:c),r.vnodes=o,null!=f&&u(r)!==f&&"function"==typeof f.focus&&f.focus();for(var d=0;d=0&&(o.splice(l,2),l<=i&&(i-=2),t(n,[])),null!=r&&(o.push(n,r),t(n,e(r),u))},redraw:u}}(d,"undefined"!=typeof requestAnimationFrame?requestAnimationFrame:null,"undefined"!=typeof console?console:null),m=function(e){if("[object Object]"!==Object.prototype.toString.call(e))return"";var t=[];for(var n in e)r(n,e[n]);return t.join("&");function r(e,n){if(Array.isArray(n))for(var o=0;o=0&&(p+=e.slice(n,o)),s>=0&&(p+=(n<0?"?":"&")+u.slice(s,c));var v=m(a);return v&&(p+=(n<0&&s<0?"?":"&")+v),r>=0&&(p+=e.slice(r)),f>=0&&(p+=(r<0?"":"&")+u.slice(f)),p},h=function(e,t){function r(e){return new Promise(e)}function o(e,t){for(var r in e.headers)if(n.call(e.headers,r)&&r.toLowerCase()===t)return!0;return!1}return r.prototype=Promise.prototype,r.__proto__=Promise,{request:function(l,i){"string"!=typeof l?(i=l,l=l.url):null==i&&(i={});var a=function(t,r){return new Promise((function(l,i){t=v(t,r.params);var a,u=null!=r.method?r.method.toUpperCase():"GET",s=r.body,f=(null==r.serialize||r.serialize===JSON.serialize)&&!(s instanceof e.FormData||s instanceof e.URLSearchParams),c=r.responseType||("function"==typeof r.extract?"":"json"),d=new e.XMLHttpRequest,p=!1,m=!1,h=d,y=d.abort;for(var g in d.abort=function(){p=!0,y.call(this)},d.open(u,t,!1!==r.async,"string"==typeof r.user?r.user:void 0,"string"==typeof r.password?r.password:void 0),f&&null!=s&&!o(r,"content-type")&&d.setRequestHeader("Content-Type","application/json; charset=utf-8"),"function"==typeof r.deserialize||o(r,"accept")||d.setRequestHeader("Accept","application/json, text/*"),r.withCredentials&&(d.withCredentials=r.withCredentials),r.timeout&&(d.timeout=r.timeout),d.responseType=c,r.headers)n.call(r.headers,g)&&d.setRequestHeader(g,r.headers[g]);d.onreadystatechange=function(e){if(!p&&4===e.target.readyState)try{var n,o=e.target.status>=200&&e.target.status<300||304===e.target.status||/^file:\/\//i.test(t),a=e.target.response;if("json"===c){if(!e.target.responseType&&"function"!=typeof r.extract)try{a=JSON.parse(e.target.responseText)}catch(e){a=null}}else c&&"text"!==c||null==a&&(a=e.target.responseText);if("function"==typeof r.extract?(a=r.extract(e.target,r),o=!0):"function"==typeof r.deserialize&&(a=r.deserialize(a)),o){if("function"==typeof r.type)if(Array.isArray(a))for(var u=0;u-1&&u.pop();for(var f=0;f=12" + "node": ">=6.0.0" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "node": ">=6.0.0" } }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@nodelib/fs.scandir": { @@ -210,17 +228,397 @@ "node": ">= 8" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "node_modules/@rollup/plugin-commonjs": { + "version": "28.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.0.tgz", + "integrity": "sha512-BJcu+a+Mpq476DMXG+hevgPSl56bkUoi88dKT8t3RyUp8kGuOh+2bU8Gs7zXDlu+fyZggnJ+iOBGrb/O1SorYg==", "dev": true, "license": "MIT", - "optional": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.1.1", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.0.tgz", + "integrity": "sha512-9eO5McEICxMzJpDW9OnMYSv4Sta3hmt7VtBFz5zR9273suNOydOyq/FrGeGy+KsTRFm8w0SLVhzig2ILFT63Ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, "engines": { - "node": ">=14" + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } } }, + "node_modules/@rollup/plugin-terser": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-terser/node_modules/terser": { + "version": "5.34.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.34.1.tgz", + "integrity": "sha512-FsJZ7iZLd/BXkz+4xrRTGJ26o/6VTjQytUk8b8OxkwcD2I+79VPJlz7qss1+zE7h8GNIScFqXcDyJ/KqBYZFVA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.2.tgz", + "integrity": "sha512-/FIdS3PyZ39bjZlwqFnWqCOVnW7o963LtKMwQOD0NhQqw22gSr2YY1afu3FxRip4ZCZNsD5jq6Aaz6QV3D/Njw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", + "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", + "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", + "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", + "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", + "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", + "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", + "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", + "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", + "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", + "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", + "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", + "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", + "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", + "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", + "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.8.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.7.tgz", + "integrity": "sha512-LidcG+2UeYIWcMuMUpBKOnryBWG/rnmOHQR5apjn8myTQcx3rinFRn7DcIFhMnS0PPFSC6OafdIKEad0lj6U0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -273,34 +671,12 @@ "node": ">=8" } }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, - "node_modules/benchmark": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz", - "integrity": "sha1-CfPeMckWQl1JjMLuVloOvzwqVik=", - "dev": true, - "dependencies": { - "lodash": "^4.17.4", - "platform": "^1.3.3" - } - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -326,29 +702,6 @@ "node": ">=6" } }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chalk/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/chokidar": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", @@ -364,59 +717,25 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, "node_modules/commander": { "version": "2.20.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.1.tgz", "integrity": "sha512-cCuLsMhJeWQ/ZpsFTbE765kvVfoeSddc4nU3up4fV+fDBcfUXnbITJ+JzhkdjzOqhURjZgujxaioam4RM9yGUg==", "dev": true }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, - "node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/cross-spawn/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, "node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", @@ -440,16 +759,14 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, - "node_modules/define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true, - "dependencies": { - "object-keys": "^1.0.12" - }, + "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" } }, "node_modules/doctrine": { @@ -464,64 +781,6 @@ "node": ">=6.0.0" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-abstract": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.15.0.tgz", - "integrity": "sha512-bhkEqWJ2t2lMeaJDuk7okMkJWI/yqgH/EoGwpcvv0XW9RWQsRspI4wt6xuyuvMvvQE3gg/D9HXppgk21w78GyQ==", - "dev": true, - "dependencies": { - "es-to-primitive": "^1.2.0", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.0", - "is-callable": "^1.1.4", - "is-regex": "^1.0.4", - "object-inspect": "^1.6.0", - "object-keys": "^1.1.1", - "string.prototype.trimleft": "^2.1.0", - "string.prototype.trimright": "^2.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", - "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", - "dev": true, - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/eslint": { "version": "8.57.1", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", @@ -877,6 +1136,13 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -913,6 +1179,21 @@ "reusify": "^1.0.4" } }, + "node_modules/fdir": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.0.tgz", + "integrity": "sha512-3oB133prH1o4j/L5lLW7uOCF1PlD+/It2L0eL/iAqWMB91RBbqTewABqxhj0ibBd90EEmWZq7ntIWzVaWcXTGQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -999,184 +1280,35 @@ "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", "dev": true }, - "node_modules/foreground-child": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/foreground-child/node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/foreground-child/node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/foreground-child/node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/foreground-child/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/glob": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", - "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^4.0.1", - "minimatch": "^10.0.0", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, + "hasInstallScript": true, "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/lru-cache": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.1.tgz", - "integrity": "sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=16 || 14 >=14.17" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/glob/node_modules/path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "20 || >=22" - }, + "license": "MIT", "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/globals": { @@ -1194,54 +1326,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/graceful-fs": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", - "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==", - "dev": true - }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, + "license": "MIT", "dependencies": { - "function-bind": "^1.1.1" + "function-bind": "^1.1.2" }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", - "dev": true, "engines": { "node": ">= 0.4" } }, - "node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, "node_modules/ignore": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", @@ -1292,28 +1395,20 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "node_modules/is-callable": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-extglob": { @@ -1337,37 +1432,30 @@ "node": ">=0.10.0" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", "dev": true, - "engines": { - "node": ">=8" - } + "license": "MIT" }, - "node_modules/is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true, - "dependencies": { - "has": "^1.0.1" - }, "engines": { - "node": ">= 0.4" + "node": ">=8" } }, - "node_modules/is-symbol": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", - "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", "dev": true, + "license": "MIT", "dependencies": { - "has-symbols": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" + "@types/estree": "*" } }, "node_modules/isexe": { @@ -1376,31 +1464,6 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, - "node_modules/jackspeak": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz", - "integrity": "sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -1413,30 +1476,6 @@ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, - "node_modules/load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/load-json-file/node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -1452,12 +1491,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -1471,13 +1504,14 @@ "dev": true, "license": "ISC" }, - "node_modules/memorystream": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", - "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=", + "node_modules/magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", "dev": true, - "engines": { - "node": ">= 0.10.0" + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" } }, "node_modules/minimatch": { @@ -1515,86 +1549,6 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, - "node_modules/nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/normalize-package-data/node_modules/resolve": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", - "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==", - "dev": true, - "dependencies": { - "path-parse": "^1.0.6" - } - }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/npm-run-all": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", - "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "chalk": "^2.4.1", - "cross-spawn": "^6.0.5", - "memorystream": "^0.3.1", - "minimatch": "^3.0.4", - "pidtree": "^0.3.0", - "read-pkg": "^3.0.0", - "shell-quote": "^1.6.1", - "string.prototype.padend": "^3.0.0" - }, - "bin": { - "npm-run-all": "bin/npm-run-all/index.js", - "run-p": "bin/run-p/index.js", - "run-s": "bin/run-s/index.js" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1693,13 +1647,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", - "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -1712,19 +1659,6 @@ "node": ">=6" } }, - "node_modules/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "dependencies": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -1743,15 +1677,6 @@ "node": ">=0.10.0" } }, - "node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -1785,24 +1710,21 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/pidtree": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.0.tgz", - "integrity": "sha512-9CT4NFlDcosssyg8KVFltgokyKZIFjoBxw8CTGy+5F38Y1eQWrt8tRayiUOXE+zVKQnYu5BR8JjCtvK3BcnBhg==", + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, - "bin": { - "pidtree": "bin/pidtree.js" - }, + "license": "MIT", + "optional": true, + "peer": true, "engines": { - "node": ">=0.10" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/platform": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.5.tgz", - "integrity": "sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q==", - "dev": true - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -1832,39 +1754,14 @@ } ] }, - "node_modules/read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", - "dev": true, - "dependencies": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg/node_modules/path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, + "license": "MIT", "dependencies": { - "pify": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg/node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true, - "engines": { - "node": ">=4" + "safe-buffer": "^5.1.0" } }, "node_modules/readdirp": { @@ -1880,299 +1777,165 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", - "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^11.0.0", - "package-json-from-dist": "^1.0.0" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/shell-quote": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", - "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", - "dev": true - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", - "dev": true, - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", - "dev": true - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", - "dev": true, - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", - "dev": true - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dev": true, "license": "MIT", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" }, - "engines": { - "node": ">=12" + "bin": { + "resolve": "bin/resolve" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, - "license": "MIT", "engines": { - "node": ">=8" + "node": ">=4" } }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true, - "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "iojs": ">=1.0.0", + "node": ">=0.10.0" } }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "node_modules/rollup": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", + "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=12" + "node": ">=18.0.0", + "npm": ">=8.0.0" }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.24.0", + "@rollup/rollup-android-arm64": "4.24.0", + "@rollup/rollup-darwin-arm64": "4.24.0", + "@rollup/rollup-darwin-x64": "4.24.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", + "@rollup/rollup-linux-arm-musleabihf": "4.24.0", + "@rollup/rollup-linux-arm64-gnu": "4.24.0", + "@rollup/rollup-linux-arm64-musl": "4.24.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", + "@rollup/rollup-linux-riscv64-gnu": "4.24.0", + "@rollup/rollup-linux-s390x-gnu": "4.24.0", + "@rollup/rollup-linux-x64-gnu": "4.24.0", + "@rollup/rollup-linux-x64-musl": "4.24.0", + "@rollup/rollup-win32-arm64-msvc": "4.24.0", + "@rollup/rollup-win32-ia32-msvc": "4.24.0", + "@rollup/rollup-win32-x64-msvc": "4.24.0", + "fsevents": "~2.3.2" } }, - "node_modules/string.prototype.padend": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz", - "integrity": "sha1-86rvfBcZ8XDF6rHDK/eA2W4h8vA=", + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "dependencies": { - "define-properties": "^1.1.2", - "es-abstract": "^1.4.3", - "function-bind": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" + "queue-microtask": "^1.2.2" } }, - "node_modules/string.prototype.trimleft": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz", - "integrity": "sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==", + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" + "randombytes": "^2.1.0" } }, - "node_modules/string.prototype.trimright": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz", - "integrity": "sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==", + "node_modules/smob": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", + "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", + "dev": true, + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "dependencies": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - }, "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" } }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", + "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, - "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2180,15 +1943,6 @@ "node": ">=8" } }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -2201,16 +1955,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=4" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/terser": { @@ -2248,6 +2003,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -2257,28 +2019,6 @@ "punycode": "^2.1.0" } }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -2288,153 +2028,6 @@ "node": ">=0.10.0" } }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2516,52 +2109,70 @@ "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", "dev": true, "requires": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + } + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "dev": true + }, + "@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true + }, + "@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" } }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true - }, - "@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "dev": true }, - "@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "requires": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true - }, - "strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "requires": { - "ansi-regex": "^6.0.1" - } - } + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "@nodelib/fs.scandir": { @@ -2590,13 +2201,219 @@ "fastq": "^1.6.0" } }, - "@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "@rollup/plugin-commonjs": { + "version": "28.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.0.tgz", + "integrity": "sha512-BJcu+a+Mpq476DMXG+hevgPSl56bkUoi88dKT8t3RyUp8kGuOh+2bU8Gs7zXDlu+fyZggnJ+iOBGrb/O1SorYg==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.1.1", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^2.3.1" + }, + "dependencies": { + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + } + } + }, + "@rollup/plugin-node-resolve": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.0.tgz", + "integrity": "sha512-9eO5McEICxMzJpDW9OnMYSv4Sta3hmt7VtBFz5zR9273suNOydOyq/FrGeGy+KsTRFm8w0SLVhzig2ILFT63Ag==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + } + }, + "@rollup/plugin-terser": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "dev": true, + "requires": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "dependencies": { + "terser": { + "version": "5.34.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.34.1.tgz", + "integrity": "sha512-FsJZ7iZLd/BXkz+4xrRTGJ26o/6VTjQytUk8b8OxkwcD2I+79VPJlz7qss1+zE7h8GNIScFqXcDyJ/KqBYZFVA==", + "dev": true, + "requires": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + } + } + } + }, + "@rollup/pluginutils": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.2.tgz", + "integrity": "sha512-/FIdS3PyZ39bjZlwqFnWqCOVnW7o963LtKMwQOD0NhQqw22gSr2YY1afu3FxRip4ZCZNsD5jq6Aaz6QV3D/Njw==", + "dev": true, + "requires": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "dependencies": { + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + } + } + }, + "@rollup/rollup-android-arm-eabi": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", + "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-android-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", + "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", + "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-x64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", + "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", + "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm-musleabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", + "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", + "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", + "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", + "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-riscv64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", + "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-s390x-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", + "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", + "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-arm64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", + "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-ia32-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", + "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", "dev": true, "optional": true }, + "@rollup/rollup-win32-x64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", + "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", + "dev": true, + "optional": true + }, + "@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, + "@types/node": { + "version": "22.8.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.7.tgz", + "integrity": "sha512-LidcG+2UeYIWcMuMUpBKOnryBWG/rnmOHQR5apjn8myTQcx3rinFRn7DcIFhMnS0PPFSC6OafdIKEad0lj6U0Q==", + "dev": true, + "requires": { + "undici-types": "~6.19.8" + } + }, + "@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true + }, "@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -2634,31 +2451,12 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, - "benchmark": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz", - "integrity": "sha1-CfPeMckWQl1JjMLuVloOvzwqVik=", - "dev": true, - "requires": { - "lodash": "^4.17.4", - "platform": "^1.3.3" - } - }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2681,25 +2479,6 @@ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - } - } - }, "chokidar": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", @@ -2709,54 +2488,24 @@ "readdirp": "^4.0.1" } }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, "commander": { "version": "2.20.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.1.tgz", "integrity": "sha512-cCuLsMhJeWQ/ZpsFTbE765kvVfoeSddc4nU3up4fV+fDBcfUXnbITJ+JzhkdjzOqhURjZgujxaioam4RM9yGUg==", "dev": true }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "dependencies": { - "semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true - } - } - }, "debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", @@ -2772,14 +2521,11 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "requires": { - "object-keys": "^1.0.12" - } + "deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true }, "doctrine": { "version": "3.0.0", @@ -2790,56 +2536,6 @@ "esutils": "^2.0.2" } }, - "eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true - }, - "emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "es-abstract": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.15.0.tgz", - "integrity": "sha512-bhkEqWJ2t2lMeaJDuk7okMkJWI/yqgH/EoGwpcvv0XW9RWQsRspI4wt6xuyuvMvvQE3gg/D9HXppgk21w78GyQ==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.0", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.0", - "is-callable": "^1.1.4", - "is-regex": "^1.0.4", - "object-inspect": "^1.6.0", - "object-keys": "^1.1.1", - "string.prototype.trimleft": "^2.1.0", - "string.prototype.trimright": "^2.1.0" - } - }, - "es-to-primitive": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", - "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", - "dev": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, "eslint": { "version": "8.57.1", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", @@ -3098,6 +2794,12 @@ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true }, + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -3131,6 +2833,13 @@ "reusify": "^1.0.4" } }, + "fdir": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.0.tgz", + "integrity": "sha512-3oB133prH1o4j/L5lLW7uOCF1PlD+/It2L0eL/iAqWMB91RBbqTewABqxhj0ibBd90EEmWZq7ntIWzVaWcXTGQ==", + "dev": true, + "requires": {} + }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -3191,126 +2900,24 @@ "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", "dev": true }, - "foreground-child": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "dependencies": { - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "glob": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", - "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, - "requires": { - "foreground-child": "^3.1.0", - "jackspeak": "^4.0.1", - "minimatch": "^10.0.0", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "lru-cache": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.1.tgz", - "integrity": "sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==", - "dev": true - }, - "minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - }, - "minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true - }, - "path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", - "dev": true, - "requires": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - } - } - } + "optional": true + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true }, "globals": { "version": "13.24.0", @@ -3321,45 +2928,21 @@ "type-fest": "^0.20.2" } }, - "graceful-fs": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", - "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==", - "dev": true - }, "graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, "requires": { - "function-bind": "^1.1.1" + "function-bind": "^1.1.2" } }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", - "dev": true - }, - "hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, "ignore": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", @@ -3398,23 +2981,14 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "is-callable": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", - "dev": true - }, - "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", - "dev": true + "is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "requires": { + "hasown": "^2.0.2" + } }, "is-extglob": { "version": "2.1.1", @@ -3431,28 +3005,25 @@ "is-extglob": "^2.1.1" } }, + "is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true + }, "is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true }, - "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "dev": true, - "requires": { - "has": "^1.0.1" - } - }, - "is-symbol": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", - "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", "dev": true, "requires": { - "has-symbols": "^1.0.0" + "@types/estree": "*" } }, "isexe": { @@ -3461,22 +3032,6 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, - "jackspeak": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz", - "integrity": "sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==", - "dev": true, - "requires": { - "@isaacs/cliui": "^8.0.2", - "@pkgjs/parseargs": "^0.11.0" - } - }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -3489,26 +3044,6 @@ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, - "load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - } - } - }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -3518,12 +3053,6 @@ "p-locate": "^5.0.0" } }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -3536,11 +3065,14 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true }, - "memorystream": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", - "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=", - "dev": true + "magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } }, "minimatch": { "version": "3.1.2", @@ -3569,70 +3101,6 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - }, - "dependencies": { - "resolve": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", - "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==", - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } - }, - "semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true - } - } - }, - "npm-run-all": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", - "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "chalk": "^2.4.1", - "cross-spawn": "^6.0.5", - "memorystream": "^0.3.1", - "minimatch": "^3.0.4", - "pidtree": "^0.3.0", - "read-pkg": "^3.0.0", - "shell-quote": "^1.6.1", - "string.prototype.padend": "^3.0.0" - } - }, - "object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "dev": true - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true - }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -3701,12 +3169,6 @@ "p-limit": "^3.0.2" } }, - "package-json-from-dist": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", - "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", - "dev": true - }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -3716,16 +3178,6 @@ "callsites": "^3.0.0" } }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3738,12 +3190,6 @@ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, "path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -3768,17 +3214,13 @@ } } }, - "pidtree": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.0.tgz", - "integrity": "sha512-9CT4NFlDcosssyg8KVFltgokyKZIFjoBxw8CTGy+5F38Y1eQWrt8tRayiUOXE+zVKQnYu5BR8JjCtvK3BcnBhg==", - "dev": true - }, - "platform": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.5.tgz", - "integrity": "sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q==", - "dev": true + "picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "optional": true, + "peer": true }, "punycode": { "version": "2.3.1", @@ -3792,32 +3234,13 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, - "read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, "requires": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - }, - "dependencies": { - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - } + "safe-buffer": "^5.1.0" } }, "readdirp": { @@ -3826,6 +3249,17 @@ "integrity": "sha512-GkMg9uOTpIWWKbSsgwb5fA4EavTR+SG/PMPoAY8hkhHfEEY0/vqljY+XHqtDf2cr2IJtoNRDbrrEpZUiZCkYRw==", "dev": true }, + "resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3838,14 +3272,30 @@ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true }, - "rimraf": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", - "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", - "dev": true, - "requires": { - "glob": "^11.0.0", - "package-json-from-dist": "^1.0.0" + "rollup": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", + "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", + "dev": true, + "requires": { + "@rollup/rollup-android-arm-eabi": "4.24.0", + "@rollup/rollup-android-arm64": "4.24.0", + "@rollup/rollup-darwin-arm64": "4.24.0", + "@rollup/rollup-darwin-x64": "4.24.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", + "@rollup/rollup-linux-arm-musleabihf": "4.24.0", + "@rollup/rollup-linux-arm64-gnu": "4.24.0", + "@rollup/rollup-linux-arm64-musl": "4.24.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", + "@rollup/rollup-linux-riscv64-gnu": "4.24.0", + "@rollup/rollup-linux-s390x-gnu": "4.24.0", + "@rollup/rollup-linux-x64-gnu": "4.24.0", + "@rollup/rollup-linux-x64-musl": "4.24.0", + "@rollup/rollup-win32-arm64-msvc": "4.24.0", + "@rollup/rollup-win32-ia32-msvc": "4.24.0", + "@rollup/rollup-win32-x64-msvc": "4.24.0", + "@types/estree": "1.0.6", + "fsevents": "~2.3.2" } }, "run-parallel": { @@ -3857,31 +3307,25 @@ "queue-microtask": "^1.2.2" } }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + }, + "serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "requires": { - "shebang-regex": "^1.0.0" + "randombytes": "^2.1.0" } }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "shell-quote": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", - "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", - "dev": true - }, - "signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "smob": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", + "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", "dev": true }, "source-map": { @@ -3900,122 +3344,6 @@ "source-map": "^0.6.0" } }, - "spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", - "dev": true - }, - "string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "requires": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true - }, - "strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "requires": { - "ansi-regex": "^6.0.1" - } - } - } - }, - "string-width-cjs": { - "version": "npm:string-width@4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "dependencies": { - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - } - } - }, - "string.prototype.padend": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz", - "integrity": "sha1-86rvfBcZ8XDF6rHDK/eA2W4h8vA=", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.4.3", - "function-bind": "^1.0.2" - } - }, - "string.prototype.trimleft": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz", - "integrity": "sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - }, - "string.prototype.trimright": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz", - "integrity": "sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -4025,35 +3353,17 @@ "ansi-regex": "^5.0.1" } }, - "strip-ansi-cjs": { - "version": "npm:strip-ansi@6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true }, "terser": { "version": "4.8.1", @@ -4078,6 +3388,12 @@ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true }, + "undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, "uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -4087,125 +3403,12 @@ "punycode": "^2.1.0" } }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, "word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true }, - "wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "requires": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true - }, - "ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true - }, - "strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "requires": { - "ansi-regex": "^6.0.1" - } - } - } - }, - "wrap-ansi-cjs": { - "version": "npm:wrap-ansi@7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - } - } - }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index 70bcde9e3..6362b86d4 100644 --- a/package.json +++ b/package.json @@ -2,34 +2,51 @@ "name": "mithril", "version": "2.2.9", "description": "A framework for building brilliant applications", - "author": "Leo Horie", + "contributors": [ + "Leo Horie (https://github.com/lhorie)", + "Claudia Meadows (https://github.com/dead-claudia)" + ], "license": "MIT", - "unpkg": "mithril.min.js", - "jsdelivr": "mithril.min.js", + "type": "module", + "unpkg": "dist/mithril.umd.min.js", + "jsdelivr": "dist/mithril.umd.min.js", "repository": "github:MithrilJS/mithril.js", + "main": "dist/mithril.umd.js", + "module": "dist/mithril.esm.js", + "files": [ + "/dist/**" + ], + "exports": { + "./stream.js": { + "import": "./dist/stream.esm.js", + "require": "./dist/stream.umd.js", + "production": { + "import": "./dist/stream.esm.min.js" + } + }, + ".": { + "import": "./dist/mithril.esm.js", + "require": "./dist/mithril.umd.js", + "production": { + "import": "./dist/mithril.esm.min.js" + } + } + }, "scripts": { - "watch": "run-p watch:**", - "watch:js": "node scripts/bundler browser.js -output mithril.js -watch", - "build": "run-p build:browser build:min build:stream-min", - "build:browser": "node scripts/bundler browser.js -output mithril.js", - "build:min": "node scripts/bundler browser.js -output mithril.min.js -minify -save", - "build:stream-min": "node scripts/minify-stream", - "cleanup:lint": "rimraf .eslintcache", - "lint": "run-s -cn lint:**", - "lint:js": "eslint . --cache", - "perf": "node performance/test-perf.js", + "build": "node scripts/build.js", + "lint": "eslint . --cache", "pretest": "npm run lint", - "test": "run-s test:js", - "test:js": "ospec" + "test": "ospec" }, "devDependencies": { - "benchmark": "^2.1.4", + "@rollup/plugin-commonjs": "^28.0.0", + "@rollup/plugin-node-resolve": "^15.3.0", + "@rollup/plugin-terser": "^0.4.4", + "@types/node": "^22.8.7", "chokidar": "^4.0.1", "eslint": "^8.9.0", - "glob": "^11.0.0", - "npm-run-all": "^4.1.5", "ospec": "4.2.1", - "rimraf": "^6.0.1", + "rollup": "^4.24.0", "terser": "^4.3.4" } } diff --git a/pathname/build.js b/pathname/build.js deleted file mode 100644 index 3cd033c3e..000000000 --- a/pathname/build.js +++ /dev/null @@ -1,42 +0,0 @@ -"use strict" - -var buildQueryString = require("../querystring/build") - -// Returns `path` from `template` + `params` -module.exports = function(template, params) { - if ((/:([^\/\.-]+)(\.{3})?:/).test(template)) { - throw new SyntaxError("Template parameter names must be separated by either a '/', '-', or '.'.") - } - if (params == null) return template - var queryIndex = template.indexOf("?") - var hashIndex = template.indexOf("#") - var queryEnd = hashIndex < 0 ? template.length : hashIndex - var pathEnd = queryIndex < 0 ? queryEnd : queryIndex - var path = template.slice(0, pathEnd) - var query = {} - - Object.assign(query, params) - - var resolved = path.replace(/:([^\/\.-]+)(\.{3})?/g, function(m, key, variadic) { - delete query[key] - // If no such parameter exists, don't interpolate it. - if (params[key] == null) return m - // Escape normal parameters, but not variadic ones. - return variadic ? params[key] : encodeURIComponent(String(params[key])) - }) - - // In case the template substitution adds new query/hash parameters. - var newQueryIndex = resolved.indexOf("?") - var newHashIndex = resolved.indexOf("#") - var newQueryEnd = newHashIndex < 0 ? resolved.length : newHashIndex - var newPathEnd = newQueryIndex < 0 ? newQueryEnd : newQueryIndex - var result = resolved.slice(0, newPathEnd) - - if (queryIndex >= 0) result += template.slice(queryIndex, queryEnd) - if (newQueryIndex >= 0) result += (queryIndex < 0 ? "?" : "&") + resolved.slice(newQueryIndex, newQueryEnd) - var querystring = buildQueryString(query) - if (querystring) result += (queryIndex < 0 && newQueryIndex < 0 ? "?" : "&") + querystring - if (hashIndex >= 0) result += template.slice(hashIndex) - if (newHashIndex >= 0) result += (hashIndex < 0 ? "" : "&") + resolved.slice(newHashIndex) - return result -} diff --git a/pathname/compileTemplate.js b/pathname/compileTemplate.js deleted file mode 100644 index 390c1e568..000000000 --- a/pathname/compileTemplate.js +++ /dev/null @@ -1,43 +0,0 @@ -"use strict" - -var parsePathname = require("./parse") - -// Compiles a template into a function that takes a resolved path (without query -// strings) and returns an object containing the template parameters with their -// parsed values. This expects the input of the compiled template to be the -// output of `parsePathname`. Note that it does *not* remove query parameters -// specified in the template. -module.exports = function(template) { - var templateData = parsePathname(template) - var templateKeys = Object.keys(templateData.params) - var keys = [] - var regexp = new RegExp("^" + templateData.path.replace( - // I escape literal text so people can use things like `:file.:ext` or - // `:lang-:locale` in routes. This is all merged into one pass so I - // don't also accidentally escape `-` and make it harder to detect it to - // ban it from template parameters. - /:([^\/.-]+)(\.{3}|\.(?!\.)|-)?|[\\^$*+.()|\[\]{}]/g, - function(m, key, extra) { - if (key == null) return "\\" + m - keys.push({k: key, r: extra === "..."}) - if (extra === "...") return "(.*)" - if (extra === ".") return "([^/]+)\\." - return "([^/]+)" + (extra || "") - } - ) + "$") - return function(data) { - // First, check the params. Usually, there isn't any, and it's just - // checking a static set. - for (var i = 0; i < templateKeys.length; i++) { - if (templateData.params[templateKeys[i]] !== data.params[templateKeys[i]]) return false - } - // If no interpolations exist, let's skip all the ceremony - if (!keys.length) return regexp.test(data.path) - var values = regexp.exec(data.path) - if (values == null) return false - for (var i = 0; i < keys.length; i++) { - data.params[keys[i].k] = keys[i].r ? values[i + 1] : decodeURIComponent(values[i + 1]) - } - return true - } -} diff --git a/pathname/parse.js b/pathname/parse.js deleted file mode 100644 index a7e97fae0..000000000 --- a/pathname/parse.js +++ /dev/null @@ -1,23 +0,0 @@ -"use strict" - -var parseQueryString = require("../querystring/parse") - -// Returns `{path, params}` from `url` -module.exports = function(url) { - var queryIndex = url.indexOf("?") - var hashIndex = url.indexOf("#") - var queryEnd = hashIndex < 0 ? url.length : hashIndex - var pathEnd = queryIndex < 0 ? queryEnd : queryIndex - var path = url.slice(0, pathEnd).replace(/\/{2,}/g, "/") - - if (!path) path = "/" - else { - if (path[0] !== "/") path = "/" + path - } - return { - path: path, - params: queryIndex < 0 - ? {} - : parseQueryString(url.slice(queryIndex + 1, queryEnd)), - } -} diff --git a/pathname/tests/test-buildPathname.js b/pathname/tests/test-buildPathname.js deleted file mode 100644 index 0b763d154..000000000 --- a/pathname/tests/test-buildPathname.js +++ /dev/null @@ -1,124 +0,0 @@ -"use strict" - -var o = require("ospec") -var buildPathname = require("../../pathname/build") - -o.spec("buildPathname", function() { - function test(prefix) { - o("returns path if no params", function () { - var string = buildPathname(prefix + "/route/foo", undefined) - - o(string).equals(prefix + "/route/foo") - }) - o("skips interpolation if no params", function () { - var string = buildPathname(prefix + "/route/:id", undefined) - - o(string).equals(prefix + "/route/:id") - }) - o("appends query strings", function () { - var string = buildPathname(prefix + "/route/foo", {a: "b", c: 1}) - - o(string).equals(prefix + "/route/foo?a=b&c=1") - }) - o("inserts template parameters at end", function () { - var string = buildPathname(prefix + "/route/:id", {id: "1"}) - - o(string).equals(prefix + "/route/1") - }) - o("inserts template parameters at beginning", function () { - var string = buildPathname(prefix + "/:id/foo", {id: "1"}) - - o(string).equals(prefix + "/1/foo") - }) - o("inserts template parameters at middle", function () { - var string = buildPathname(prefix + "/route/:id/foo", {id: "1"}) - - o(string).equals(prefix + "/route/1/foo") - }) - o("inserts variadic paths", function () { - var string = buildPathname(prefix + "/route/:foo...", {foo: "id/1"}) - - o(string).equals(prefix + "/route/id/1") - }) - o("inserts variadic paths with initial slashes", function () { - var string = buildPathname(prefix + "/route/:foo...", {foo: "/id/1"}) - - o(string).equals(prefix + "/route//id/1") - }) - o("skips template parameters at end if param missing", function () { - var string = buildPathname(prefix + "/route/:id", {param: 1}) - - o(string).equals(prefix + "/route/:id?param=1") - }) - o("skips template parameters at beginning if param missing", function () { - var string = buildPathname(prefix + "/:id/foo", {param: 1}) - - o(string).equals(prefix + "/:id/foo?param=1") - }) - o("skips template parameters at middle if param missing", function () { - var string = buildPathname(prefix + "/route/:id/foo", {param: 1}) - - o(string).equals(prefix + "/route/:id/foo?param=1") - }) - o("skips variadic template parameters if param missing", function () { - var string = buildPathname(prefix + "/route/:foo...", {param: "/id/1"}) - - o(string).equals(prefix + "/route/:foo...?param=%2Fid%2F1") - }) - o("handles escaped values", function() { - var data = buildPathname(prefix + "/route/:foo", {"foo": ";:@&=+$,/?%#"}) - - o(data).equals(prefix + "/route/%3B%3A%40%26%3D%2B%24%2C%2F%3F%25%23") - }) - o("handles unicode", function() { - var data = buildPathname(prefix + "/route/:ΓΆ", {"ΓΆ": "ΓΆ"}) - - o(data).equals(prefix + "/route/%C3%B6") - }) - o("handles zero", function() { - var string = buildPathname(prefix + "/route/:a", {a: 0}) - - o(string).equals(prefix + "/route/0") - }) - o("handles false", function() { - var string = buildPathname(prefix + "/route/:a", {a: false}) - - o(string).equals(prefix + "/route/false") - }) - o("handles dashes", function() { - var string = buildPathname(prefix + "/:lang-:region/route", { - lang: "en", - region: "US" - }) - - o(string).equals(prefix + "/en-US/route") - }) - o("handles dots", function() { - var string = buildPathname(prefix + "/:file.:ext/view", { - file: "image", - ext: "png" - }) - - o(string).equals(prefix + "/image.png/view") - }) - o("merges query strings", function() { - var string = buildPathname(prefix + "/item?a=1&b=2", {c: 3}) - - o(string).equals(prefix + "/item?a=1&b=2&c=3") - }) - o("merges query strings with other parameters", function() { - var string = buildPathname(prefix + "/item/:id?a=1&b=2", {id: "foo", c: 3}) - - o(string).equals(prefix + "/item/foo?a=1&b=2&c=3") - }) - o("consumes template parameters without modifying query string", function() { - var string = buildPathname(prefix + "/item/:id?a=1&b=2", {id: "foo"}) - - o(string).equals(prefix + "/item/foo?a=1&b=2") - }) - } - o.spec("absolute", function() { test("") }) - o.spec("relative", function() { test("..") }) - o.spec("absolute + domain", function() { test("https://example.com") }) - o.spec("absolute + `file:`", function() { test("file://") }) -}) diff --git a/pathname/tests/test-compileTemplate.js b/pathname/tests/test-compileTemplate.js deleted file mode 100644 index e9725b06c..000000000 --- a/pathname/tests/test-compileTemplate.js +++ /dev/null @@ -1,221 +0,0 @@ -"use strict" - -var o = require("ospec") -var parsePathname = require("../../pathname/parse") -var compileTemplate = require("../../pathname/compileTemplate") - -o.spec("compileTemplate", function() { - o("checks empty string", function() { - var data = parsePathname("/") - o(compileTemplate("/")(data)).equals(true) - o(data.params).deepEquals({}) - }) - o("checks identical match", function() { - var data = parsePathname("/foo") - o(compileTemplate("/foo")(data)).equals(true) - o(data.params).deepEquals({}) - }) - o("checks identical mismatch", function() { - var data = parsePathname("/bar") - o(compileTemplate("/foo")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks single parameter", function() { - var data = parsePathname("/1") - o(compileTemplate("/:id")(data)).equals(true) - o(data.params).deepEquals({id: "1"}) - }) - o("checks single variadic parameter", function() { - var data = parsePathname("/some/path") - o(compileTemplate("/:id...")(data)).equals(true) - o(data.params).deepEquals({id: "some/path"}) - }) - o("checks single parameter with extra match", function() { - var data = parsePathname("/1/foo") - o(compileTemplate("/:id/foo")(data)).equals(true) - o(data.params).deepEquals({id: "1"}) - }) - o("checks single parameter with extra mismatch", function() { - var data = parsePathname("/1/bar") - o(compileTemplate("/:id/foo")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks single variadic parameter with extra match", function() { - var data = parsePathname("/some/path/foo") - o(compileTemplate("/:id.../foo")(data)).equals(true) - o(data.params).deepEquals({id: "some/path"}) - }) - o("checks single variadic parameter with extra mismatch", function() { - var data = parsePathname("/some/path/bar") - o(compileTemplate("/:id.../foo")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks multiple parameters", function() { - var data = parsePathname("/1/2") - o(compileTemplate("/:id/:name")(data)).equals(true) - o(data.params).deepEquals({id: "1", name: "2"}) - }) - o("checks incomplete multiple parameters", function() { - var data = parsePathname("/1") - o(compileTemplate("/:id/:name")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks multiple parameters with extra match", function() { - var data = parsePathname("/1/2/foo") - o(compileTemplate("/:id/:name/foo")(data)).equals(true) - o(data.params).deepEquals({id: "1", name: "2"}) - }) - o("checks multiple parameters with extra mismatch", function() { - var data = parsePathname("/1/2/bar") - o(compileTemplate("/:id/:name/foo")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks multiple parameters, last variadic, with extra match", function() { - var data = parsePathname("/1/some/path/foo") - o(compileTemplate("/:id/:name.../foo")(data)).equals(true) - o(data.params).deepEquals({id: "1", name: "some/path"}) - }) - o("checks multiple parameters, last variadic, with extra mismatch", function() { - var data = parsePathname("/1/some/path/bar") - o(compileTemplate("/:id/:name.../foo")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks multiple separated parameters", function() { - var data = parsePathname("/1/sep/2") - o(compileTemplate("/:id/sep/:name")(data)).equals(true) - o(data.params).deepEquals({id: "1", name: "2"}) - }) - o("checks incomplete multiple separated parameters", function() { - var data = parsePathname("/1") - o(compileTemplate("/:id/sep/:name")(data)).equals(false) - o(data.params).deepEquals({}) - data = parsePathname("/1/sep") - o(compileTemplate("/:id/sep/:name")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks multiple separated parameters missing sep", function() { - var data = parsePathname("/1/2") - o(compileTemplate("/:id/sep/:name")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks multiple separated parameters with extra match", function() { - var data = parsePathname("/1/sep/2/foo") - o(compileTemplate("/:id/sep/:name/foo")(data)).equals(true) - o(data.params).deepEquals({id: "1", name: "2"}) - }) - o("checks multiple separated parameters with extra mismatch", function() { - var data = parsePathname("/1/sep/2/bar") - o(compileTemplate("/:id/sep/:name/foo")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks multiple separated parameters, last variadic, with extra match", function() { - var data = parsePathname("/1/sep/some/path/foo") - o(compileTemplate("/:id/sep/:name.../foo")(data)).equals(true) - o(data.params).deepEquals({id: "1", name: "some/path"}) - }) - o("checks multiple separated parameters, last variadic, with extra mismatch", function() { - var data = parsePathname("/1/sep/some/path/bar") - o(compileTemplate("/:id/sep/:name.../foo")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks multiple parameters + prefix", function() { - var data = parsePathname("/route/1/2") - o(compileTemplate("/route/:id/:name")(data)).equals(true) - o(data.params).deepEquals({id: "1", name: "2"}) - }) - o("checks incomplete multiple parameters + prefix", function() { - var data = parsePathname("/route/1") - o(compileTemplate("/route/:id/:name")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks multiple parameters + prefix with extra match", function() { - var data = parsePathname("/route/1/2/foo") - o(compileTemplate("/route/:id/:name/foo")(data)).equals(true) - o(data.params).deepEquals({id: "1", name: "2"}) - }) - o("checks multiple parameters + prefix with extra mismatch", function() { - var data = parsePathname("/route/1/2/bar") - o(compileTemplate("/route/:id/:name/foo")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks multiple parameters + prefix, last variadic, with extra match", function() { - var data = parsePathname("/route/1/some/path/foo") - o(compileTemplate("/route/:id/:name.../foo")(data)).equals(true) - o(data.params).deepEquals({id: "1", name: "some/path"}) - }) - o("checks multiple parameters + prefix, last variadic, with extra mismatch", function() { - var data = parsePathname("/route/1/some/path/bar") - o(compileTemplate("/route/:id/:name.../foo")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks multiple separated parameters + prefix", function() { - var data = parsePathname("/route/1/sep/2") - o(compileTemplate("/route/:id/sep/:name")(data)).equals(true) - o(data.params).deepEquals({id: "1", name: "2"}) - }) - o("checks incomplete multiple separated parameters + prefix", function() { - var data = parsePathname("/route/1") - o(compileTemplate("/route/:id/sep/:name")(data)).equals(false) - o(data.params).deepEquals({}) - var data = parsePathname("/route/1/sep") - o(compileTemplate("/route/:id/sep/:name")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks multiple separated parameters + prefix missing sep", function() { - var data = parsePathname("/route/1/2") - o(compileTemplate("/route/:id/sep/:name")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks multiple separated parameters + prefix with extra match", function() { - var data = parsePathname("/route/1/sep/2/foo") - o(compileTemplate("/route/:id/sep/:name/foo")(data)).equals(true) - o(data.params).deepEquals({id: "1", name: "2"}) - }) - o("checks multiple separated parameters + prefix with extra mismatch", function() { - var data = parsePathname("/route/1/sep/2/bar") - o(compileTemplate("/route/:id/sep/:name/foo")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks multiple separated parameters + prefix, last variadic, with extra match", function() { - var data = parsePathname("/route/1/sep/some/path/foo") - o(compileTemplate("/route/:id/sep/:name.../foo")(data)).equals(true) - o(data.params).deepEquals({id: "1", name: "some/path"}) - }) - o("checks multiple separated parameters + prefix, last variadic, with extra mismatch", function() { - var data = parsePathname("/route/1/sep/some/path/bar") - o(compileTemplate("/route/:id/sep/:name.../foo")(data)).equals(false) - o(data.params).deepEquals({}) - }) - o("checks query params match", function() { - var data = parsePathname("/route/1?foo=bar") - o(compileTemplate("/route/:id?foo=bar")(data)).equals(true) - o(data.params).deepEquals({id: "1", foo: "bar"}) - }) - o("checks query params mismatch", function() { - var data = parsePathname("/route/1?foo=bar") - o(compileTemplate("/route/:id?foo=1")(data)).equals(false) - o(data.params).deepEquals({foo: "bar"}) - o(compileTemplate("/route/:id?bar=foo")(data)).equals(false) - o(data.params).deepEquals({foo: "bar"}) - }) - o("checks dot before dot", function() { - var data = parsePathname("/file.test.png/edit") - o(compileTemplate("/:file.:ext/edit")(data)).equals(true) - o(data.params).deepEquals({file: "file.test", ext: "png"}) - }) - o("checks dash before dot", function() { - var data = parsePathname("/file-test.png/edit") - o(compileTemplate("/:file.:ext/edit")(data)).equals(true) - o(data.params).deepEquals({file: "file-test", ext: "png"}) - }) - o("checks dot before dash", function() { - var data = parsePathname("/file.test-png/edit") - o(compileTemplate("/:file-:ext/edit")(data)).equals(true) - o(data.params).deepEquals({file: "file.test", ext: "png"}) - }) - o("checks dash before dash", function() { - var data = parsePathname("/file-test-png/edit") - o(compileTemplate("/:file-:ext/edit")(data)).equals(true) - o(data.params).deepEquals({file: "file-test", ext: "png"}) - }) -}) diff --git a/pathname/tests/test-parsePathname.js b/pathname/tests/test-parsePathname.js deleted file mode 100644 index 03d7c1b67..000000000 --- a/pathname/tests/test-parsePathname.js +++ /dev/null @@ -1,126 +0,0 @@ -"use strict" - -var o = require("ospec") -var parsePathname = require("../../pathname/parse") - -o.spec("parsePathname", function() { - o("parses empty string", function() { - var data = parsePathname("") - o(data).deepEquals({ - path: "/", - params: {} - }) - }) - o("parses query at start", function() { - var data = parsePathname("?a=b&c=d") - o(data).deepEquals({ - path: "/", - params: {a: "b", c: "d"} - }) - }) - o("ignores hash at start", function() { - var data = parsePathname("#a=b&c=d") - o(data).deepEquals({ - path: "/", - params: {} - }) - }) - o("parses query, ignores hash at start", function() { - var data = parsePathname("?a=1&b=2#c=3&d=4") - o(data).deepEquals({ - path: "/", - params: {a: "1", b: "2"} - }) - }) - o("parses root", function() { - var data = parsePathname("/") - o(data).deepEquals({ - path: "/", - params: {} - }) - }) - o("parses root + query at start", function() { - var data = parsePathname("/?a=b&c=d") - o(data).deepEquals({ - path: "/", - params: {a: "b", c: "d"} - }) - }) - o("parses root, ignores hash at start", function() { - var data = parsePathname("/#a=b&c=d") - o(data).deepEquals({ - path: "/", - params: {} - }) - }) - o("parses root + query, ignores hash at start", function() { - var data = parsePathname("/?a=1&b=2#c=3&d=4") - o(data).deepEquals({ - path: "/", - params: {a: "1", b: "2"} - }) - }) - o("parses route", function() { - var data = parsePathname("/route/foo") - o(data).deepEquals({ - path: "/route/foo", - params: {} - }) - }) - o("parses route + empty query", function() { - var data = parsePathname("/route/foo?") - o(data).deepEquals({ - path: "/route/foo", - params: {} - }) - }) - o("parses route + empty hash", function() { - var data = parsePathname("/route/foo?") - o(data).deepEquals({ - path: "/route/foo", - params: {} - }) - }) - o("parses route + empty query + empty hash", function() { - var data = parsePathname("/route/foo?#") - o(data).deepEquals({ - path: "/route/foo", - params: {} - }) - }) - o("parses route + query", function() { - var data = parsePathname("/route/foo?a=1&b=2") - o(data).deepEquals({ - path: "/route/foo", - params: {a: "1", b: "2"} - }) - }) - o("parses route + hash", function() { - var data = parsePathname("/route/foo?c=3&d=4") - o(data).deepEquals({ - path: "/route/foo", - params: {c: "3", d: "4"} - }) - }) - o("parses route + query, ignores hash", function() { - var data = parsePathname("/route/foo?a=1&b=2#c=3&d=4") - o(data).deepEquals({ - path: "/route/foo", - params: {a: "1", b: "2"} - }) - }) - o("parses route + query, ignores hash with lots of junk slashes", function() { - var data = parsePathname("//route/////foo//?a=1&b=2#c=3&d=4") - o(data).deepEquals({ - path: "/route/foo/", - params: {a: "1", b: "2"} - }) - }) - o("doesn't comprehend protocols", function() { - var data = parsePathname("https://example.com/foo/bar") - o(data).deepEquals({ - path: "/https:/example.com/foo/bar", - params: {} - }) - }) -}) diff --git a/performance/README.md b/performance/README.md new file mode 100644 index 000000000..267c3ad35 --- /dev/null +++ b/performance/README.md @@ -0,0 +1,727 @@ +# Benchmarks + +## Usage + +Usage is extremely simple and designed to put the ultimate focus on the benchmark code itself: + +```js +import {setupBenchmarks} from "./bench.js" + +async function setup() { + // Test suite setup. May be async. +} + +async function cleanup() { + // Post-benchmark cleanup. May be async and always performed, even on error. +} + +await setupBenchmarks(setup, cleanup, { + "foo": { + // Before every measurement interval. `fn` may be called multiple times in a single + // interval, so be aware. + tick() { + // ... + }, + // The actual benchmarked code. Be sure to return a result so it doesn't get optimized out. + fn() { + // ... + }, + }, +}) +``` + +Then, make sure the script is loaded in an HTML file like follows: + +```html + + + + + Performance tests + + + + Initializing tests. If this text doesn't change, open the browser console and check for + load errors. + + +``` + +To run the benchmarks: + +1. Spin up a server using `node scripts/server.js` in the repo root. +2. Open or whatever the path to your benchmark is. +3. Run the tests and wait for them to complete. They'll then show a bunch of benchmark graphs for detailed inspection. If you want a simpler view without all the graphs or if you just want to copy all the samples out, open the console. + +## Rationale + +Why are we using a home-rolled system instead of a pre-made framework? Two reasons: + +1. It won't have maintenance problems. If there's an issue with the benchmark, it can be patched in-repo. +2. I can better account for overhead. +3. It correctly accounts for timer accuracy problems. + +Things it does better than Benchmark.js: + +- It uses an exposed benchmark loop, so I can precisely control inter-frame delays. +- It prints out much more useful output: a confidence interval-based range and a total run count. +- It works around the low resolution inherent to modern browsers. + +Before I go further, I'd like to explain some theory, theory that might not be obvious to most. + +### What timers actually measure + +Timers do *not* measure raw intervals directly. They simply measure the number of clock ticks that +have occurred since a given point of time. What benchmarks actually do when they compare two +timestamps is count the number of clock ticks that occurred between them. + +Operating systems hide this low-level detail by exposing a monotonic "clock". This clock is based +on two things: a CPU cycle counter and the CPU's current frequency. It optionally can also track +system sleep time using a built-in counter (usually plugged into a small quartz crystal). "Sleep" +here just means the CPU is in a low-power state, clock disabled, while waiting for either I/O or a +hardware timer to go off, not that the system itself is physically powered off. While it's useful +to expose this as a duration offset (usually relative to some point during the boot process), this +is only an abstraction. + +### Browser interactions + +Browsers, for privacy and security reasons, limit the granularity of timers. + +- https://w3c.github.io/hr-time/#dfn-coarsen-time +- https://github.com/w3c/hr-time/issues/79 +- https://github.com/w3c/hr-time/issues/56 + +The spec requires a mandatory maximum of 20 microseconds of granularity for isolated frames, but +some browsers coarsen the timers even further: + +- Firefox: 20 microseconds +- Chrome: 100 microseconds with 100 microseconds jitter +- Safari: 1000 microseconds +- Edge pre-Chromium: 20 microseconds with 20 microseconds jitter +- Brave: 100 microseconds + +Once again, keep in mind, the monotonic clock is an abstraction. What this clock coarsening does +isn't directly reducing resolution. What it really does from a theoretical perspective is establish +a different kind of tick, one that's closer to that of a wall clock's ticking. It creates a clock +that ticks at the granularity interval, but relies on the OS's CPU frequency tracking to detect if +it's supposed to tick. (In practice, instead of having the OS directly manage that in memory, it +instead uses a mathematical formula to derive it. And this is also how it's actually specified.) + +The induced jitter some browsers use mean that there's some uncertainty, a chance the clock may +wait up to that long to tick. For instance, Chrome's clock may wait anywhere from 100 to 200 +microseconds to generate a tick. + +So, in reality, comparing times between two `performance.now()` calls is simply counting the number +of times the clock ticked. Or to put it another way, you could assume it's really doing something like this pseudocode, assuming no jitter: + +```js +// Firefox in isolated contexts +const resolution = 0.02 // milliseconds + +let ticks = 0 + +spawnThread(() => { + while (true) { + sleepMs(resolution) + ticks++ + } +}) + +performance.now = () => { + return ticks * resolution +} +``` + +> For performance reasons, it doesn't work like this in browsers, but you get the point. And it very much *does* work this way at the hardware and OS level. + +Jitter is more involved, as you'd need partial ticks. But it otherwise works similarly: + +```js +// Chrome in isolated contexts +const resolution = 0.1 // milliseconds +const jitter = 0.1 // milliseconds + +let ticks = 0 + +spawnThread(() => { + while (true) { + const jitterAmount = Math.random() + const jitterTicks = (resolution / jitter) * jitterAmount + sleepMs(resolution + jitter * jitterAmount) + ticks += 1 + jitterTicks + } +}) + +performance.now = () => { + return ticks * resolution +} +``` + +### How the coarseness impacts accuracy + +Durations in tests are usually measured like this: + +```js +const start = performance.now() +// ... +const end = performance.now() +const duration = end - start +``` + +Suppose resolution for the clock is 100 microseconds, and the span between the two calls is precisely 550 microseconds. You might expect to get a `duration` of `0.5` or `0.6` as that's the rounded duration. But this isn't necessarily the case, not with jitter involved. Let's go back to the timer-as-ticking-clock model to see how this could wildly differ. + +1. Get `start`. For the sake of example, let's let the tick be 10, resulting in a return value of `1.0`. +2. Ticker sleeps for 100 microseconds = 1 tick. Random jitter ends up as 28 microseconds = 0.28 ticks, so after it sleeps, it would have slept for a total of 1.28 ticks. +3. Ticker sleeps for 100 microseconds = 1 tick. Random jitter ends up as 52 microseconds = 0.52 ticks, so after it sleeps, it would have slept for a total of 2.8 ticks. +4. Ticker sleeps for 100 microseconds = 1 tick. Random jitter ends up as 44 microseconds = 0.44 ticks, so after it sleeps, it would have slept for a total of 4.24 ticks. +5. Ticker sleeps for 100 microseconds = 1 tick. Random jitter ends up as 88 microseconds = 0.88 ticks, so after it sleeps, it would have slept for a total of 6.12 ticks. +6. Block completes to `end = performance.now()`. The previous ticker sleep hasn't completed, so it returns based on a tick of 10 + 4.24 = 14.24, or a return value of `1.424`. +7. `duration` evaluates to `1.424 - 1.0` = `0.42399999999999993`. + +Browsers, in a quest for optimization, implement this a bit differently, and so for longer spans between successive `performance.now()` calls, you see less jitter. + +```js +const resolution = 0.1 // milliseconds +const jitter = 0.1 // milliseconds + +let lastTimestamp = -1 + +performance.now = () => { + const rawTimestamp = getUnsafeTimestamp() + const coarsened = Math.round(rawTimestamp / resolution) * resolution + const coarsenedWithJitter = coarsened + Math.random() * jitter + if (coarsenedWithJitter < lastTimestamp) return lastTimestamp + lastTimestamp = coarsenedWithJitter + return coarsenedWithJitter +} +``` + +> Chrome takes a subtly different approach (this assumes it's not cross-origin isolated, the case where it uses 100us + 100us jitter): +> +> ```js +> // This is approximate. Chrome operates on integer microseconds and uses a (weak) scrambler instead +> // of a proper (pseudo-)random number generator to compute its jitter. +> +> const resolution = 0.1 // milliseconds +> const jitter = 0.1 // milliseconds +> +> const clamperSecret = randomBigInt64() +> +> const clampedTimeOrigin = getClampedTimeOrigin() +> +> performance.now = () => { +> const rawTimestamp = getUnsafeTimestamp() +> const sign = Math.sign(rawTimestamp) +> const time = Math.abs(rawTimestamp) +> const lowerDigits = time % 10_000_000 +> const upperDigits = time - lowerDigits +> +> let clampedTime = lowerDigits - lowerDigits % resolution +> if (lowerDigits >= jitter * Math.random()) clampedTime += jitter +> clampedTime = (clampedTime + upperDigits) * sign +> return Math.max(clampedTime - clampedTimeOrigin, 0) +> } +> ``` +> +> Or, in math notation: +> - $R$ is the raw millisecond timestamp. +> - $O$ is the clamped time origin. +> - $j$ is the jitter for the returned timestamp. +> - $d$ is the result duration. +> +> $$ +> \begin{align*} +> R &\in \reals \\ +> j &\in [0, 1) \\ +> j & \text{ is uniformly random} \\ +> t &= |R| \\ +> l &= t - 1000000 \left\lfloor \frac{t}{1000000} \right\rfloor \\ +> d &= \text{max}\left(\text{sgn}(R)\left(t-100\left\lfloor \frac{l}{100} \right\rfloor+1_{[-\infty, l]}(0.1j)~0.1\right) - O\right) \\ +> \end{align*} +> $$ + +In this case, the sequence would look like this: + +1. Get `start`. For the sake of example, let's let the raw timestamp be `1.00` and assume the random value for the jitter comes out to `0.1`, thus giving us a return value of `1.01`. +2. System clock sleeps for sufficient nanoseconds to equal 550 microseconds. +3. Block completes to `end = performance.now()`. The raw timestamp would now be `1.55`. Let's assume the random value for the jitter is `0.8`, which would give us a value of `1.68`. +4. `duration` evaluates to `1.68-1.01` = `0.67`, representing a duration of 670 microseconds. + +In theory, `duration` could go as low as `0.45` (`0.95` + max jitter start, `1.50` + min jitter end) or as high as `0.65` (`1.00` + min jitter start, `1.55` + max jitter end), or an uncertainty of 10% with this model. + +Thing is, this is just with longer times between polls. In reality, we're often calling `performance.now()` multiple times in a single interval. Here's one way that could shake out with the above pseudocode, assuming the time between calls is 20 microseconds: + +1. Get `start`. For the sake of example, let's let the raw timestamp be `1.00` and assume the random value for the jitter comes out to `0.1`, thus giving us a return value of `1.01`. +2. System clock sleeps for sufficient nanoseconds to equal 20 microseconds. +3. Block completes to `end = performance.now()`. The raw timestamp would now be `1.02`. Let's assume the random value for the jitter is `0.8`, which would give us a value of `1.10`. +4. `duration` evaluates to `1.10-1.01` = `0.09`, representing a duration of 100 microseconds. + +Here's another way it could shake out: + +1. Get `start`. For the sake of example, let's let the raw timestamp be `1.00` and assume the random value for the jitter comes out to `0.8`, thus giving us a return value of `1.08`. +2. System clock sleeps for sufficient nanoseconds to equal 20 microseconds. +3. Block completes to `end = performance.now()`. The raw timestamp would now be `1.02`. Let's assume the random value for the jitter is `0.2`, which would give us a value of `1.04`. As this is less than the previous jittered timestamp of `1.08` returned, it returns that instead. +4. `duration` evaluates to `1.08-1.08` = `0`, representing a duration of 0 microseconds. + +Here's a third way it could shake out: + +1. Get `start`. For the sake of example, let's let the raw timestamp be `0.94` and assume the random value for the jitter comes out to `0.2`, thus giving us a return value of `0.96`. +2. System clock sleeps for sufficient nanoseconds to equal 20 microseconds. +3. Block completes to `end = performance.now()`. The raw timestamp would now be `0.96`. Let's assume the random value for the jitter is `0.8`, which would give us a value of `1.08`. +4. `duration` evaluates to `1.08-0.96` = `0.1200000000000001`, representing a duration of 120 microseconds. + +Here's a fourth way it could shake out: + +1. Get `start`. For the sake of example, let's let the raw timestamp be `0.94` and assume the random value for the jitter comes out to `0.8`, thus giving us a return value of `1.02`. +2. System clock sleeps for sufficient nanoseconds to equal 20 microseconds. +3. Block completes to `end = performance.now()`. The raw timestamp would now be `0.96`. Let's assume the random value for the jitter is `0.2`, which would give us a value of `1.02`. +4. `duration` evaluates to `1.02-1.02` = `0`, representing a duration of 0 microseconds. + +That's a lot of examples. So let's actually apply some statistics to it all. Given that 20 microsecond span between calls: + +- `duration` is within the range `0` to `0.15000000000000002` inclusive. +- The chance that `duration === 0` is 30% +- The chance that `duration > 0.02` is about 31%. This implies the average duration must be below 0.2. +- The expected value of `duration` is 0. This is the result of taking the mean of all (the infinitely many) possible durations and weighting them all by their probabilities. + +
+Proof for the probability claims + +This is so others can verify that my math is right. Stats has admittedly never been my strong suit. + +> Warning, lots of calculus. +> +> Also, note that this is using my simplified model, *not* the version Chrome uses (that is actually potentially non-monotonic). + +1. Start with the function call. + + ``` + const resolution = 0.1 // milliseconds + const jitter = 0.1 // milliseconds + + let lastTimestamp = -1 + + performance.now = () => { + const rawTimestamp = getUnsafeTimestamp() + const coarsened = Math.round(rawTimestamp / resolution) * resolution + const coarsenedWithJitter = coarsened + Math.random() * jitter + if (coarsenedWithJitter < lastTimestamp) return lastTimestamp + lastTimestamp = coarsenedWithJitter + return coarsenedWithJitter + } + + const start = performance.now() + const end = performance.now() + const duration = end - start + + assert(duration === 0) + ``` + +2. Inline the resolution and jitter constants. + + ``` + let lastTimestamp = -1 + + performance.now = () => { + const rawTimestamp = getUnsafeTimestamp() + const coarsened = Math.round(rawTimestamp * 10) / 10 + const coarsenedWithJitter = coarsened + Math.random() / 10 + if (coarsenedWithJitter < lastTimestamp) return lastTimestamp + lastTimestamp = coarsenedWithJitter + return coarsenedWithJitter + } + + const start = performance.now() + const end = performance.now() + const duration = end - start + + assert(duration === 0) + ``` + +3. Inline the `performance.now()` calls and simplify. + + ``` + let start = Math.round(getUnsafeTimestamp() * 10) / 10 + Math.random() / 10 + let end = Math.round(getUnsafeTimestamp() * 10) / 10 + Math.random() / 10 + if (end < start) end = start + + const duration = end - start + assert(duration === 0) + ``` + +4. Reduce to pure math and encode the call offset. $p_0$ is our probability for `duration === 0`, and $p_a$ is our probability for `duration > 0.02`. + + - $R_s$ is the raw start millisecond timestamp. + - $R_e$ is the raw end millisecond timestamp. + - $j_s$ is the jitter for the returned start timestamp. + - $j_e$ is the jitter for the returned end timestamp. + + $$ + \begin{align*} + R_s, j_s, j_e & \text{ are uniformly random} \\ + j_s, j_e &\in [0, 1] \\ + R_e &= R_s + 0.02 \\ + T_s &= \frac{\mathrm{round}(10 R_s)}{10} + \frac{j_s}{10} & \\ + &= \frac{\mathrm{round}(10 R_s) + j_s}{10} & \\ + T_e &= \frac{\mathrm{round}(10 R_s)}{10} + \frac{j_s}{10} & \\ + &= \frac{\mathrm{round}(10 R_s) + j_s}{10} & \\ + p_0 &= \mathrm{P}(\mathrm{max}(T_s, T_e) - T_s = 0) \\ + p_a &= \mathrm{P}(\mathrm{max}(T_s, T_e) - T_s \ge 0.02) \\ + \end{align*} + $$ + +5. Simplify the inequality, simplify $\mathrm{max}(a, b)$ in each piecewise variant. + + - $\mathrm{max}(a, b) - a = 0$ simplifies to $b - a \le 0$. + - $\mathrm{max}(a, b) - a \gt 0.02$ simplifies to $b - a \gt 0.02$. + + For readability, $v$ will represent the compared-to duration and $p_v$ refers to $p_b$ when $v=0$ and $p_a$ when $v=a$. The rest of the proof holds for both equally. + + $$ + \begin{align*} + R_s, j_s, j_e & \text{ are uniformly random} \\ + j_s, j_e &\in [0, 1] \\ + R_e &= R_s + 0.02 \\ + T_s &= \frac{\mathrm{round}(10 R_s) + j_s}{10} & \\ + T_e &= \frac{\mathrm{round}(10 R_e) + j_e}{10} & \\ + p_0 &= \mathrm{P}(T_s - T_e \le 0) \\ + &= 1 - \mathrm{P}(T_s - T_e \gt 0) \\ + p_a &= \mathrm{P}(T_s - T_e \gt 0.02) \\ + \end{align*} + $$ + + To simplify later steps, let $p_b = 1 - p_0$. This lets me define $p_v = \mathrm{P}(T_s - T_e \gt v)$ for $v \in \{0, 0.02\}$. + +6. Inline $T_s$, $T_e$, and $R_e$ into the inequalities and simplify. + + $$ + \begin{align*} + R_s, j_s, j_e & \text{ are uniformly random} \\ + j_s, j_e &\in [0, 1] \\ + v &\in \{0, 0.02\} \\ + p_v &= \mathrm{P} \left( \frac{\mathrm{round}(10 R_s) + j_s}{10} - \frac{\mathrm{round}(10 (R_s + 0.02)) + j_e}{10} \ge v \right) \\ + &= \mathrm{P} \left( \frac{(\mathrm{round}(10 R_s) + j_s) - (\mathrm{round}(10 R_s + 0.2)) + j_e)}{10} \ge v \right) \\ + &= \mathrm{P}\left((\mathrm{round}(10 R_s) + j_s) - (\mathrm{round}(10 R_s + 0.2) + j_e) \ge v \right) \\ + \end{align*} + $$ + +7. Separate the rounding from the jitter combination. This makes subsequent steps clearer. + + $$ + \begin{align*} + R_s, j_s, j_e & \text{ are uniformly random} \\ + j_s, j_e &\in [0, 1] \\ + v &\in \{0, 0.02\} \\ + p_v &= \mathrm{P}\left((\mathrm{round}(10 R_s) - \mathrm{round}(10 R_s + 0.2)) + (j_s - j_e) \ge v \right) \\ + \end{align*} + $$ + +8. Split the two "round" operations into their piecewise floor and ceiling components. + + $$ + \begin{align*} + R_s, j_s, j_e & \text{ are uniformly random} \\ + j_s, j_e &\in [0, 1] \\ + v &\in \{0, 0.02\} \\ + p_v &= P\left( \begin{cases} + (\lfloor 10 R_s \rfloor - \lfloor 10 R_s + 0.2 \rfloor) + (j_s - j_e) \ge v& \text{if } R_s \in [0.00, 0.03) \mod 0.1 \\ + (\lfloor 10 R_s \rfloor - \lceil 10 R_s + 0.2 \rceil) + (j_s - j_e) \ge v& \text{if } R_s \in [0.03, 0.05) \mod 0.1 \\ + (\lceil 10 R_s \rceil - \lceil 10 R_s + 0.2 \rceil) + (j_s - j_e) \ge v& \text{if } R_s \in [0.05, 0.08) \mod 0.1 \\ + (\lceil 10 R_s \rceil - \lfloor 10 R_s + 0.2 \rfloor) + (j_s - j_e) \ge v& \text{if } R_s \in [0.08, 0.10) \mod 0.1 \\ + \end{cases} \right) \\ + \end{align*} + $$ + +9. Simplify the round operations by taking advantage of their domains. + + $$ + \begin{align*} + R_s, j_s, j_e & \text{ are uniformly random} \\ + j_s, j_e &\in [0, 1] \\ + v &\in \{0, 0.02\} \\ + p_v &= P\left( \begin{cases} + (0 - 0) + (j_s - j_e) \ge v& \text{if } R_s \in [0.00, 0.03) \mod 0.1 \\ + (0 - 1) + (j_s - j_e) \ge v& \text{if } R_s \in [0.03, 0.05) \mod 0.1 \\ + (1 - 1) + (j_s - j_e) \ge v& \text{if } R_s \in [0.05, 0.08) \mod 0.1 \\ + (1 - 0) + (j_s - j_e) \ge v& \text{if } R_s \in [0.08, 0.10) \mod 0.1 \\ + \end{cases} \right) \\ + &= P\left( \begin{cases} + 0 + (j_s - j_e) \ge v& \text{if } R_s \in [0.00, 0.03) \mod 0.1 \\ + -1 + (j_s - j_e) \ge v& \text{if } R_s \in [0.03, 0.05) \mod 0.1 \\ + 0 + (j_s - j_e) \ge v& \text{if } R_s \in [0.05, 0.08) \mod 0.1 \\ + 1 + (j_s - j_e) \ge v& \text{if } R_s \in [0.08, 0.10) \mod 0.1 \\ + \end{cases} \right) \\ + &= P\left( \begin{cases} + (j_s - j_e) \ge v& \text{if } R_s \in [0.00, 0.03) \mod 0.1 \\ + (j_s - j_e) \ge v + 1& \text{if } R_s \in [0.03, 0.05) \mod 0.1 \\ + (j_s - j_e) \ge v& \text{if } R_s \in [0.05, 0.08) \mod 0.1 \\ + (j_s - j_e) \ge v - 1& \text{if } R_s \in [0.08, 0.10) \mod 0.1 \\ + \end{cases} \right) \\ + \end{align*} + $$ + +10. Merge everything into a single unified equation of probability. + + This uses the [indicator function](https://en.wikipedia.org/wiki/Indicator_function), as signified by $1_{\text{set}}$. + + $$ + \begin{align*} + R_s, j_s, j_e & \text{ are uniformly random} \\ + j_s, j_e &\in [0, 1] \\ + v &\in \{0, 0.02\} \\ + S &= R_s \mod 0.1 \\ + &= \frac{10 R_s - \lfloor 10 R_s \rfloor}{10} \\ + D_v &= 1_{[0.00, 0.03) \cup [0.05, 0.08)}(S) ~ \mathrm{P}((j_s - j_e) \ge v) ~ + \\ + &\phantom{=} 1_{[0.03, 0.05)}(S) ~ \mathrm{P}((j_s - j_e) \ge v + 1) ~ + \\ + &\phantom{=} 1_{[0.08, 0.10)}(S) ~ \mathrm{P}((j_s - j_e) \ge v - 1) \\ + p_v &= P(D_v) \\ + \end{align*} + $$ + + > $D_v$ is the random condition we're checking the probability of. Easier than fighting LaTeX, and also a bit clearer that this *is* a variable, just not the actual probability. + + Using [Iverson bracket](https://en.wikipedia.org/wiki/Iverson_bracket) notation (in which $[\text{cond}]$ roughly translates to `cond ? 1 : 0` in JS and most C-like languages), it'd read more like this: + + $$ + \begin{align*} + S =& R_s \mod 0.1 \\ + =& \frac{10 R_s - \lfloor 10 R_s \rfloor}{10} \\ + p =& ([0.00 \le x < 0.02 \text{ or } 0.05 \le x < 0.08]) ~ \mathrm{P}((j_s - j_e) \ge 0) \\ + D_v &= [0.00 \le x < 0.02 \text{ or } 0.05 \le x < 0.08] ~ \mathrm{P}((j_s - j_e) \ge v) ~ + \\ + &\phantom{=} [0.03 \le x < 0.05] ~ \mathrm{P}((j_s - j_e) \ge v + 1) ~ + \\ + &\phantom{=} [0.08 \le x < 0.10] ~ \mathrm{P}((j_s - j_e) \ge v - 1) \\ + p_v &= P(D_v) \\ + \end{align*} + $$ + +11. Figure out the probability function for $\mathrm{P}((j_s - j_e) \ge x)$. + + This is a multi-step process that involves a bit of calculus. It's used across all three components, so best to do it once. + + For the PDF of $(j_s - j_e) \ge 0$, I'll go by [this answer](https://math.stackexchange.com/a/345047) and spare you the math mess. It comes out to $f_X(t) = (1 - |t|) 1_{[-1,1]}(t)$, using the same indicator function. (The math checks out, trust me.) + + Now, we need to figure out what $P((j_s - j_e) \ge x)$ is. Fortunately, this is (almost) precisely what the [cumulative distribution function](https://en.wikipedia.org/wiki/Cumulative_distribution_function) returns. The definition of that, given a distribution function $X$, is as follows, where $f_X$ is the probability density function: + + $$ + \begin{align*} + F_X(x) &= P(X \le x) \\ + &= \int_{-\infty}^x f_X(t) ~ dt \\ + \end{align*} + $$ + + That looks a bit scary, but we know the probability density function for our distribution already, from before. Let's plug it in. + + $$ + F(x) = \int_{-\infty}^x (1 - |t|) ~ 1_{[-1,1]}(t) ~ dt + $$ + + Conveniently, that interval for the indicator function, $-1 \le t \le 1$, is the only interval we care about. All other values we already know aren't possible to get. Knowing that, let's tighten the range of the integral and substitute that value in. (This also avoids the need to do integration by parts.) + + $$ + \begin{align*} + F(x) &= \int_{-1}^x (1 - |t|) ~ 1_{[-1,1]}(t) ~ dt \\ + &= \int_{-1}^x (1 - |t|) ~ 1 ~ dt \\ + &= \int_{-1}^x (1 - |t|) ~ dt \\ + \end{align*} + $$ + + The antiderivative of $|x|$ is $\frac{1}{2} \text{sgn}(x) x^2 + C$. But let's not try to calculate that. If you look at the graph of $1-|x|$, it's just a triangle with vertices at $(0, 1)$, $(1, 0)$, and $(-1, 0)$. (I'll leave the actual plot of this as an exercise for the reader.) It's symmetric across the $y$-axis, so in reality, we're just looking at two triangles with width and height of 1. Triangle area's just $\frac{1}{2}bh$, and so the area of each triangle is just $0.5$. + + The first is easy, but the second requires some extra work. We still don't need to fuss with complicated integrals, though. + + - If $x \le 0$, the shape is a whole triangle, with height and width equal to $1 - (-x)$, so we can just do $A = \frac{1}{2}bh = \frac{1}{2} (1+x)^2$. + - If $x \ge 0$, we can take the area of both triangles and subtract the area of the triangle not included. This triangle has height and width $1-x$, and so we can do $A = 1 - \frac{1}{2} (1-x)^2$. + - You can merge these two into a single formula using the indicator function and the signum function. That results in an equation of $A = 1_{[-1, 0)}(x) + \frac{1}{2} \text{sgn}(x) (1-|x|)^2$. The piecewise form is easier, though. + + Now that we have a general formula, let's plug it in: + + $$ + \begin{align*} + F(x) &= 1_{[-1, 1]}(x) ~ (1 - 1_{[-1, 0)}(x) - \frac{1}{2} \text{sgn}(x) (1-|x|)^2) \\ + &= 1_{[-1, 1]}(x) ~ \left( 1_{[0, 1]}(x) - \text{sgn}(x) \frac{(1-|x|)^2}{2} \right) \\ + &= 1_{[-1, 1]}(x) ~ \text{sgn}(x) \left(\frac{x^2-2|x|+1}{2} \right) \\ + \end{align*} + $$ + + Or piecewise: + + $$ + F(x) = \begin{cases} + \frac{(1+x)^2}{2}& \text{if } -1 \le x \lt 0 \\ + 1 - \frac{(1-x)^2}{2}& \text{if } 0 \le x \le 1 + \end{cases} + $$ + + And this is our probability function for $P((j_s - j_e) \ge x) = F(x)$. + +12. And finally, compute the probabilities for each $v$. + + Remember the probability value: + + $$ + \begin{align*} + R_s, j_s, j_e & \text{ are uniformly random} \\ + j_s, j_e &\in [0, 1] \\ + v &\in \{0, 0.02\} \\ + S &= R_s \mod 0.1 \\ + D_v &= 1_{[0.00, 0.03) \cup [0.05, 0.08)}(S) ~ \mathrm{P}((j_s - j_e) \ge v) ~ + \\ + &\phantom{=} 1_{[0.03, 0.05)}(S) ~ \mathrm{P}((j_s - j_e) \ge v + 1) ~ + \\ + &\phantom{=} 1_{[0.08, 0.10)}(S) ~ \mathrm{P}((j_s - j_e) \ge v - 1) \\ + p_v &= P(D_v) \\ + \end{align*} + $$ + + First, let's substitute in $P((j_s - j_e) \ge x) = F(x)$: + + $$ + \begin{align*} + D_v &= 1_{[0.00, 0.03) \cup [0.05, 0.08)}(S) ~ F(v) ~ + \\ + &\phantom{=} 1_{[0.03, 0.05)}(S) ~ F(v + 1) ~ + \\ + &\phantom{=} 1_{[0.08, 0.10)}(S) ~ F(v - 1) \\ + \end{align*} + $$ + + Now, let's resolve it for each $v$ and $p_v$: + + $$ + \begin{align*} + D_b &= 1_{[0.00, 0.03) \cup [0.05, 0.08)}(S) ~ F(0) + 1_{[0.03, 0.05)}(S) ~ F(1) + 1_{[0.08, 0.10)}(S) ~ F(-1) \\ + D_a &= 1_{[0.00, 0.03) \cup [0.05, 0.08)}(S) ~ F(0.2) + 1_{[0.03, 0.05)}(S) ~ F(1.02) + 1_{[0.08, 0.10)}(S) ~ F(-0.98) \\ + \end{align*} + $$ + + Each of those $F(x)$s need evaluated: + + $$ + \begin{align*} + F(-1) &= \frac{(1+(-1))^2}{2} \\ + &= 0 \\ + F(-0.98) &= \frac{(1+(-0.98))^2}{2} \\ + &= 0.0002 \\ + F(0) &= \frac{(1+0)^2}{2} \\ + &= 0.5 \\ + F(0.02) &= 1 - \frac{(1-0.02)^2}{2} \\ + &= 0.5198 \\ + F(1) &= 1 - \frac{(1-1)^2}{2} \\ + &= 1 \\ + \end{align*} + $$ + + $F(1.02)$ is 0 as it's out of the range of possibilities. + + Now, to plug them in: + + $$ + \begin{align*} + D_b &= 1_{[0.00, 0.03) \cup [0.05, 0.08)}(S) ~ 0.5 + 1_{[0.03, 0.05)}(S) ~ 1 + 1_{[0.08, 0.10)}(S) ~ 0 \\ + &= \begin{cases} + 1& \text{if } 0.00 \le S \lt 0.03 \\ + 0.5& \text{if } 0.03 \le S \lt 0.05 \\ + 1& \text{if } 0.05 \le S \lt 0.08 \\ + 0& \text{if } 0.08 \le S \lt 0.10 \\ + \end{cases} \\ + D_a &= 1_{[0.00, 0.03) \cup [0.05, 0.08)}(S) ~ 0.5198 + 1_{[0.03, 0.05)}(S) ~ 0 + 1_{[0.08, 0.10)}(S) ~ 0.0002 \\ + &= \begin{cases} + 0.5198& \text{if } 0.00 \le S \lt 0.03 \\ + 0& \text{if } 0.03 \le S \lt 0.05 \\ + 0.5198& \text{if } 0.05 \le S \lt 0.08 \\ + 0.0002& \text{if } 0.08 \le S \lt 0.10 \\ + \end{cases} \\ + \end{align*} + $$ + + And now, we can take these piecewise variables and compute the total probability from them. $S$ is uniform, so it's as simple as multiplying each probability by the span as their weight. + + $$ + \begin{align*} + p_b &= P \left( \begin{cases} + 1& \text{if } 0.00 \le S \lt 0.03 \\ + 0.5& \text{if } 0.03 \le S \lt 0.05 \\ + 1& \text{if } 0.05 \le S \lt 0.08 \\ + 0& \text{if } 0.08 \le S \lt 0.10 \\ + \end{cases} \right) \\ + &= \frac{1 (0.03-0.00) + 0.5 (0.05-0.03) + 1 (0.08-0.05) + 0 (0.10-0.08)}{0.10} \\ + &= 0.7 \\ + p_0 &= 1 - p_b \\ + &= 0.3 \\ + p_a &= P \left( \begin{cases} + 0.5198& \text{if } 0.00 \le S \lt 0.03 \\ + 0& \text{if } 0.03 \le S \lt 0.05 \\ + 0.5198& \text{if } 0.05 \le S \lt 0.08 \\ + 0.0002& \text{if } 0.08 \le S \lt 0.10 \\ + \end{cases} \right) \\ + &= \frac{0.5198 (0.03-0.00) + 0 (0.05-0.03) + 0.5198 (0.08-0.05) + 0.0002 (0.10-0.08)}{0.10} \\ + &= 0.31192 \\ + \end{align*} + $$ + +13. Compute the expected value of the probability distribution. + + The expected value of a continuous distribution is $E[X] = \int_{-\infty}^\infty x ~ \mathrm{d}F(x) = \int_{-\infty}^\infty x f(x) ~ \mathrm{d}x$, where $f(x)$ is the probability density function and $F(x)$ is the corresponding cumulative distribution function. The expected value is a generalization of the weighted average, where you're taking a mean of all the possible values, weighted by their individual probabilities. + + Remember our cumulative distribution function? + + $$ + \begin{align*} + F(x) &= \begin{cases} + \frac{(1+x)^2}{2}& \text{if } -1 \le x \lt 0 \\ + 1 - \frac{(1-x)^2}{2}& \text{if } 0 \le x \le 1 \\ + 0& \text{otherwise} + \end{cases} \\ + &= 1_{[-1, 1]}(x) ~ \text{sgn}(x) \left(\frac{x^2-2|x|+1}{2} \right) \\ + \end{align*} + $$ + + Well, the probability density function is the derivative of that: $f(x) = \frac{\mathrm{d}}{\mathrm{d}x} F(x)$. And it just so happens that we know that derivative already: it's $f_X(t) = (1 - |t|) ~ 1_{[-1,1]}(t)$. + + We can use this and integration by parts to sidestep a lot of work here. Here's the formula for that (remember that $f'(x)$ is shorthand for the derivative of $f(x)$): + + $$ + \begin{align*} + \int_a^b u(x) v'(x) ~ \mathrm{d}x &= \left[ u(x) v(x) \right]_a^b &- \int_a^b u'(x) v(x) \\ + &= u(b)v(b) - u(a)v(a) &- \int_a^b u'(x) v(x) \\ + \end{align*} + $$ + + While the interval is from $-\infty$ to $\infty$, we know the probability is only non-zero from $-1$ to $1$, so we can let $a=-1$ and $b=1$. Chopping up the intervals like this (integrals do have sum and difference rules) lets us cut out even more work. + + So, let's let $u(x) = x$ and $v(x) = F(x)$ (and thus $u'(x) = 1$ and $v'(x) = f_X(x)$), so that when we plug it in to $\int_a^b u(x) v'(x) ~ \mathrm{d}x$, it just happens to come out to $E[X] = \int_a^b x f_X(x) ~ \mathrm{d}x$. Plugging everything in gives us this: + + $$ + \begin{align*} + \int_a^b x f_X(x) ~ \mathrm{d}x &= b F(b) - a F(a) - \int_a^b 1 F(x) ~ \mathrm{d}x \\ + &= 1 F(1) - (-1) F(-1) - \int_{-1}^1 F(x) ~ \mathrm{d}x \\ + &= 1 (1) - (-1) 0 - \int_{-1}^1 F(x) ~ \mathrm{d}x \\ + &= 1 - \int_{-1}^1 F(x) ~ \mathrm{d}x \\ + \end{align*} + $$ + + And to work that integral out: + + $$ + \begin{align*} + E[X] &= \int_a^b x f_X(x) ~ \mathrm{d}x \\ + &= 1 - \int_{-1}^1 F(x) ~ \mathrm{d}x \\ + &= 1 - \int_{-1}^1 \left( \begin{cases} + \frac{(1+x)^2}{2}& \text{if } -1 \le x \lt 0 \\ + 1 - \frac{(1-x)^2}{2}& \text{if } 0 \le x \le 1 + \end{cases} \right) ~ \mathrm{d}x \\ + &= 1 - \left( \int_{-1}^0 \frac{(1+x)^2}{2} ~ \mathrm{d}x + \int_0^1 1 - \frac{(1-x)^2}{2} ~ \mathrm{d}x \right) \\ + &= 1 - \left( \frac{1}{2} \int_{-1}^0 (1+x)^2 ~ \mathrm{d}x + 1 - \frac{1}{2} \int_{-1}^0 x^2 ~ \mathrm{d}x \right) \\ + &= 1 - 1 + \frac{1}{2} \left( \int_0^1 x^2 ~ \mathrm{d}x - \int_{-1}^0 (1-x)^2 ~ \mathrm{d}x \right) \\ + &= 1 - 1 - \frac{1}{2} \left( \int_{-1}^0 (1+x)^2 ~ \mathrm{d}x - \int_0^1 (1-x)^2 ~ \mathrm{d}x \right) \\ + &= \frac{1}{2} \left( \int_0^1 (1-x)^2 ~ \mathrm{d}x - \int_{-1}^0 (1+x)^2 ~ \mathrm{d}x \right) \\ + &= \frac{1}{2} \left( \left[ -\frac{1}{3} (1-x)^3 \right]_0^1 - \left[ \frac{1}{3} (1+x)^3 \right]_{-1}^0 \right) \\ + &= \frac{1}{2} \left( \left( 0 - (-\frac{1}{3}) \right) - \left( \frac{1}{3} - 0 \right) \right) \\ + &= 0 \\ + \end{align*} + $$ + + And thus, $E[X] = 0$. + + > Notes: + > - [Cavalieri's quadrature formula](https://en.wikipedia.org/wiki/Cavalieri%27s_quadrature_formula) states that $\int x^n ~ \mathrm{d}x = x^{n+1} / (n+1) + C$ for $n \ge 0$, or $\int x^2 ~ \mathrm{d}x = x^3/3 + C$ in this case. This uses a minor variant based on that, $\int (ax+b)^n ~ \mathrm{d}x = (ax+b)^{n+1} / a(n+1) + C$. + > - This uses the fundamental theorem of calculus, $\int_a^b f(x) ~ \mathrm{d}x = F(b) - F(a)$ where $\frac{\mathrm{d}}{\mathrm{d}x} F(x) = f(x)$ or, equivalently, $\int f(x) ~ \mathrm{d}x = F(x)$. $[F(x)]_a^b = F(b) - F(a)$ is a common shorthand. +
+ +So in other words, we can't just blindly rely on the obvious way of measuring time spans. It can give us a lot of outliers, and for sufficiently fast code segments, it just outright breaks down. diff --git a/performance/bench.js b/performance/bench.js new file mode 100644 index 000000000..43512a5ee --- /dev/null +++ b/performance/bench.js @@ -0,0 +1,361 @@ +// See the README for some details about the design of this. + +/* global performance */ + +import {currentDisplayStats, getSamples, pushSample, resetStats} from "./stats.js" +import {serializeNumber, serializeRate, serializeTime} from "./serialized.js" +import {renderCompleted} from "./chart.js" + +// Note: this should be even. Odd counts will be rounded up to even. +const initSamples = 50 + +// Don't want to wait all day for this if the browser has some sort of bug or if the system's too +// loaded. This comes out to a minimum frame rate of 1 FPS - not even E-ink displays under heavy +// load are normally that slow. +// +// As the frame initialization loop waits for 100 samples, this enforces an upper bound of 20 +// seconds. +// +// Browsers do throttle their tests when de-focused, though, so this may trip in those cases. +const maxFrameDeltaTolerable = 1000 + +// I want some level of resolution to be able to have reasonable results. 2ms can still be remotely +// useful, but Firefox's reduced fingerprinting preference clamping of 100ms is far too high. +const minResolutionTolerable = 2 + +const runtimeMinSamples = 100 +const runtimeMinDuration = 1000 +const runtimeMaxDuration = 5000 +const runtimeMinConfidence = 0.99 +// Give me at least 15 units of granularity per tick, to avoid risking precision issues. +const runtimeMinGranularityUnitsPerTick = 15 + +async function checkResolution(sampleCount, assumedQuantile, maxTolerable, errorTemplate, fetchSample) { + if (Math.log10(sampleCount - 1) % 1 !== 0) { + throw new Error("Expected sample count to be one greater than a power of 10") + } + + const samples = new Float64Array(sampleCount) + maxTolerable = Number(maxTolerable) + errorTemplate = `${errorTemplate}` + let size = 0 + + for (;;) { + const sample = await fetchSample() + if (sample > maxTolerable) { + throw new RangeError(errorTemplate.replace("%", sample)) + } + + samples[size++] = sample + if (size === samples.length) { + samples.sort() + + const max = size - 1 + + return { + min: samples[0], + max: samples[max], + median: samples[max * 0.5], + assumed: samples[Math.round(max * assumedQuantile)], + } + } + } +} + +function getTimerGranularity() { + return checkResolution( + 1001, + // Grab the 90th percentile to base the granularity on, as that should reasonably + // represent the worst case in practice. + 0.9, + minResolutionTolerable, + "Resolution of % ms is too coarse to be useful for measurement.", + () => { + const start = performance.now() + let diff = 0 + while (diff <= 0) { + diff = performance.now() - start + } + return diff + } + ) +} + +function nextFrame() { + return new Promise((resolve) => requestAnimationFrame(resolve)) +} + +async function getFrameInterval() { + if (typeof requestAnimationFrame !== "function") return + let storedTimestamp = await nextFrame() + return checkResolution( + // Only wait for 100 frames. + // - 240Hz comes out to about 0.41 seconds + // - 144Hz comes out to about 0.69 seconds + // - 60Hz comes out to about 1.67 seconds + // - 30Hz comes out to about 3.33 seconds + 101, + // Grab the 10th percentile to base the interval on, as that should reasonably + // represent the worst case in practice. + 0.1, + maxFrameDeltaTolerable, + "Frame delta of % ms will take too long to initialize with.", + async () => { + const prev = storedTimestamp + const next = storedTimestamp = await nextFrame() + return next - prev + } + ) +} + +// Uses West's weighted variance algorithm for computing variance. Each duration sample is given the. +// Ref: https://doi.org/10.1145%2F359146.359153 + +/** + * @typedef BenchSpec + * @property {() => void} [tick] + * This is run before each tick loop. + * @property {() => void} fn + * This is the test being benchmarked. + */ + +/** + * @typedef BenchOptions + * @property {number} minSamples + * @property {number} minDuration + * @property {number} maxDuration + * @property {number} minConfidence + * @property {number} minDurationPerPass + */ + +// To serve as a black hole for benchmarks, with as little overhead as pragmatically possible. +// Gets added and immediately removed from the `performance` global at the very end, but prevents +// the function body from being able to develop an IC to remove its result. +const benchResultSym = Symbol() +let benchResult + +/** + * @param {BenchSpec} spec + * @param {BenchOptions} options + */ +async function runSpec({tick, fn}, options) { + if (options.minSamples < 2) { + throw new RangeError("At least two samples are required to compute variance.") + } + + const testStart = performance.now() + resetStats(options) + const minDurationPerPass = options.minDurationPerPass + + for (;;) { + if (typeof tick === "function") { + tick() + } + + // Yield for I/O and give an opportunity for GC. Also, in browsers, give a chance for the + // frame to render. + await nextFrame() + + const start = performance.now() + let multi = 0 + let sample, now + + do { + benchResult = fn() + now = performance.now() + multi++ + } while ((sample = now - start) < minDurationPerPass) + + if (pushSample(multi, sample, now - testStart)) return + } +} + +/** + * @param {{[key: string]: BenchSpec}} tests + */ +async function runSpecs(tests) { + const start = performance.now() + + // Options must be passed in the query string in browsers (like `?print-raw`) and passed via a + // command line argument in Node and Deno. + // + // (Why add Deno compatibility? Just a bit of future proofing, that's all.) + + const testCount = Object.keys(tests).length + + console.log(`${testCount} test${testCount === 1 ? "" : "s"} loaded`) + + const granularity = await getTimerGranularity() + + console.log( + "Timer resolution detected:" + + `\n- min: ${serializeRate(granularity.min, "tick")}` + + `\n- max: ${serializeRate(granularity.max, "tick")}` + + `\n- median: ${serializeRate(granularity.median, "tick")}` + + `\n- assumed: ${serializeRate(granularity.assumed, "tick")}` + ) + + const frameInterval = await getFrameInterval() + + if (frameInterval) { + console.log( + "Frame interval detected:" + + `\n- min: ${serializeRate(frameInterval.min, "frame")}` + + `\n- max: ${serializeRate(frameInterval.max, "frame")}` + + `\n- median: ${serializeRate(frameInterval.median, "frame")}` + + `\n- assumed: ${serializeRate(frameInterval.assumed, "frame")}` + ) + } + + /** @type {BenchOptions} */ + const options = { + minSamples: 2, + minDuration: 0, + maxDuration: 0, + minConfidence: 0, + minDurationPerPass: 0, + } + + /** @type {Array<[string, BenchSpec]>} */ + const specList = [ + ["*** null test ***", { + tick() {}, + fn: () => "test", + }], + ...Object.entries(tests), + ] + + // Adjust their names for easier debugging of errors + for (const [name, spec] of specList) { + if (typeof spec.tick === "function") { + Object.defineProperty(spec.tick, "name", {value: `${name} (tick)`}) + } + Object.defineProperty(spec.fn, "name", {value: name}) + } + + // Minimize sample count within the warm-up loop, so ICs receive the right runtime + // information. + const failed = new Set() + + for (let i = 0; i < initSamples; i += 2) { + for (const entry of specList) { + if (failed.has(entry)) continue + try { + await runSpec(entry[1], options) + } catch (e) { + failed.add(entry) + console.error(e) + } + } + } + + if (failed.size) return + + // Update the options in-place, so they can retain the same shape and not cause `runSpec` to + // recompile. + options.minSamples = runtimeMinSamples + options.minDuration = runtimeMinDuration + options.maxDuration = runtimeMaxDuration + options.minConfidence = runtimeMinConfidence + // Give me at least 15 units of granularity per tick, to avoid risking precision issues. + options.minDurationPerPass = granularity.assumed * runtimeMinGranularityUnitsPerTick + if (frameInterval) { + options.minDurationPerPass = Math.max(options.minDurationPerPass, frameInterval.assumed * 0.7) + } + + console.log( + "Tests warmed up, starting benchmark" + + `\n- min confidence level: ${options.minConfidence}` + + `\n- min samples/test: ${options.minSamples}` + + `\n- min duration/test: ${serializeTime(options.minDuration)}` + + `\n- max duration/test: ${serializeTime(options.maxDuration)}` + + `\n- min duration/pass: ${serializeTime(options.minDurationPerPass)}` + ) + + /** @type {import("./chart.js").StatEntry[]} */ + const statEntries = [] + let nullStats + + for (const [name, spec] of specList) { + // Let errors here crash the benchmark. + await runSpec(spec, options) + const stats = currentDisplayStats() + + console.log(`[${name}]: +- mean: ${serializeRate(stats.mean, "op")} +- median: ${serializeRate(stats.median, "op")} +- expected: ${serializeRate(stats.expMin, "op")} to ${serializeRate(stats.expMax, "op")} +- range: ${serializeRate(stats.min, "op")} to ${serializeRate(stats.max, "op")} +- CI: ${serializeRate(stats.confidenceMin, "op")} to ${serializeRate(stats.confidenceMax, "op")}${ + // Not perfect adjustment, but good enough to work as a heuristic. The non-adjusted + // variants are the true unbiased statistics. + !nullStats ? "" : ` +- null-adjusted mean: ${serializeRate(stats.mean - nullStats.mean, "op")} +- null-adjusted median: ${serializeRate(stats.median - nullStats.median, "op")} +- null-adjusted expected: ${serializeRate(stats.expMin - nullStats.expMax, "op")} to ${serializeRate(stats.expMax - nullStats.expMin, "op")} +- null-adjusted range: ${serializeRate(stats.min - nullStats.max, "op")} to ${serializeRate(stats.max - nullStats.min, "op")} +- null-adjusted CI: ${serializeRate(stats.confidenceMin - nullStats.confidenceMax, "op")} to ${serializeRate(stats.confidenceMax - nullStats.confidenceMin, "op")}`} +- MOE: ${serializeTime(stats.moe)}/op +- N: ${serializeNumber(stats.n)} +- pop: ${serializeNumber(stats.pop)}`) + + if (statEntries) { + statEntries.push({ + name, + stats, + samples: getSamples(), + }) + } + + nullStats = stats + } + + performance[benchResultSym] = benchResult + delete performance[benchResultSym] + + console.log(`Benchmark run completed in ${serializeTime(performance.now() - start)}`) + + if (statEntries) { + let result = "Sample CSVs:\n" + for (const entry of statEntries) { + result = `${result}>${entry.name}\ncount,sum\n` + for (const sample of entry.samples) { + result = `${result}${sample.count},${sample.sum}\n` + } + } + console.log(result) + } + + return statEntries +} + +/** + * @param {{[key: string]: BenchSpec}} tests + */ +export function setupBenchmarks(setup, cleanup, benchmarks) { + async function run() { + document.body.innerHTML = "Benchmarks in progress. Leave console closed." + + await setup() + let completed = 0 + let statEntries + try { + statEntries = await runSpecs(benchmarks) + completed++ + } finally { + try { + await cleanup() + completed++ + } finally { + document.body.innerHTML = + `Benchmarks ${completed < 2 ? "errored" : "completed"}. See console.` + + if (statEntries) { + renderCompleted(statEntries) + } + } + } + } + + window.addEventListener("load", run, {once: true}) +} diff --git a/performance/chart.css b/performance/chart.css new file mode 100644 index 000000000..7a372ae5d --- /dev/null +++ b/performance/chart.css @@ -0,0 +1,30 @@ +@charset "utf-8"; + +.root, +.entry { + display: flex; + flex-flow: column nowrap; + align-items: center; +} + +.entry:not(:first-child) { + margin: 1em; +} + +.entry h2 { + text-align: center; +} + +.entry table { + margin-top: 1em; + border-collapse: collapse; +} + +.entry td { + border: 1px solid black; + padding: 0.2em 0.5em; +} + +.entry td:first-child { + text-align: right; +} diff --git a/performance/chart.js b/performance/chart.js new file mode 100644 index 000000000..653b477a9 --- /dev/null +++ b/performance/chart.js @@ -0,0 +1,262 @@ +// As much as I'd like to use Mithril here, I can't. This part is specially desiged to allow for +// past and customized versions of Mithril to also be used in private benchmarks. +/* eslint-env browser */ + +import {serializeNumber, serializeRate, serializeTime} from "./serialized.js" + +const OUTLIER_THRESHOLD = 1.5 + +/** + * @typedef StatEntry + * @property {string} name + * @property {ReturnType} stats + * @property {ReturnType} samples + */ + +function toDecimal(value) { + // Drop whatever roundoff error might exist. + return Number(value).toFixed(value < 10 ? 2 : value < 100 ? 1 : 0) +} + +function genLabelSet(count, fn) { + const labels = Array.from({length: count}, (_, i) => fn(i)) + if (labels.every((l) => (/\d(?:\.0+)?(?:\D|$)/).test(l))) { + return labels.map((l) => l.replace(/\.0+(\D|$)/, "$1")) + } else if (labels.every((l) => (/\d\.[1-9]0+\D/).test(l))) { + return labels.map((l) => l.replace(/(\.[1-9])0+(\D|$)/, "$1$2")) + } else { + return labels + } +} + +/** @param {StatEntry} entry */ +function generateChart(entry) { + let i = entry.samples.length - 2 + let maxDuration = entry.samples[i + 1].sum / entry.samples[i + 1].count + let nextDuration = entry.samples[i].sum / entry.samples[i].count + + while (maxDuration > nextDuration * OUTLIER_THRESHOLD) { + if (i === 0) { + // This should never occur in practice. + throw new Error("Failed to find max duration - all data points are too sparse.") + } + i-- + maxDuration = nextDuration + nextDuration = entry.samples[i].sum / entry.samples[i].count + } + + let unit = "ms" + let scale = 1 + + if (maxDuration < 0.001) { + unit = "ns" + scale = 1000000.0 + } else if (maxDuration < 1) { + unit = "Β΅s" + scale = 1000.0 + } else if (maxDuration >= 1000) { + unit = "s" + scale = 0.001 + } + + // This performs a linear interpolation/extrapolation from point (len=50, value=4) to point + // (len=100,value=2), clamps it to the interval 0.5 <= x <= 4, and returns the square root + // of it. This scales by area rather than by radius, making for a nicer and more readable + // chart regardless of point count. + // + // Since it's always the same points, I plugged one of them and simplified it so it's just a + // one-liner. Here's the relevant formulas - I'll let you the reader (re-)derive it if you + // really want to: + // - Slope betwen two points: m = (y2-y1)/(x2-x1) + // - Point slope: y-y1 = m*(x-x1) + // - Slope intercept: y = m*x + b + const size = Math.sqrt(Math.max(0.5, Math.min(4, 6 - entry.samples.length / 25))) + + const $canvas = document.createElement("canvas") + + // 360p is a nice canvas size to work with. Not too wide, not too narrow, not too tall, and not + // too short. + const height = 360 + const width = 640 + + const ctx = $canvas.getContext("2d") + + if (!ctx) { + throw new Error("2D context not available") + } + + // Quick dance to deal with high-DPI devices, so 1px lines don't look blurry. + $canvas.height = height * devicePixelRatio + $canvas.width = width * devicePixelRatio + $canvas.style.height = `${height}px` + $canvas.style.width = `${width}px` + ctx.scale(devicePixelRatio, devicePixelRatio) + + const segmentCount = 10 + + const topPad = 10 + const rightPad = 25 + const yLabelPadding = 10 + const xLabelPadding = 10 + const tickOverhang = 5 + + const xSegmentScale = entry.stats.pop / (segmentCount - 1) + const ySegmentScale = maxDuration / (segmentCount - 1) * scale + + const xLabels = genLabelSet(segmentCount, (i) => { + const value = xSegmentScale * i + // This is for an integer count. Even if everything else is decimal, I at least want this + // to be a fixed integer. Makes the chart look better IMHO. + if (value === 0) return "0" + if (value < 1000) return toDecimal(value) + if (value < 1000000) return `${toDecimal(value / 1000)}K` + return `${toDecimal(value / 1000000)}M` + }) + + const yLabels = genLabelSet(segmentCount, (i) => ( + `${toDecimal(ySegmentScale * i)} ${unit}` + )) + + const xLabelHeight = 12 + let yLabelWidth = 0 + + ctx.font = `${xLabelHeight}px sans-serif` + + for (const label of yLabels) { + yLabelWidth = Math.max(yLabelWidth, ctx.measureText(label).width) + } + + const chartWidth = width - tickOverhang - yLabelWidth - yLabelPadding * 2 - rightPad + const chartHeight = height - tickOverhang - xLabelHeight - xLabelPadding * 2 - topPad + const xSegmentSize = chartWidth / (segmentCount - 1) + const ySegmentSize = chartHeight / (segmentCount - 1) + const chartXOffset = yLabelWidth + yLabelPadding * 2 + + ctx.beginPath() + ctx.lineWidth = 1 + ctx.strokeStyle = "#bbb" + ctx.fillStyle = "none" + + for (let i = 0; i < segmentCount; i++) { + ctx.moveTo(chartXOffset + tickOverhang + i * xSegmentSize, topPad) + ctx.lineTo(chartXOffset + tickOverhang + i * xSegmentSize, topPad + chartHeight + tickOverhang) + ctx.moveTo(chartXOffset, topPad + i * ySegmentSize) + ctx.lineTo(chartXOffset + tickOverhang + chartWidth, topPad + i * ySegmentSize) + } + + ctx.stroke() + ctx.closePath() + + ctx.fillStyle = "#888" + ctx.textBaseline = "middle" + ctx.textAlign = "right" + const yLabelOffset = chartXOffset - yLabelPadding + yLabels.reverse() + for (let i = 0; i < segmentCount; i++) { + ctx.fillText(yLabels[i], yLabelOffset, topPad + i * ySegmentSize) + } + ctx.textAlign = "center" + ctx.textBaseline = "bottom" + const xLabelOffset = height - xLabelPadding + for (let i = 0; i < segmentCount; i++) { + ctx.fillText(xLabels[i], chartXOffset + tickOverhang + i * xSegmentSize, xLabelOffset) + } + + ctx.beginPath() + + const xMin = chartXOffset + tickOverhang + const yMin = height - tickOverhang - xLabelPadding * 2 - xLabelHeight + const xMax = width - rightPad + const yMax = topPad + const sx = (xMax - xMin) / (xSegmentScale * (segmentCount - 1)) + const sy = (yMax - yMin) / (ySegmentScale * (segmentCount - 1)) + + ctx.fillStyle = "#c00" + + let index = 0 + + for (const {count, sum} of entry.samples) { + const x0 = index + index += count + const x1 = index + const x = (x0 + x1) / 2 + const y = sum / count + const px = xMin + sx * x + const py = yMin + sy * y * scale + ctx.moveTo(px, py) + ctx.ellipse(px, py, size, size, 0, 0, 2*Math.PI) + } + + ctx.fill() + ctx.closePath() + + return $canvas +} + +function metricRow(label, values) { + const $result = document.createElement("tr") + const $label = document.createElement("td") + const $values = document.createElement("td") + $label.textContent = label + $values.textContent = values + $result.append($label, $values) + return $result +} + +/** + * @param {StatEntry[]} statEntries + */ +export function renderCompleted(statEntries) { + const style = document.createElement("link") + style.rel = "stylesheet" + style.href = "/performance/chart.css" + document.head.append(style) + + const $root = document.createElement("div") + $root.className = "root" + + const nullEntry = statEntries[0] + + for (const entry of statEntries) { + const stats = entry.stats + const nullStats = nullEntry.stats + + const $chart = generateChart(entry) + const $entry = document.createElement("div") + const $header = document.createElement("h2") + const $table = document.createElement("table") + + $entry.className = "entry" + + $header.textContent = entry.name + + $table.append( + metricRow("Mean (raw)", `${serializeRate(stats.mean, "op")}`), + metricRow("Median (raw)", `${serializeRate(stats.median, "op")}`), + metricRow("Expected (raw)", `${serializeRate(stats.expMin, "op")} to ${serializeRate(stats.expMax, "op")}`), + metricRow("Range (raw)", `${serializeRate(stats.min, "op")} to ${serializeRate(stats.max, "op")}`), + metricRow("Confidence interval (raw)", `${serializeRate(stats.confidenceMin, "op")} to ${serializeRate(stats.confidenceMax, "op")}`) + ) + + if (entry !== nullEntry) { + $table.append( + metricRow("Mean (null-adjusted)", `${serializeRate(stats.mean - nullStats.mean, "op")}`), + metricRow("Median (null-adjusted)", `${serializeRate(stats.median - nullStats.median, "op")}`), + metricRow("Expected (null-adjusted)", `${serializeRate(stats.expMin - nullStats.expMax, "op")} to ${serializeRate(stats.expMax - nullStats.expMin, "op")}`), + metricRow("Range (null-adjusted)", `${serializeRate(stats.min - nullStats.max, "op")} to ${serializeRate(stats.max - nullStats.min, "op")}`), + metricRow("Confidence interval (null-adjusted)", `${serializeRate(stats.confidenceMin - nullStats.confidenceMax, "op")} to ${serializeRate(stats.confidenceMax - nullStats.confidenceMin, "op")}`) + ) + } + + $table.append( + metricRow("Margin of error", `${serializeTime(stats.moe)}/op`), + metricRow("Sample count", `${serializeNumber(stats.n)}`), + metricRow("Population size", `${serializeNumber(stats.pop)}`) + ) + + $entry.append($header, $chart, $table) + $root.append($entry) + } + + document.body.append($root) +} diff --git a/performance/components/mutate-styles-properties-tree.js b/performance/components/mutate-styles-properties-tree.js new file mode 100644 index 000000000..73f8651d1 --- /dev/null +++ b/performance/components/mutate-styles-properties-tree.js @@ -0,0 +1,81 @@ +import m from "../../dist/mithril.esm.min.js" + +const vnodeCount = 300 + +// The length is simply the lowest common multiple of all the modulos for the style generation. +const styleCount = 10200 + +const multivalue = ["0 1px", "0 0 1px 0", "0", "1px", "20px 10px", "7em 5px", "1px 0 5em 2px"] +const classes = ["foo", "foo bar", "", "baz-bat", null, "fooga", null, null, undefined] +const styles = [] + +const toColor = (c) => `rgba(${c % 255},${255 - c % 255},${50 + c % 150},${c % 50 / 50})` + +const get = (array, index) => array[index % array.length] + +for (let i = 0, counter = 0; i < styleCount; i++) { + const c = ++counter + const style = {} + styles.push(style) + switch (i % 8) { + case 7: + style.border = c % 5 ? `${c % 10}px ${c % 2 ? "solid" : "dotted"} ${toColor(c)}` : "" + // falls through + case 6: + style.color = toColor(c) + // falls through + case 5: + style.display = c % 10 ? c % 2 ? "block" : "inline" : "none" + // falls through + case 4: + style.position = c % 5 ? c % 2 ? "absolute" : "relative" : null + // falls through + case 3: + style.padding = get(multivalue, c) + // falls through + case 2: + style.margin = get(multivalue, c).replace("1px", `${c}px`) + // falls through + case 1: + style.top = c % 2 ? `${c}px` : c + // falls through + case 0: + style.left = c % 3 ? `${c}px` : c + // falls through + } +} + +const titles = [ + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f", "g", "h", + "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", +] + +const inputValues = ["test0", "test1", "test2", "test3"] + +let count = 0 + +export function mutateStylesPropertiesTree() { + count += vnodeCount + var vnodes = [] + for (var i = 0; i < vnodeCount; i++) { + var index = i + count + vnodes.push( + m("div.booga", + { + class: get(classes, index), + "data-index": index, + title: get(titles, index), + }, + m("input.dooga", {type: "checkbox", checked: index % 3 === 0}), + m("input", {value: get(inputValues, index), disabled: index % 10 ? null : true}), + m("div", {class: get(classes, Math.imul(index, 11))}, + m("p", {style: get(styles, index)}, "p1"), + m("p", {style: get(styles, index + 1)}, "p2"), + m("p", {style: get(styles, Math.imul(index, 2))}, "p3"), + m("p.zooga", {style: get(styles, Math.imul(index, 3) + 1), className: get(classes, Math.imul(index, 7))}, "p4") + ) + ) + ) + } + return vnodes +} diff --git a/performance/components/nested-tree.js b/performance/components/nested-tree.js new file mode 100644 index 000000000..de1b392be --- /dev/null +++ b/performance/components/nested-tree.js @@ -0,0 +1,63 @@ +import m from "../../dist/mithril.esm.min.js" + +const fields = [] + +for (let i=100; i--;) { + fields.push((i * 999).toString(36)) +} + +var NestedHeader = () => m("header", + m("h1.asdf", "a ", "b", " c ", 0, " d"), + m("nav", + m("a", {href: "/foo"}, "Foo"), + m("a", {href: "/bar"}, "Bar") + ) +) + +var NestedForm = () => m("form", {onsubmit() {}}, + m("input[type=checkbox][checked]"), + m("input[type=checkbox]", {checked: false}), + m("fieldset", + m("label", + m("input[type=radio][checked]") + ), + m("label", + m("input[type=radio]") + ) + ), + m("fieldset", fields.map((field) => + m("label", field, ":", m("input", {placeholder: field})) + )), + m(NestedButtonBar, null) +) + +var NestedButtonBar = () => m(".button-bar", + m(NestedButton, + {style: "width:10px; height:10px; border:1px solid #FFF;"}, + "Normal CSS" + ), + m(NestedButton, + {style: "top:0 ; right: 20"}, + "Poor CSS" + ), + m(NestedButton, + {style: "invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;", icon: true}, + "Poorer CSS" + ), + m(NestedButton, + {style: {margin: 0, padding: "10px", overflow: "visible"}}, + "Object CSS" + ) +) + +var NestedButton = (attrs) => m("button", attrs) + +var NestedMain = () => m(NestedForm) + +var NestedRoot = () => m("div.foo.bar[data-foo=bar]", + {p: 2}, + m(NestedHeader), + m(NestedMain) +) + +export const nestedTree = () => m(NestedRoot) diff --git a/performance/components/repeated-tree.js b/performance/components/repeated-tree.js new file mode 100644 index 000000000..e4e2730b1 --- /dev/null +++ b/performance/components/repeated-tree.js @@ -0,0 +1,54 @@ +import m from "../../dist/mithril.esm.min.js" + +const RepeatedHeader = () => m("header", + m("h1.asdf", "a ", "b", " c ", 0, " d"), + m("nav", + m("a", {href: "/foo"}, "Foo"), + m("a", {href: "/bar"}, "Bar") + ) +) + +const RepeatedForm = () => m("form", {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(RepeatedButtonBar, null) +) + +const RepeatedButtonBar = () => m(".button-bar", + m(RepeatedButton, + {style: "width:10px; height:10px; border:1px solid #FFF;"}, + "Normal CSS" + ), + m(RepeatedButton, + {style: "top:0 ; right: 20"}, + "Poor CSS" + ), + m(RepeatedButton, + {style: "invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;", icon: true}, + "Poorer CSS" + ), + m(RepeatedButton, + {style: {margin: 0, padding: "10px", overflow: "visible"}}, + "Object CSS" + ) +) + +const RepeatedButton = (attrs) => m("button", attrs) + +const RepeatedMain = () => m(RepeatedForm) + +const RepeatedRoot = () => m("div.foo.bar[data-foo=bar]", + {p: 2}, + m(RepeatedHeader, null), + m(RepeatedMain, null) +) + +export const repeatedTree = () => m(RepeatedRoot) diff --git a/performance/components/shuffled-keyed-tree.js b/performance/components/shuffled-keyed-tree.js new file mode 100644 index 000000000..a49d4ead8 --- /dev/null +++ b/performance/components/shuffled-keyed-tree.js @@ -0,0 +1,21 @@ +import m from "../../dist/mithril.esm.min.js" + +const keys = [] +for (let i = 0; i < 1000; i++) keys.push(`key-${i}`) + +function shuffle() { + // Performs a simple Fisher-Yates shuffle. + let current = keys.length + while (current) { + // eslint-disable-next-line no-bitwise + const index = (Math.random() * current--) | 0 + const temp = keys[index] + keys[index] = keys[current] + keys[current] = temp + } +} + +export const shuffledKeyedTree = () => { + shuffle() + return m.keyed(keys, (key) => [key, m("div.item")]) +} diff --git a/performance/components/simple-tree.js b/performance/components/simple-tree.js new file mode 100644 index 000000000..fea83c5b1 --- /dev/null +++ b/performance/components/simple-tree.js @@ -0,0 +1,45 @@ +import m from "../../dist/mithril.esm.min.js" + +const fields = [] + +for (let i=100; i--;) { + fields.push((i * 999).toString(36)) +} + +export const simpleTree = () => m(".foo.bar[data-foo=bar]", {p: 2}, + m("header", + m("h1.asdf", "a ", "b", " c ", 0, " d"), + m("nav", + m("a[href=/foo]", "Foo"), + m("a[href=/bar]", "Bar") + ) + ), + m("main", + m("form", + {onsubmit() {}}, + m("input[type=checkbox][checked]"), + m("input[type=checkbox]"), + m("fieldset", fields.map((field) => + m("label", field, ":", m("input", {placeholder: field})) + )), + 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" + ) + ) + ) + ) +) diff --git a/performance/index.html b/performance/index.html index d1a7a458b..ae98ccb61 100644 --- a/performance/index.html +++ b/performance/index.html @@ -2,11 +2,11 @@ - - - - + Performance tests + + Initializing tests. If this text doesn't change, open the browser console and check for + load errors. diff --git a/performance/routes.js b/performance/routes.js new file mode 100644 index 000000000..fb0a293ee --- /dev/null +++ b/performance/routes.js @@ -0,0 +1,24 @@ +// This makes this reusable across both the standard benchmark test set and any local ports and +// throwaway benchmark scripts that may be created. + +let routesJson = "" +let stringVarsJson = "" +let numVarsJson = "" +let templatesJson = "" + +for (let i = 0; i < 16; i++) { + for (let j = 0; j < 16; j++) { + templatesJson += `,"/foo${i}/:id${i}/bar${j}/:sub${j}"` + routesJson += `,"/foo${i}/${i}/bar${j}/${j}"` + stringVarsJson += `,{"id${i}":"${i}","sub${j}":"${j}"}` + numVarsJson += `,{"id${i}":${i},"sub${j}":${j}}` + } +} + +// Flatten everything, since they're usually flat strings in practice. +export const {routes, stringVars, numVars, templates} = JSON.parse(`{ +"routes":[${routesJson.slice(1)}], +"templates":[${templatesJson.slice(1)}], +"stringVars":[${stringVarsJson.slice(1)}], +"numVars":[${numVarsJson.slice(1)}] +}`) diff --git a/performance/serialized.js b/performance/serialized.js new file mode 100644 index 000000000..0720fb2ac --- /dev/null +++ b/performance/serialized.js @@ -0,0 +1,34 @@ +/** @param {number} value */ +export function serializeTime(value) { + let sign = "" + let unit = "ms" + let precision = 0 + + if (value < 0) { + sign = "-" + value = -value + } + + if (value >= 1000) { + value /= 1000 + unit = "s" + } else if (value >= 0.995) { + precision = 3 + } else if (value >= 0.000995) { + precision = 3 + value *= 1000 + unit = "Β΅s" + } else { + precision = value >= 0.000000995 ? 3 : 2 + value *= 1000000 + unit = "ns" + } + + return `${sign}${precision ? value.toPrecision(precision) : `${Math.round(value)}`} ${unit}` +} + +export const serializeNumber = new Intl.NumberFormat(undefined, {useGrouping: true}).format + +export function serializeRate(value, op) { + return `${serializeTime(value)}/${op} (${serializeNumber(Math.round(1000 / value))} Hz)` +} diff --git a/performance/stats.js b/performance/stats.js new file mode 100644 index 000000000..0d2146222 --- /dev/null +++ b/performance/stats.js @@ -0,0 +1,460 @@ +/** @type {Array<{count: number, sum: number}>} */ +const samples = [] +let ticks = 0 +let minSamples = 0 +let minDuration = 0 +let maxDuration = 0 +let minConfidence = 0 +let meanSum = 0 + +/** @param {import("./bench.js").BenchOptions} options */ +export function resetStats(options) { + samples.length = 0 + ticks = 0 + minSamples = options.minSamples + minDuration = options.minDuration + maxDuration = options.maxDuration + minConfidence = options.minConfidence + meanSum = 0 +} + +// Returns the population mean. The population mean is what you get when you take all the +// samples and multiply them by their probabilities. +// +// We don't have all the samples here, and for some, there's a lot of holes, possibly thousands +// of them per sample. Fortunately, math comes to the rescue, letting us not try to impute all +// that missing data. +// +// - The probability of each tick is literally just `P(v) = 1 / N`, where `N` is the total +// number of ticks. +// - Each sample represents a group of ticks that sums up to `value`, giving us a formula of +// `value = sum(0 <= i < N: x[i])`. +// - The population mean is the sample sum weighted by its probability. For this, the formula +// would be `mean = sum(0 <= i < N: x[i]) * P(x[i])`. Substituting `P(v)` in, that becomes +// `mean = sum(0 <= i < N: x[i] / N)`. This happens to be precisely the arithmetic mean. +// - The whole mean is `mean = sum(0 <= j < N: y[j] * P(y[j]))`, or as per above, +// `mean = sum(0 <= j < N: y[j] / N)`. This also is the same as the arithmetic mean of the +// inner data. +// - Merging unweighted arithmetic means (like the two above) together is very simple: take a +// weighted arithmetic mean of the means, with their weights being their sample counts. +// +// So, the population mean is just that weighted arithmetic mean. Or, in other words, where `s` +// is the list of samples: +// +// ``` +// weightSum = sum(0 <= i < count(s): count(s[i])) +// = N +// weight(v) = count(v) / weightSum +// mean = sum(0 <= i < count(s): sum(s[i]) * weight(s[i])) / N +// ``` +// +// That of course can be optimized a bit, and it's what this formula uses: +// +// ``` +// mean = sum(0 <= i < count(s): sum(s[i]) * count(s[i])) / (N^2) +// ``` +// +// Note that the above sum is built incrementally in `pushSample`. It's not generated here in this +// function - this function's intentionally cheap. +function mean() { + return meanSum / (ticks * ticks) +} + +/** + * @param {number} count + * @param {number} sum + * @param {number} duration + */ +export function pushSample(count, sum, duration) { + // Performs binary search to find the index to insert into. After the loop, the number of + // elements greater than the value is `sampleCount - R`, and the number of elements less or + // equal to the value is `R`. `R` is thus the index we want to insert at, to retain + // insertion order while still remaining sorted. + + /* eslint-disable no-bitwise */ + + let L = 0 + let R = samples.length + + while (L < R) { + // eslint-disable-next-line no-bitwise + const m = (L >>> 1) + (R >>> 1) + (L & R & 1) + if (samples[m].sum / samples[m].count > sum / count) { + R = m + } else { + L = m + 1 + } + } + + /* eslint-enable no-bitwise */ + + // Avoid the overhead of `.splice`, since that creates an array. I don't want to trust the + // engine's ability to elide the allocation. The test operation could potentially mess with + // that. + + const sample = {count, sum} + const prevLen = samples.length + // Ensure the engine can only see the sample array as a dense sample object array. + samples.push(sample) + samples.copyWithin(R + 1, R, prevLen) + samples[R] = sample + + ticks += count + meanSum += sum * count + + if (samples.length >= minSamples) { + if (duration >= maxDuration) return true + if (duration >= minDuration) { + if (marginOfError() / mean() >= minConfidence) return true + } + } + + return false +} + +// The quantile is required for the margin of error calculation and the median both, and it's +// pretty easy to compute when all samples are known: +// +// ``` +// i = q * count(s) +// j = floor(i) +// k = ceil(i) +// quantile[q] = s[i], if j = k +// = s[j] * (i - j) + s[k] * (k - i), if j β‰  k +// ``` +// +// We don't have all samples, so we're estimating it through linear interpolation. Each sample +// value is treated as a midpoint, and the count is the span the midpoint covers. Given a sample +// list of `{count: 1, mean: 0.5}, {count: 3, mean: 1}, {count: 2, mean: 2}`, we'll have a domain +// from 0 inclusive to 6 exclusive. (In reality, the values are stored as `mean * count`, not as +// `mean` directly. But means are easier to explain here) Here's a graph of this list, with each +// span's midpoints: +// +// +-----------------------------------+ +// | | +// 2 | +-----*-----| +// | | +// | | +// | | +// 1 | +--------*--------+ | +// | | +// |--*--+ | +// | | +// 0 +-----------------------------------+ +// 0 1 2 3 4 5 6 +// +// What we're actually calculating is the linear interpolation of the midpoints. At the edges, +// we'll extrapolate the previous line to the sides. Samples are always sorted by value, so this +// is safe and will never be negative on the right side. +// +// +-----------------------------------+ +// | _-Β―| +// 2 | _*Β― | +// | _-Β― | +// | _-Β― | +// | _-Β― | +// 1 | _ *Β― | +// | _ - Β― | +// | * Β― | +// | | +// 0 +-----------------------------------+ +// 0 1 2 3 4 5 6 +// +// Suppose we're looking for a quantile of 0.5, the estimated median. That would reside at offset +// 3. The midpoints we'd be interpolating are (2.5, 1) and (5, 2), giving us a segment span of 3.5. +// Interpolating this gives us a formula of `1*((3-2.5)/3.5)+2*((5-3)/3.5)`, which comes out to +// 9/7 or about 1.2857142857142856. +// +// +-----------------------------------+ +// | _-Β―| +// 2 | _*Β― | +// | _-Β― | +// | _-Β― | +// | _@Β― | +// 1 | _ *Β― | +// | _ - Β― | +// | * Β― | +// | | +// 0 +-----------------------------------+ +// 0 1 2 3 4 5 6 +// +// Suppose we're looking for a quantile of 0.98 instead. That would reside at offset 0.98*6 = 5.88, +// exceeding the midpoint (5) of the last span (4 to 6). In this case, we need to take the last two +// points, (2.5, 1) and (5, 2), and extrapolate the line they form out to x=5.88. +// +// +-----------------------------------+ +// | _-@| +// 2 | _*Β― | +// | _-Β― | +// | _-Β― | +// | _-Β― | +// 1 | _ *Β― | +// | _ - Β― | +// | * Β― | +// | | +// 0 +-----------------------------------+ +// 0 1 2 3 4 5 6 +// +// The slope between those two points is `m=(2-1)/(5-2.5)`, or 0.4. We could plug this into +// point-slope form and get an equation right away, but we need a direct formula for `y` in terms +// of `x` for the code, so we need to solve for that. +// +// ``` +// y = m*x+c <- What we want +// y-y1 = m*(x-x1) <- What we have +// y = m*(x-x1)+y1 <- We can use this as our formula +// ``` +// +// Our `(x1, y1)` can be either (2.5, 1) or (5, 2), doesn't matter. Applying this formula using the +// point (5, 2) comes out to exactly 2.352. +// +// How does this translate to code? Well, each count is a relative offset from the previous index. +// We need to scan and do an incremental sum. The span start value is the sum before adding the +// count, and the span end is the sum after adding the count. This below generates all the +// coordinates. +// +// ``` +// let sum = 0 +// for (const sample of samples) { +// const start = sum +// const end = sum + getCount(sample) +// const value = getValue(sample) +// const coordinate = {x: (end + start) / 2, y: value} +// +// sum += getCount(sample) +// } +// ``` +// +// To do the interpolation, we need to track the previous coordinate. +// +// ``` +// let lastCoordinate +// // ... +// for (const sample of samples) { +// // ... +// lastCoordinate = coordinate +// } +// ``` +// +// If the target X value equals the coordinate, it's the coordinate's value. +// +// ``` +// const targetX = Q * ticks +// // ... +// for (const sample of samples) { +// // ... +// if (coordinate.x === targetX) return coordinate.y +// // ... +// } +// ``` +// +// If the target X value is within the span between the current and previous coordiate, we perform +// linear interpolation. +// +// ``` +// const targetX = Q * ticks +// let lastCoordinate = {x: NaN, y: NaN} +// // ... +// for (const sample of samples) { +// // ... +// if (coordinate.x > targetX) { +// const dx = coordinate.x - lastCoordinate.x +// return lastCoordinate.y * ((targetX - lastCoordinate.x) / dx) + +// coordinate.y * ((coordinate.x - targetX) / dx) +// } +// // ... +// } +// ``` +// +// To do the right extrapolation, we need to track the two previous coordinates. Using the last two +// coordinates, we compute the slope and perform linear extrapolation. (Left extrapolation is +// similar, but I'm omitting it for brevity.) +// +// ``` +// const targetX = Q * ticks +// let secondLastCoordinate +// // ... +// for (const sample of samples) { +// secondLastCoordinate = lastCoordinate +// // ... +// } +// +// const m = (lastCoordinate.y - secondLastCoordinate.y) / +// (lastCoordinate.x - secondLastCoordinate.x) +// return m * (targetX - lastCoordinate.x) + lastCoordinate.y +// ``` +// +// Put all together, it looks like this. The actual code differs, but that's due to three things: +// samples are stored and accessed differently than the above code, it also implements the left +// extrapolation, and I've added a few small optimizations to things like operation order. +// +// ``` +// const targetX = Q * ticks +// let lastCoordinate = {x: NaN, y: NaN} +// let secondLastCoordinate +// let sum = 0 +// for (const sample of samples) { +// const start = sum +// const end = sum + getCount(sample) +// const value = getValue(sample) +// const coordinate = {x: (end + start) / 2, y: value} +// +// if (coordinate.x === targetX) return coordinate.y +// if (lastCoordinate.x < targetX) { +// const dx = coordinate.x - lastCoordinate.x +// return lastCoordinate.y * ((targetX - lastCoordinate.x) / dx) + +// coordinate.y * ((coordinate.x - targetX) / dx) +// } +// +// secondLastCoordinate = lastCoordinate +// lastCoordinate = coordinate +// +// sum += getCount(sample) +// } +// +// const m = (lastCoordinate.y - secondLastCoordinate.y) / +// (lastCoordinate.x - secondLastCoordinate.x) +// return m * (targetX - lastCoordinate.x) + lastCoordinate.y +// ``` +/** @param {number} Q */ +function quantile(Q) { + if (Q <= 0) { + throw new RangeError("Quantile is undefined for Q <= 0") + } + if (Q >= 1) { + throw new RangeError("Quantile is undefined for Q >= 1") + } + if (samples.length < 2) { + throw new RangeError("Quantile is undefined for N < 2") + } + + const targetX = Q * ticks + let lastX1 = NaN + let lastY1 = NaN + let lastX2 = NaN + let lastY2 = NaN + let sum = 0 + let extrapolateBack = false + + for (const sample of samples) { + const x1 = lastX1 = lastX2 + const y1 = lastY1 = lastY2 + const start = sum + const end = sum + sample.count + const x2 = lastX2 = (end + start) / 2 + const y2 = lastY2 = sample.sum / sample.count + + sum = end + + if (x2 === targetX) return y2 + if (x2 > targetX) { + // Interval: x1 <= targetX < x2 + // Interpolate from (x1, y1) to (x2, y2) + // eslint-disable-next-line no-self-compare + if (x1 === x1) { + const dx = x2 - x1 + return (y1 / dx) * (targetX - x1) + (y2 / dx) * (x2 - targetX) + } + // Interval: 0 <= targetX < first coordinate's X + // Extrapolate the line (x1, y1) to (x2, y2) backwards to targetX + if (extrapolateBack) break + extrapolateBack = true + } + } + + // Interval: last coordinate's X <= targetX < N + // Extrapolate the line (lastX1, lastY1) to (lastX2, lastY2) out to targetX + return (lastY2 - lastY1) / (lastX2 - lastX1) * (targetX - lastX2) + lastY2 +} + +// Returns the (estimated) population variance. +// +// The population variance (squared population standard deviation) is very simple: take +// each sample, subtract each one by the mean, and take the mean of the squares of those +// differences. The general formula for that is this: +// +// ``` +// variance = sum(0 <= i < N: (x[i] - mean)^2) / N +// ``` +// +// Unfortunately, we don't have all the samples. So we need to use an alternate formula to +// estimate it (and apply Bessel's correction as the weights are frequency weights). +// +// ``` +// variance = sum(0 <= i < count(s): count(s[i]) * (avg(s[i]) - mean)^2) / +// (sum(0 <= i < count(s): count(s[i])) - 1) +// = sum(0 <= i < count(s): count(s[i]) * (avg(s[i]) - mean)^2) / (N - 1) +// = sum(0 <= i < count(s): count(s[i]) * (sum(s[i]) / count(s[i]) - mean)^2) / (N - 1) +// ``` +function variance() { + if (samples.length < 2) { + throw new RangeError("Variance is undefined for N < 2") + } + + if (ticks < 2) { + throw new RangeError("Variance is undefined for population < 2") + } + + const m = mean() + let sum = 0 + + for (const sample of samples) { + const delta = sample.sum / sample.count - m + sum += sample.count * delta * delta + } + + return sum / (ticks - 1) +} + +// Returns the margin of error, with finite population correction applied. +// +// The formula for the finite population correction is this, where `n` is the sample count: +// +// ``` +// FPC = sqrt((N - n) / (N - 1)) +// ``` +// +// It's needed because it's quite common that the number of samples is pretty close to the +// total population count. Only in very fast tests is it not. +// +// The margin of error formula is this, where `0 <= q <= 1`: +// +// ``` +// MOE[q] = quantile[q] * sqrt(variance / N) * FPC +// ``` +// +// (The quantile function is computed using a separate helper function.) +// +// These two calculations can be combined, since `sqrt(a) * sqrt(b) = sqrt(a * b)`. +// +// ``` +// MOE[q] = quantile[q] * sqrt(variance / N) * sqrt((N - n) / (N - 1)) +// = quantile[q] * sqrt((variance / N) * (N - n) / (N - 1)) +// = quantile[q] * sqrt((variance * (N - n)) / (N * (N - 1))) +// ``` +function marginOfError() { + return quantile(minConfidence) * Math.sqrt( + (variance() / ticks) * + ((ticks - samples.length) / (ticks - 1)) + ) +} + +export function currentDisplayStats() { + return { + pop: ticks, + n: samples.length, + moe: marginOfError(), + mean: mean(), + median: quantile(0.5), + min: samples[0].sum / samples[0].count, + max: samples[samples.length - 1].sum / samples[samples.length - 1].count, + confidenceMin: quantile(1 - minConfidence), + confidenceMax: quantile(minConfidence), + expMin: quantile(0.3173), + expMax: quantile(0.6827), + } +} + +export function getSamples() { + return samples.slice() +} diff --git a/performance/test-perf.js b/performance/test-perf.js index 6d0891fd9..84139f49c 100644 --- a/performance/test-perf.js +++ b/performance/test-perf.js @@ -1,493 +1,279 @@ -"use strict" - -/* Based off of preact's perf tests, so including their MIT license */ -/* -The MIT License (MIT) - -Copyright (c) 2017 Jason Miller - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ - -// Note: this tests against the generated bundle in browsers, but it tests -// against `index.js` in Node. Please do keep that in mind while testing. -// -// Mithril.js and Benchmark.js are loaded globally via bundle in the browser, so -// this doesn't require a CommonJS sham polyfill. - -// I add it globally just so it's visible in the tests. -/* global m, rootElem: true */ - -// set up browser env on before running tests -var isDOM = typeof window !== "undefined" -var Benchmark - -if (isDOM) { - Benchmark = window.Benchmark - window.rootElem = null -} else { - /* eslint-disable global-require */ - global.window = require("../test-utils/browserMock")() - global.document = window.document - // We're benchmarking renders, not our throttling. - global.requestAnimationFrame = function () { - throw new Error("This should never be called.") +/* global window, document */ + +import m from "../dist/mithril.esm.min.js" + +import {setupBenchmarks} from "./bench.js" + +import {mutateStylesPropertiesTree} from "./components/mutate-styles-properties-tree.js" +import {nestedTree} from "./components/nested-tree.js" +import {repeatedTree} from "./components/repeated-tree.js" +import {shuffledKeyedTree} from "./components/shuffled-keyed-tree.js" +import {simpleTree} from "./components/simple-tree.js" + +import {numVars, routes, stringVars, templates} from "./routes.js" + +async function setup() { + if (typeof window === "undefined") { + // Gotta use `eval` here for Node. + // eslint-disable-next-line no-eval + await eval('import("../test-utils/injectBrowserMock.js")') } - global.m = require("../index.js") - global.rootElem = null - Benchmark = require("benchmark") - /* eslint-enable global-require */ } -function cycleRoot() { - if (rootElem) document.body.removeChild(rootElem) - document.body.appendChild(rootElem = document.createElement("div")) -} +const allTrees = [ + simpleTree, + nestedTree, + mutateStylesPropertiesTree, + repeatedTree, + shuffledKeyedTree, +] + +// 1. `m.match` requires a `{path, params}` object to be given (to match). +// 2. In practice, these have a shared shape, and this ensures it has that shared shape. +const routeObjects = routes.map((path) => ({path, params: new URLSearchParams()})) -// Initialize benchmark suite -Benchmark.options.async = true -Benchmark.options.initCount = 10 -Benchmark.options.minSamples = 40 +// For route selection +let i = 0 -if (isDOM) { - // Wait long enough for the browser to actually commit the DOM changes to - // the screen before moving on to the next cycle, so things are at least - // reasonably fresh each cycle. - Benchmark.options.delay = 1 / 30 /* frames per second */ +let rootElem, allElems, redraw, allRedraws + +function cycleRoot() { + if (allElems) { + for (const elem of allElems) { + elem.remove() + m.render(elem, null) + } + } + if (rootElem) { + rootElem.remove() + m.render(rootElem, null) + } + document.body.appendChild(rootElem = document.createElement("div")) } -var suite = new Benchmark.Suite("Mithril.js perf", { - onStart: function () { - this.start = Date.now() +setupBenchmarks(setup, cycleRoot, { + // This just needs to be sub-millisecond + "route match": { + fn() { + // eslint-disable-next-line no-bitwise + i = (i - 1) & 255 + return m.match(routeObjects[i], templates[i]) + }, }, - onCycle: function (e) { - console.log(e.target.toString()) - cycleRoot() + // These four need to be at most a few microseconds, as 300 of these * 3 us/op = 0.9 ms. (And yes, + // while 300 may seem like a lot, I've worked with apps that exceeded 100, and for 60 FPS, you only + // truly have room for about 5ms total for logic.) + + "route non-match": { + fn() { + const j = i + // eslint-disable-next-line no-bitwise + i = (i - 1) & 255 + return m.match(routeObjects[i], templates[j]) + }, }, - onComplete: function () { - console.log("Completed perf tests in " + (Date.now() - this.start) + "ms") + "path generate with string interpolations": { + fn() { + // eslint-disable-next-line no-bitwise + i = (i - 1) & 255 + return m.p(templates[i], stringVars[i]) + }, }, - onError: function (e) { - console.error(e) + "path generate with number interpolations": { + fn() { + // eslint-disable-next-line no-bitwise + i = (i - 1) & 255 + return m.p(templates[i], numVars[i]) + }, }, -}) -// eslint-disable-next-line no-unused-vars -var xsuite = {add: function(name) { console.log("skipping " + name) }} -suite.add("construct large vnode tree", { - setup: function () { - this.fields = [] - - for(var i=100; i--;) { - this.fields.push((i * 999).toString(36)) - } + "path generate no interpolations": { + fn() { + // eslint-disable-next-line no-bitwise + i = (i - 1) & 255 + return m.p(templates[i]) + }, }, - fn: function () { - m(".foo.bar[data-foo=bar]", {p: 2}, - m("header", - m("h1.asdf", "a ", "b", " c ", 0, " d"), - m("nav", - m("a[href=/foo]", "Foo"), - m("a[href=/bar]", "Bar") - ) - ), - m("main", - m("form", - {onSubmit: function () {}}, - m("input[type=checkbox][checked]"), - m("input[type=checkbox]"), - m("fieldset", - this.fields.map(function (field) { - return m("label", - field, - ":", - m("input", {placeholder: field}) - ) - }) - ), - 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("rerender identical vnode", { - setup: function () { - this.cached = m(".foo.bar[data-foo=bar]", {p: 2}, - m("header", - m("h1.asdf", "a ", "b", " c ", 0, " d"), - m("nav", - m("a", {href: "/foo"}, "Foo"), - m("a", {href: "/bar"}, "Bar") - ) - ), - m("main", - m("form", {onSubmit: function () {}}, - 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" - ) - ) - ) - ) - ) - }, - fn: function () { - m.render(rootElem, this.cached) + "construct `simpleTree`": { + fn: simpleTree, }, -}) -suite.add("rerender same tree", { - fn: function () { - m.render(rootElem, m(".foo.bar[data-foo=bar]", {p: 2}, - m("header", - m("h1.asdf", "a ", "b", " c ", 0, " d"), - m("nav", - m("a", {href: "/foo"}, "Foo"), - m("a", {href: "/bar"}, "Bar") - ) - ), - m("main", - m("form", {onSubmit: function () {}}, - 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" - ) - ) - ) - ) - )) + "render `simpleTree`": { + tick() { + cycleRoot() + m.render(rootElem, simpleTree()) + }, + fn() { + m.render(rootElem, simpleTree()) + }, }, -}) -suite.add("add large nested tree", { - setup: function () { - var fields = [] - - for(var i=100; i--;) { - fields.push((i * 999).toString(36)) - } - - var NestedHeader = { - view: function () { - return m("header", - m("h1.asdf", "a ", "b", " c ", 0, " d"), - m("nav", - m("a", {href: "/foo"}, "Foo"), - m("a", {href: "/bar"}, "Bar") - ) - ) - } - } + "add/remove `simpleTree`": { + tick() { + cycleRoot() + m.render(rootElem, null) + }, + fn() { + m.render(rootElem, simpleTree()) + m.render(rootElem, null) + }, + }, - var NestedForm = { - view: function () { - return m("form", {onSubmit: function () {}}, - m("input[type=checkbox][checked]"), - m("input[type=checkbox]", {checked: false}), - m("fieldset", - m("label", - m("input[type=radio][checked]") - ), - m("label", - m("input[type=radio]") - ) - ), - m("fieldset", - fields.map(function (field) { - return m("label", - field, - ":", - m("input", {placeholder: field}) - ) - }) - ), - m(NestedButtonBar, null) - ) - } - } + "construct `nestedTree`": { + fn: nestedTree, + }, - var NestedButtonBar = { - view: function () { - return m(".button-bar", - m(NestedButton, - {style: "width:10px; height:10px; border:1px solid #FFF;"}, - "Normal CSS" - ), - m(NestedButton, - {style: "top:0 ; right: 20"}, - "Poor CSS" - ), - m(NestedButton, - {style: "invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;", icon: true}, - "Poorer CSS" - ), - m(NestedButton, - {style: {margin: 0, padding: "10px", overflow: "visible"}}, - "Object CSS" - ) - ) - } - } + "render `nestedTree`": { + tick() { + cycleRoot() + m.render(rootElem, nestedTree()) + }, + fn() { + m.render(rootElem, nestedTree()) + }, + }, - var NestedButton = { - view: function (vnode) { - return m("button", m.censor(vnode.attrs), vnode.children) - } - } + "add/remove `nestedTree`": { + tick() { + cycleRoot() + m.render(rootElem, null) + }, + fn() { + m.render(rootElem, nestedTree()) + m.render(rootElem, null) + }, + }, - var NestedMain = { - view: function () { - return m(NestedForm) - } - } + "construct `mutateStylesPropertiesTree`": { + fn: mutateStylesPropertiesTree, + }, - this.NestedRoot = { - view: function () { - return m("div.foo.bar[data-foo=bar]", - {p: 2}, - m(NestedHeader), - m(NestedMain) - ) - } - } + "render `mutateStylesPropertiesTree`": { + tick() { + cycleRoot() + m.render(rootElem, mutateStylesPropertiesTree()) + }, + fn() { + m.render(rootElem, mutateStylesPropertiesTree()) + }, }, - fn: function () { - m.render(rootElem, m(this.NestedRoot)) + + "add/remove `mutateStylesPropertiesTree`": { + tick() { + cycleRoot() + m.render(rootElem, null) + }, + fn() { + m.render(rootElem, mutateStylesPropertiesTree()) + m.render(rootElem, null) + }, }, -}) -suite.add("mutate styles/properties", { - setup: function () { - function get(obj, i) { return obj[i % obj.length] } - var counter = 0 - 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 = [ - ["left", function (c) { return c % 3 ? c + "px" : c }], - ["top", function (c) { return c % 2 ? c + "px" : c }], - ["margin", function (c) { return get(multivalue, c).replace("1px", c+"px") }], - ["padding", function (c) { return get(multivalue, c) }], - ["position", function (c) { return c%5 ? c%2 ? "absolute" : "relative" : null }], - ["display", function (c) { return c%10 ? c%2 ? "block" : "inline" : "none" }], - ["color", function (c) { return ("rgba(" + (c%255) + ", " + (255 - c%255) + ", " + (50+c%150) + ", " + (c%50/50) + ")") }], - ["border", function (c) { return c%5 ? (c%10) + "px " + (c%2?"solid":"dotted") + " " + stylekeys[6][1](c) : "" }] - ] - var i, j, style, conf - - for (i=0; i<1000; i++) { - style = {} - for (j=0; j { + const elem = document.createElement("div") + rootElem.appendChild(elem) + // For consistency across the interval + m.mount(elem, tree) + return elem + }) + }, + fn() { + for (let i = 0; i < allTrees.length; i++) { + m.mount(allElems[i], allTrees[i]) } - } + }, }, - fn: function () { - m.render(rootElem, [m(this.RepeatedRoot)]) - m.render(rootElem, []) + + "redraw all": { + tick() { + cycleRoot() + allElems = allTrees.map(() => { + const elem = document.createElement("div") + rootElem.appendChild(elem) + return elem + }) + allRedraws = allElems.map((elem, i) => m.mount(elem, allTrees[i])) + }, + fn() { + for (const redraw of allRedraws) redraw.sync() + }, }, }) - -if (isDOM) { - window.onload = function () { - cycleRoot() - suite.run() - } -} else { - cycleRoot() - suite.run() -} diff --git a/querystring/build.js b/querystring/build.js deleted file mode 100644 index 041249565..000000000 --- a/querystring/build.js +++ /dev/null @@ -1,26 +0,0 @@ -"use strict" - -module.exports = function(object) { - if (Object.prototype.toString.call(object) !== "[object Object]") return "" - - var args = [] - for (var key in object) { - destructure(key, object[key]) - } - - return args.join("&") - - function destructure(key, value) { - if (Array.isArray(value)) { - for (var i = 0; i < value.length; i++) { - destructure(key + "[" + i + "]", value[i]) - } - } - else if (Object.prototype.toString.call(value) === "[object Object]") { - for (var i in value) { - destructure(key + "[" + i + "]", value[i]) - } - } - else args.push(encodeURIComponent(key) + (value != null && value !== "" ? "=" + encodeURIComponent(value) : "")) - } -} diff --git a/querystring/parse.js b/querystring/parse.js deleted file mode 100644 index 1f2300a28..000000000 --- a/querystring/parse.js +++ /dev/null @@ -1,51 +0,0 @@ -"use strict" - -function decodeURIComponentSave(str) { - try { - return decodeURIComponent(str) - } catch(err) { - return str - } -} - -module.exports = function(string) { - if (string === "" || string == null) return {} - if (string.charAt(0) === "?") string = string.slice(1) - - var entries = string.split("&"), counters = {}, data = {} - for (var i = 0; i < entries.length; i++) { - var entry = entries[i].split("=") - var key = decodeURIComponentSave(entry[0]) - var value = entry.length === 2 ? decodeURIComponentSave(entry[1]) : "" - - if (value === "true") value = true - else if (value === "false") value = false - - var levels = key.split(/\]\[?|\[/) - var cursor = data - if (key.indexOf("[") > -1) levels.pop() - for (var j = 0; j < levels.length; j++) { - var level = levels[j], nextLevel = levels[j + 1] - var isNumber = nextLevel == "" || !isNaN(parseInt(nextLevel, 10)) - if (level === "") { - var key = levels.slice(0, j).join() - if (counters[key] == null) { - counters[key] = Array.isArray(cursor) ? cursor.length : 0 - } - level = counters[key]++ - } - // Disallow direct prototype pollution - else if (level === "__proto__") break - if (j === levels.length - 1) cursor[level] = value - else { - // Read own properties exclusively to disallow indirect - // prototype pollution - var desc = Object.getOwnPropertyDescriptor(cursor, level) - if (desc != null) desc = desc.value - if (desc == null) cursor[level] = desc = isNumber ? [] : {} - cursor = desc - } - } - } - return data -} diff --git a/querystring/tests/test-buildQueryString.js b/querystring/tests/test-buildQueryString.js deleted file mode 100644 index 9b5149af5..000000000 --- a/querystring/tests/test-buildQueryString.js +++ /dev/null @@ -1,87 +0,0 @@ -"use strict" - -var o = require("ospec") -var buildQueryString = require("../../querystring/build") - -o.spec("buildQueryString", function() { - o("handles flat object", function() { - var string = buildQueryString({a: "b", c: 1}) - - o(string).equals("a=b&c=1") - }) - o("handles escaped values", function() { - var data = buildQueryString({";:@&=+$,/?%#": ";:@&=+$,/?%#"}) - - o(data).equals("%3B%3A%40%26%3D%2B%24%2C%2F%3F%25%23=%3B%3A%40%26%3D%2B%24%2C%2F%3F%25%23") - }) - o("handles unicode", function() { - var data = buildQueryString({"ΓΆ": "ΓΆ"}) - - o(data).equals("%C3%B6=%C3%B6") - }) - o("handles nested object", function() { - var string = buildQueryString({a: {b: 1, c: 2}}) - - o(string).equals("a%5Bb%5D=1&a%5Bc%5D=2") - }) - o("handles deep nested object", function() { - var string = buildQueryString({a: {b: {c: 1, d: 2}}}) - - o(string).equals("a%5Bb%5D%5Bc%5D=1&a%5Bb%5D%5Bd%5D=2") - }) - o("handles nested array", function() { - var string = buildQueryString({a: ["x", "y"]}) - - o(string).equals("a%5B0%5D=x&a%5B1%5D=y") - }) - o("handles array w/ dupe values", function() { - var string = buildQueryString({a: ["x", "x"]}) - - o(string).equals("a%5B0%5D=x&a%5B1%5D=x") - }) - o("handles deep nested array", function() { - var string = buildQueryString({a: [["x", "y"]]}) - - o(string).equals("a%5B0%5D%5B0%5D=x&a%5B0%5D%5B1%5D=y") - }) - o("handles deep nested array in object", function() { - var string = buildQueryString({a: {b: ["x", "y"]}}) - - o(string).equals("a%5Bb%5D%5B0%5D=x&a%5Bb%5D%5B1%5D=y") - }) - o("handles deep nested object in array", function() { - var string = buildQueryString({a: [{b: 1, c: 2}]}) - - o(string).equals("a%5B0%5D%5Bb%5D=1&a%5B0%5D%5Bc%5D=2") - }) - o("handles date", function() { - var string = buildQueryString({a: new Date(0)}) - - o(string).equals("a=" + encodeURIComponent(new Date(0).toString())) - }) - o("turns null into value-less string (like jQuery)", function() { - var string = buildQueryString({a: null}) - - o(string).equals("a") - }) - o("turns undefined into value-less string (like jQuery)", function() { - var string = buildQueryString({a: undefined}) - - o(string).equals("a") - }) - o("turns empty string into value-less string (like jQuery)", function() { - var string = buildQueryString({a: ""}) - - o(string).equals("a") - }) - o("handles zero", function() { - var string = buildQueryString({a: 0}) - - o(string).equals("a=0") - }) - o("handles false", function() { - var string = buildQueryString({a: false}) - - o(string).equals("a=false") - }) -}) diff --git a/querystring/tests/test-parseQueryString.js b/querystring/tests/test-parseQueryString.js deleted file mode 100644 index 8d497cdb6..000000000 --- a/querystring/tests/test-parseQueryString.js +++ /dev/null @@ -1,119 +0,0 @@ -"use strict" - -var o = require("ospec") -var parseQueryString = require("../../querystring/parse") - -o.spec("parseQueryString", function() { - o("works", function() { - var data = parseQueryString("?aaa=bbb") - o(data).deepEquals({aaa: "bbb"}) - }) - o("parses empty string", function() { - var data = parseQueryString("") - o(data).deepEquals({}) - }) - o("parses flat object", function() { - var data = parseQueryString("?a=b&c=d") - o(data).deepEquals({a: "b", c: "d"}) - }) - o("handles escaped values", function() { - var data = parseQueryString("?%3B%3A%40%26%3D%2B%24%2C%2F%3F%25%23=%3B%3A%40%26%3D%2B%24%2C%2F%3F%25%23") - o(data).deepEquals({";:@&=+$,/?%#": ";:@&=+$,/?%#"}) - }) - o("handles wrongly escaped values", function() { - var data = parseQueryString("?test=%c5%a1%e8ZM%80%82H") - o(data).deepEquals({test: "%c5%a1%e8ZM%80%82H"}) - }) - o("handles escaped slashes followed by a number", function () { - var data = parseQueryString("?hello=%2Fen%2F1") - o(data.hello).equals("/en/1") - }) - o("handles escaped square brackets", function() { - var data = parseQueryString("?a%5B%5D=b") - o(data).deepEquals({"a": ["b"]}) - }) - o("handles escaped unicode", function() { - var data = parseQueryString("?%C3%B6=%C3%B6") - o(data).deepEquals({"ΓΆ": "ΓΆ"}) - }) - o("handles unicode", function() { - var data = parseQueryString("?ΓΆ=ΓΆ") - o(data).deepEquals({"ΓΆ": "ΓΆ"}) - }) - o("parses without question mark", function() { - var data = parseQueryString("a=b&c=d") - o(data).deepEquals({a: "b", c: "d"}) - }) - o("parses nested object", function() { - var data = parseQueryString("a[b]=x&a[c]=y") - o(data).deepEquals({a: {b: "x", c: "y"}}) - }) - o("parses deep nested object", function() { - var data = parseQueryString("a[b][c]=x&a[b][d]=y") - o(data).deepEquals({a: {b: {c: "x", d: "y"}}}) - }) - o("parses nested array", function() { - var data = parseQueryString("a[0]=x&a[1]=y") - o(data).deepEquals({a: ["x", "y"]}) - }) - o("parses deep nested array", function() { - var data = parseQueryString("a[0][0]=x&a[0][1]=y") - o(data).deepEquals({a: [["x", "y"]]}) - }) - o("parses deep nested object in array", function() { - var data = parseQueryString("a[0][c]=x&a[0][d]=y") - o(data).deepEquals({a: [{c: "x", d: "y"}]}) - }) - o("parses deep nested array in object", function() { - var data = parseQueryString("a[b][0]=x&a[b][1]=y") - o(data).deepEquals({a: {b: ["x", "y"]}}) - }) - o("parses array without index", function() { - var data = parseQueryString("a[]=x&a[]=y&b[]=w&b[]=z") - o(data).deepEquals({a: ["x", "y"], b: ["w", "z"]}) - }) - o("casts booleans", function() { - var data = parseQueryString("a=true&b=false") - o(data).deepEquals({a: true, b: false}) - }) - o("does not cast numbers", function() { - var data = parseQueryString("a=1&b=-2.3&c=0x10&d=1e2&e=Infinity") - o(data).deepEquals({a: "1", b: "-2.3", c: "0x10", d: "1e2", e: "Infinity"}) - }) - o("does not cast NaN", function() { - var data = parseQueryString("a=NaN") - o(data.a).equals("NaN") - }) - o("does not casts Date", function() { - var data = parseQueryString("a=1970-01-01") - o(typeof data.a).equals("string") - o(data.a).equals("1970-01-01") - }) - o("does not cast empty string to number", function() { - var data = parseQueryString("a=") - o(data).deepEquals({a: ""}) - }) - o("does not cast void to number", function() { - var data = parseQueryString("a") - o(data).deepEquals({a: ""}) - }) - o("prefers later values", function() { - var data = parseQueryString("a=1&b=2&a=3") - o(data).deepEquals({a: "3", b: "2"}) - }) - o("doesn't pollute prototype directly, censors `__proto__`", function() { - var prev = Object.prototype.toString - var data = parseQueryString("a=b&__proto__%5BtoString%5D=123") - o(Object.prototype.toString).equals(prev) - o(data).deepEquals({a: "b"}) - }) - o("doesn't pollute prototype indirectly, retains `constructor`", function() { - var prev = Object.prototype.toString - var data = parseQueryString("a=b&constructor%5Bprototype%5D%5BtoString%5D=123") - o(Object.prototype.toString).equals(prev) - // The deep matcher is borked here. - o(Object.keys(data)).deepEquals(["a", "constructor"]) - o(data.a).equals("b") - o(data.constructor).deepEquals({prototype: {toString: "123"}}) - }) -}) diff --git a/redraw.js b/redraw.js deleted file mode 100644 index af43394dc..000000000 --- a/redraw.js +++ /dev/null @@ -1,3 +0,0 @@ -"use strict" - -module.exports = require("./mount-redraw").redraw diff --git a/render.js b/render.js deleted file mode 100644 index 042d78125..000000000 --- a/render.js +++ /dev/null @@ -1,3 +0,0 @@ -"use strict" - -module.exports = require("./render/render")(typeof window !== "undefined" ? window : null) diff --git a/render/domFor.js b/render/domFor.js deleted file mode 100644 index 16b17a972..000000000 --- a/render/domFor.js +++ /dev/null @@ -1,27 +0,0 @@ -"use strict" - -var delayedRemoval = new WeakMap - -function *domFor(vnode, object = {}) { - // To avoid unintended mangling of the internal bundler, - // parameter destructuring is not used here. - var dom = vnode.dom - var domSize = vnode.domSize - var generation = object.generation - if (dom != null) do { - var nextSibling = dom.nextSibling - - if (delayedRemoval.get(dom) === generation) { - yield dom - domSize-- - } - - dom = nextSibling - } - while (domSize) -} - -module.exports = { - delayedRemoval: delayedRemoval, - domFor: domFor, -} diff --git a/render/fragment.js b/render/fragment.js deleted file mode 100644 index 90cad40da..000000000 --- a/render/fragment.js +++ /dev/null @@ -1,12 +0,0 @@ -"use strict" - -var Vnode = require("../render/vnode") -var hyperscriptVnode = require("./hyperscriptVnode") - -module.exports = function() { - var vnode = hyperscriptVnode.apply(0, arguments) - - vnode.tag = "[" - vnode.children = Vnode.normalizeChildren(vnode.children) - return vnode -} diff --git a/render/hyperscript.js b/render/hyperscript.js deleted file mode 100644 index 8b34233e9..000000000 --- a/render/hyperscript.js +++ /dev/null @@ -1,86 +0,0 @@ -"use strict" - -var Vnode = require("../render/vnode") -var hyperscriptVnode = require("./hyperscriptVnode") -var hasOwn = require("../util/hasOwn") - -var selectorParser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[(.+?)(?:\s*=\s*("|'|)((?:\\["'\]]|.)*?)\5)?\])/g -var selectorCache = Object.create(null) - -function isEmpty(object) { - for (var key in object) if (hasOwn.call(object, key)) return false - return true -} - -function compileSelector(selector) { - var match, tag = "div", classes = [], attrs = {} - while (match = selectorParser.exec(selector)) { - var type = match[1], value = match[2] - if (type === "" && value !== "") tag = value - else if (type === "#") attrs.id = value - else if (type === ".") classes.push(value) - else if (match[3][0] === "[") { - var attrValue = match[6] - if (attrValue) attrValue = attrValue.replace(/\\(["'])/g, "$1").replace(/\\\\/g, "\\") - if (match[4] === "class") classes.push(attrValue) - else attrs[match[4]] = attrValue === "" ? attrValue : attrValue || true - } - } - if (classes.length > 0) attrs.className = classes.join(" ") - if (isEmpty(attrs)) attrs = null - return selectorCache[selector] = {tag: tag, attrs: attrs} -} - -function execSelector(state, vnode) { - var attrs = vnode.attrs - var hasClass = hasOwn.call(attrs, "class") - var className = hasClass ? attrs.class : attrs.className - - vnode.tag = state.tag - - if (state.attrs != null) { - attrs = Object.assign({}, state.attrs, attrs) - - if (className != null || state.attrs.className != null) attrs.className = - className != null - ? state.attrs.className != null - ? String(state.attrs.className) + " " + String(className) - : className - : state.attrs.className != null - ? state.attrs.className - : null - } else { - if (className != null) attrs.className = className - } - - if (hasClass) attrs.class = null - - // workaround for #2622 (reorder keys in attrs to set "type" first) - // The DOM does things to inputs based on the "type", so it needs set first. - // See: https://github.com/MithrilJS/mithril.js/issues/2622 - if (state.tag === "input" && hasOwn.call(attrs, "type")) { - attrs = Object.assign({type: attrs.type}, attrs) - } - - vnode.attrs = attrs - - return vnode -} - -function hyperscript(selector) { - 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."); - } - - var vnode = hyperscriptVnode.apply(1, arguments) - - if (typeof selector === "string") { - vnode.children = Vnode.normalizeChildren(vnode.children) - if (selector !== "[") return execSelector(selectorCache[selector] || compileSelector(selector), vnode) - } - - vnode.tag = selector - return vnode -} - -module.exports = hyperscript diff --git a/render/hyperscriptVnode.js b/render/hyperscriptVnode.js deleted file mode 100644 index 9f31235c4..000000000 --- a/render/hyperscriptVnode.js +++ /dev/null @@ -1,53 +0,0 @@ -"use strict" - -var Vnode = require("../render/vnode") - -// Call via `hyperscriptVnode.apply(startOffset, arguments)` -// -// The reason I do it this way, forwarding the arguments and passing the start -// offset in `this`, is so I don't have to create a temporary array in a -// performance-critical path. -// -// In native ES6, I'd instead add a final `...args` parameter to the -// `hyperscript` and `fragment` factories and define this as -// `hyperscriptVnode(...args)`, since modern engines do optimize that away. But -// ES5 (what Mithril.js requires thanks to IE support) doesn't give me that luxury, -// and engines aren't nearly intelligent enough to do either of these: -// -// 1. Elide the allocation for `[].slice.call(arguments, 1)` when it's passed to -// another function only to be indexed. -// 2. Elide an `arguments` allocation when it's passed to any function other -// than `Function.prototype.apply` or `Reflect.apply`. -// -// In ES6, it'd probably look closer to this (I'd need to profile it, though): -// module.exports = function(attrs, ...children) { -// if (attrs == null || typeof attrs === "object" && attrs.tag == null && !Array.isArray(attrs)) { -// if (children.length === 1 && Array.isArray(children[0])) children = children[0] -// } else { -// children = children.length === 0 && Array.isArray(attrs) ? attrs : [attrs, ...children] -// attrs = undefined -// } -// -// if (attrs == null) attrs = {} -// return Vnode("", attrs.key, attrs, children) -// } -module.exports = function() { - var attrs = arguments[this], start = this + 1, children - - if (attrs == null) { - attrs = {} - } else if (typeof attrs !== "object" || attrs.tag != null || Array.isArray(attrs)) { - attrs = {} - start = this - } - - if (arguments.length === start + 1) { - children = arguments[start] - if (!Array.isArray(children)) children = [children] - } else { - children = [] - while (start < arguments.length) children.push(arguments[start++]) - } - - return Vnode("", attrs.key, attrs, children) -} diff --git a/render/render.js b/render/render.js deleted file mode 100644 index a31d19346..000000000 --- a/render/render.js +++ /dev/null @@ -1,936 +0,0 @@ -"use strict" - -var Vnode = require("../render/vnode") -var df = require("../render/domFor") -var delayedRemoval = df.delayedRemoval -var domFor = df.domFor - -module.exports = function() { - var nameSpace = { - svg: "http://www.w3.org/2000/svg", - math: "http://www.w3.org/1998/Math/MathML" - } - - var currentRedraw - var currentRender - - function getDocument(dom) { - return dom.ownerDocument; - } - - function getNameSpace(vnode) { - return vnode.attrs && vnode.attrs.xmlns || nameSpace[vnode.tag] - } - - //sanity check to discourage people from doing `vnode.state = ...` - function checkState(vnode, original) { - if (vnode.state !== original) throw new Error("'vnode.state' must not be modified.") - } - - //Note: the hook is passed as the `this` argument to allow proxying the - //arguments without requiring a full array allocation to do so. It also - //takes advantage of the fact the current `vnode` is the first argument in - //all lifecycle methods. - function callHook(vnode) { - var original = vnode.state - try { - return this.apply(original, arguments) - } finally { - checkState(vnode, original) - } - } - - // IE11 (at least) throws an UnspecifiedError when accessing document.activeElement when - // inside an iframe. Catch and swallow this error, and heavy-handidly return null. - function activeElement(dom) { - try { - return getDocument(dom).activeElement - } catch (e) { - return null - } - } - //create - function createNodes(parent, vnodes, start, end, hooks, nextSibling, ns) { - for (var i = start; i < end; i++) { - var vnode = vnodes[i] - if (vnode != null) { - createNode(parent, vnode, hooks, ns, nextSibling) - } - } - } - function createNode(parent, vnode, hooks, ns, nextSibling) { - var tag = vnode.tag - if (typeof tag === "string") { - vnode.state = {} - if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) - switch (tag) { - case "#": createText(parent, vnode, nextSibling); break - case "<": createHTML(parent, vnode, ns, nextSibling); break - case "[": createFragment(parent, vnode, hooks, ns, nextSibling); break - default: createElement(parent, vnode, hooks, ns, nextSibling) - } - } - else createComponent(parent, vnode, hooks, ns, nextSibling) - } - function createText(parent, vnode, nextSibling) { - vnode.dom = getDocument(parent).createTextNode(vnode.children) - insertDOM(parent, vnode.dom, nextSibling) - } - var possibleParents = {caption: "table", thead: "table", tbody: "table", tfoot: "table", tr: "tbody", th: "tr", td: "tr", colgroup: "table", col: "colgroup"} - function createHTML(parent, vnode, ns, nextSibling) { - var match = vnode.children.match(/^\s*?<(\w+)/im) || [] - // not using the proper parent makes the child element(s) vanish. - // var div = document.createElement("div") - // div.innerHTML = "ij" - // console.log(div.innerHTML) - // --> "ij", no in sight. - var temp = getDocument(parent).createElement(possibleParents[match[1]] || "div") - if (ns === "http://www.w3.org/2000/svg") { - temp.innerHTML = "" + vnode.children + "" - temp = temp.firstChild - } else { - temp.innerHTML = vnode.children - } - vnode.dom = temp.firstChild - vnode.domSize = temp.childNodes.length - // Capture nodes to remove, so we don't confuse them. - var fragment = getDocument(parent).createDocumentFragment() - var child - while (child = temp.firstChild) { - fragment.appendChild(child) - } - insertDOM(parent, fragment, nextSibling) - } - function createFragment(parent, vnode, hooks, ns, nextSibling) { - var fragment = getDocument(parent).createDocumentFragment() - if (vnode.children != null) { - var children = vnode.children - createNodes(fragment, children, 0, children.length, hooks, null, ns) - } - vnode.dom = fragment.firstChild - vnode.domSize = fragment.childNodes.length - insertDOM(parent, fragment, nextSibling) - } - function createElement(parent, vnode, hooks, ns, nextSibling) { - var tag = vnode.tag - var attrs = vnode.attrs - var is = attrs && attrs.is - - ns = getNameSpace(vnode) || ns - - var element = ns ? - is ? getDocument(parent).createElementNS(ns, tag, {is: is}) : getDocument(parent).createElementNS(ns, tag) : - is ? getDocument(parent).createElement(tag, {is: is}) : getDocument(parent).createElement(tag) - vnode.dom = element - - if (attrs != null) { - setAttrs(vnode, attrs, ns) - } - - insertDOM(parent, element, nextSibling) - - if (!maybeSetContentEditable(vnode)) { - if (vnode.children != null) { - var children = vnode.children - createNodes(element, children, 0, children.length, hooks, null, ns) - if (vnode.tag === "select" && attrs != null) setLateSelectAttrs(vnode, attrs) - } - } - } - function initComponent(vnode, hooks) { - var sentinel - if (typeof vnode.tag.view === "function") { - vnode.state = Object.create(vnode.tag) - sentinel = vnode.state.view - if (sentinel.$$reentrantLock$$ != null) return - sentinel.$$reentrantLock$$ = true - } else { - vnode.state = void 0 - sentinel = vnode.tag - if (sentinel.$$reentrantLock$$ != null) return - sentinel.$$reentrantLock$$ = true - vnode.state = (vnode.tag.prototype != null && typeof vnode.tag.prototype.view === "function") ? new vnode.tag(vnode) : vnode.tag(vnode) - } - initLifecycle(vnode.state, vnode, hooks) - if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) - vnode.instance = Vnode.normalize(callHook.call(vnode.state.view, vnode)) - if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") - sentinel.$$reentrantLock$$ = null - } - function createComponent(parent, vnode, hooks, ns, nextSibling) { - initComponent(vnode, hooks) - if (vnode.instance != null) { - createNode(parent, vnode.instance, hooks, ns, nextSibling) - vnode.dom = vnode.instance.dom - vnode.domSize = vnode.dom != null ? vnode.instance.domSize : 0 - } - else { - vnode.domSize = 0 - } - } - - //update - /** - * @param {Element|Fragment} parent - the parent element - * @param {Vnode[] | null} old - the list of vnodes of the last `render()` call for - * this part of the tree - * @param {Vnode[] | null} vnodes - as above, but for the current `render()` call. - * @param {Function[]} hooks - an accumulator of post-render hooks (oncreate/onupdate) - * @param {Element | null} nextSibling - the next DOM node if we're dealing with a - * fragment that is not the last item in its - * parent - * @param {'svg' | 'math' | String | null} ns) - the current XML namespace, if any - * @returns void - */ - // This function diffs and patches lists of vnodes, both keyed and unkeyed. - // - // We will: - // - // 1. describe its general structure - // 2. focus on the diff algorithm optimizations - // 3. discuss DOM node operations. - - // ## Overview: - // - // The updateNodes() function: - // - deals with trivial cases - // - determines whether the lists are keyed or unkeyed based on the first non-null node - // of each list. - // - diffs them and patches the DOM if needed (that's the brunt of the code) - // - manages the leftovers: after diffing, are there: - // - old nodes left to remove? - // - new nodes to insert? - // deal with them! - // - // The lists are only iterated over once, with an exception for the nodes in `old` that - // are visited in the fourth part of the diff and in the `removeNodes` loop. - - // ## Diffing - // - // Reading https://github.com/localvoid/ivi/blob/ddc09d06abaef45248e6133f7040d00d3c6be853/packages/ivi/src/vdom/implementation.ts#L617-L837 - // may be good for context on longest increasing subsequence-based logic for moving nodes. - // - // In order to diff keyed lists, one has to - // - // 1) match nodes in both lists, per key, and update them accordingly - // 2) create the nodes present in the new list, but absent in the old one - // 3) remove the nodes present in the old list, but absent in the new one - // 4) figure out what nodes in 1) to move in order to minimize the DOM operations. - // - // To achieve 1) one can create a dictionary of keys => index (for the old list), then iterate - // over the new list and for each new vnode, find the corresponding vnode in the old list using - // the map. - // 2) is achieved in the same step: if a new node has no corresponding entry in the map, it is new - // and must be created. - // For the removals, we actually remove the nodes that have been updated from the old list. - // The nodes that remain in that list after 1) and 2) have been performed can be safely removed. - // The fourth step is a bit more complex and relies on the longest increasing subsequence (LIS) - // algorithm. - // - // the longest increasing subsequence is the list of nodes that can remain in place. Imagine going - // from `1,2,3,4,5` to `4,5,1,2,3` where the numbers are not necessarily the keys, but the indices - // corresponding to the keyed nodes in the old list (keyed nodes `e,d,c,b,a` => `b,a,e,d,c` would - // match the above lists, for example). - // - // In there are two increasing subsequences: `4,5` and `1,2,3`, the latter being the longest. We - // can update those nodes without moving them, and only call `insertNode` on `4` and `5`. - // - // @localvoid adapted the algo to also support node deletions and insertions (the `lis` is actually - // the longest increasing subsequence *of old nodes still present in the new list*). - // - // It is a general algorithm that is fireproof in all circumstances, but it requires the allocation - // and the construction of a `key => oldIndex` map, and three arrays (one with `newIndex => oldIndex`, - // the `LIS` and a temporary one to create the LIS). - // - // So we cheat where we can: if the tails of the lists are identical, they are guaranteed to be part of - // the LIS and can be updated without moving them. - // - // If two nodes are swapped, they are guaranteed not to be part of the LIS, and must be moved (with - // the exception of the last node if the list is fully reversed). - // - // ## Finding the next sibling. - // - // `updateNode()` and `createNode()` expect a nextSibling parameter to perform DOM operations. - // When the list is being traversed top-down, at any index, the DOM nodes up to the previous - // vnode reflect the content of the new list, whereas the rest of the DOM nodes reflect the old - // list. The next sibling must be looked for in the old list using `getNextSibling(... oldStart + 1 ...)`. - // - // In the other scenarios (swaps, upwards traversal, map-based diff), - // the new vnodes list is traversed upwards. The DOM nodes at the bottom of the list reflect the - // bottom part of the new vnodes list, and we can use the `v.dom` value of the previous node - // as the next sibling (cached in the `nextSibling` variable). - - - // ## DOM node moves - // - // In most scenarios `updateNode()` and `createNode()` perform the DOM operations. However, - // this is not the case if the node moved (second and fourth part of the diff algo). We move - // the old DOM nodes before updateNode runs because it enables us to use the cached `nextSibling` - // variable rather than fetching it using `getNextSibling()`. - - function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) { - if (old === vnodes || old == null && vnodes == null) return - else if (old == null || old.length === 0) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns) - else if (vnodes == null || vnodes.length === 0) removeNodes(parent, old, 0, old.length) - else { - var isOldKeyed = old[0] != null && old[0].key != null - var isKeyed = vnodes[0] != null && vnodes[0].key != null - var start = 0, oldStart = 0 - if (!isOldKeyed) while (oldStart < old.length && old[oldStart] == null) oldStart++ - if (!isKeyed) while (start < vnodes.length && vnodes[start] == null) start++ - if (isOldKeyed !== isKeyed) { - removeNodes(parent, old, oldStart, old.length) - createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns) - } else if (!isKeyed) { - // Don't index past the end of either list (causes deopts). - var commonLength = old.length < vnodes.length ? old.length : vnodes.length - // Rewind if necessary to the first non-null index on either side. - // We could alternatively either explicitly create or remove nodes when `start !== oldStart` - // but that would be optimizing for sparse lists which are more rare than dense ones. - start = start < oldStart ? start : oldStart - for (; start < commonLength; start++) { - o = old[start] - v = vnodes[start] - if (o === v || o == null && v == null) continue - else if (o == null) createNode(parent, v, hooks, ns, getNextSibling(old, start + 1, nextSibling)) - else if (v == null) removeNode(parent, o) - else updateNode(parent, o, v, hooks, getNextSibling(old, start + 1, nextSibling), ns) - } - if (old.length > commonLength) removeNodes(parent, old, start, old.length) - if (vnodes.length > commonLength) createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns) - } else { - // keyed diff - var oldEnd = old.length - 1, end = vnodes.length - 1, map, o, v, oe, ve, topSibling - - // bottom-up - while (oldEnd >= oldStart && end >= start) { - oe = old[oldEnd] - ve = vnodes[end] - if (oe.key !== ve.key) break - if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns) - if (ve.dom != null) nextSibling = ve.dom - oldEnd--, end-- - } - // top-down - while (oldEnd >= oldStart && end >= start) { - o = old[oldStart] - v = vnodes[start] - if (o.key !== v.key) break - oldStart++, start++ - if (o !== v) updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), ns) - } - // swaps and list reversals - while (oldEnd >= oldStart && end >= start) { - if (start === end) break - if (o.key !== ve.key || oe.key !== v.key) break - topSibling = getNextSibling(old, oldStart, nextSibling) - moveDOM(parent, oe, topSibling) - if (oe !== v) updateNode(parent, oe, v, hooks, topSibling, ns) - if (++start <= --end) moveDOM(parent, o, nextSibling) - if (o !== ve) updateNode(parent, o, ve, hooks, nextSibling, ns) - if (ve.dom != null) nextSibling = ve.dom - oldStart++; oldEnd-- - oe = old[oldEnd] - ve = vnodes[end] - o = old[oldStart] - v = vnodes[start] - } - // bottom up once again - while (oldEnd >= oldStart && end >= start) { - if (oe.key !== ve.key) break - if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns) - if (ve.dom != null) nextSibling = ve.dom - oldEnd--, end-- - oe = old[oldEnd] - ve = vnodes[end] - } - if (start > end) removeNodes(parent, old, oldStart, oldEnd + 1) - else if (oldStart > oldEnd) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns) - else { - // inspired by ivi https://github.com/ivijs/ivi/ by Boris Kaul - var originalNextSibling = nextSibling, vnodesLength = end - start + 1, oldIndices = new Array(vnodesLength), li=0, i=0, pos = 2147483647, matched = 0, map, lisIndices - for (i = 0; i < vnodesLength; i++) oldIndices[i] = -1 - for (i = end; i >= start; i--) { - if (map == null) map = getKeyMap(old, oldStart, oldEnd + 1) - ve = vnodes[i] - var oldIndex = map[ve.key] - if (oldIndex != null) { - pos = (oldIndex < pos) ? oldIndex : -1 // becomes -1 if nodes were re-ordered - oldIndices[i-start] = oldIndex - oe = old[oldIndex] - old[oldIndex] = null - if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns) - if (ve.dom != null) nextSibling = ve.dom - matched++ - } - } - nextSibling = originalNextSibling - if (matched !== oldEnd - oldStart + 1) removeNodes(parent, old, oldStart, oldEnd + 1) - if (matched === 0) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns) - else { - if (pos === -1) { - // the indices of the indices of the items that are part of the - // longest increasing subsequence in the oldIndices list - lisIndices = makeLisIndices(oldIndices) - li = lisIndices.length - 1 - for (i = end; i >= start; i--) { - v = vnodes[i] - if (oldIndices[i-start] === -1) createNode(parent, v, hooks, ns, nextSibling) - else { - if (lisIndices[li] === i - start) li-- - else moveDOM(parent, v, nextSibling) - } - if (v.dom != null) nextSibling = vnodes[i].dom - } - } else { - for (i = end; i >= start; i--) { - v = vnodes[i] - if (oldIndices[i-start] === -1) createNode(parent, v, hooks, ns, nextSibling) - if (v.dom != null) nextSibling = vnodes[i].dom - } - } - } - } - } - } - } - function updateNode(parent, old, vnode, hooks, nextSibling, ns) { - var oldTag = old.tag, tag = vnode.tag - if (oldTag === tag) { - vnode.state = old.state - vnode.events = old.events - if (shouldNotUpdate(vnode, old)) return - if (typeof oldTag === "string") { - if (vnode.attrs != null) { - updateLifecycle(vnode.attrs, vnode, hooks) - } - switch (oldTag) { - case "#": updateText(old, vnode); break - case "<": updateHTML(parent, old, vnode, ns, nextSibling); break - case "[": updateFragment(parent, old, vnode, hooks, nextSibling, ns); break - default: updateElement(old, vnode, hooks, ns) - } - } - else updateComponent(parent, old, vnode, hooks, nextSibling, ns) - } - else { - removeNode(parent, old) - createNode(parent, vnode, hooks, ns, nextSibling) - } - } - function updateText(old, vnode) { - if (old.children.toString() !== vnode.children.toString()) { - old.dom.nodeValue = vnode.children - } - vnode.dom = old.dom - } - function updateHTML(parent, old, vnode, ns, nextSibling) { - if (old.children !== vnode.children) { - removeDOM(parent, old, undefined) - createHTML(parent, vnode, ns, nextSibling) - } - else { - vnode.dom = old.dom - vnode.domSize = old.domSize - } - } - function updateFragment(parent, old, vnode, hooks, nextSibling, ns) { - updateNodes(parent, old.children, vnode.children, hooks, nextSibling, ns) - var domSize = 0, children = vnode.children - vnode.dom = null - if (children != null) { - for (var i = 0; i < children.length; i++) { - var child = children[i] - if (child != null && child.dom != null) { - if (vnode.dom == null) vnode.dom = child.dom - domSize += child.domSize || 1 - } - } - if (domSize !== 1) vnode.domSize = domSize - } - } - function updateElement(old, vnode, hooks, ns) { - var element = vnode.dom = old.dom - ns = getNameSpace(vnode) || ns - - updateAttrs(vnode, old.attrs, vnode.attrs, ns) - if (!maybeSetContentEditable(vnode)) { - updateNodes(element, old.children, vnode.children, hooks, null, ns) - } - } - function updateComponent(parent, old, vnode, hooks, nextSibling, ns) { - vnode.instance = Vnode.normalize(callHook.call(vnode.state.view, vnode)) - if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") - updateLifecycle(vnode.state, vnode, hooks) - if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks) - if (vnode.instance != null) { - if (old.instance == null) createNode(parent, vnode.instance, hooks, ns, nextSibling) - else updateNode(parent, old.instance, vnode.instance, hooks, nextSibling, ns) - vnode.dom = vnode.instance.dom - vnode.domSize = vnode.instance.domSize - } - else if (old.instance != null) { - removeNode(parent, old.instance) - vnode.dom = undefined - vnode.domSize = 0 - } - else { - vnode.dom = old.dom - vnode.domSize = old.domSize - } - } - function getKeyMap(vnodes, start, end) { - var map = Object.create(null) - for (; start < end; start++) { - var vnode = vnodes[start] - if (vnode != null) { - var key = vnode.key - if (key != null) map[key] = start - } - } - return map - } - // Lifted from ivi https://github.com/ivijs/ivi/ - // takes a list of unique numbers (-1 is special and can - // occur multiple times) and returns an array with the indices - // of the items that are part of the longest increasing - // subsequence - var lisTemp = [] - function makeLisIndices(a) { - var result = [0] - var u = 0, v = 0, i = 0 - var il = lisTemp.length = a.length - for (var i = 0; i < il; i++) lisTemp[i] = a[i] - for (var i = 0; i < il; ++i) { - if (a[i] === -1) continue - var j = result[result.length - 1] - if (a[j] < a[i]) { - lisTemp[i] = j - result.push(i) - continue - } - u = 0 - v = result.length - 1 - while (u < v) { - // Fast integer average without overflow. - // eslint-disable-next-line no-bitwise - var c = (u >>> 1) + (v >>> 1) + (u & v & 1) - if (a[result[c]] < a[i]) { - u = c + 1 - } - else { - v = c - } - } - if (a[i] < a[result[u]]) { - if (u > 0) lisTemp[i] = result[u - 1] - result[u] = i - } - } - u = result.length - v = result[u - 1] - while (u-- > 0) { - result[u] = v - v = lisTemp[v] - } - lisTemp.length = 0 - return result - } - - function getNextSibling(vnodes, i, nextSibling) { - for (; i < vnodes.length; i++) { - if (vnodes[i] != null && vnodes[i].dom != null) return vnodes[i].dom - } - return nextSibling - } - - // This handles fragments with zombie children (removed from vdom, but persisted in DOM through onbeforeremove) - function moveDOM(parent, vnode, nextSibling) { - if (vnode.dom != null) { - var target - if (vnode.domSize == null) { - // don't allocate for the common case - target = vnode.dom - } else { - target = getDocument(parent).createDocumentFragment() - for (var dom of domFor(vnode)) target.appendChild(dom) - } - insertDOM(parent, target, nextSibling) - } - } - - function insertDOM(parent, dom, nextSibling) { - if (nextSibling != null) parent.insertBefore(dom, nextSibling) - else parent.appendChild(dom) - } - - function maybeSetContentEditable(vnode) { - if (vnode.attrs == null || ( - vnode.attrs.contenteditable == null && // attribute - vnode.attrs.contentEditable == null // property - )) return false - var children = vnode.children - if (children != null && children.length === 1 && children[0].tag === "<") { - var content = children[0].children - if (vnode.dom.innerHTML !== content) vnode.dom.innerHTML = content - } - else if (children != null && children.length !== 0) throw new Error("Child node of a contenteditable must be trusted.") - return true - } - - //remove - function removeNodes(parent, vnodes, start, end) { - for (var i = start; i < end; i++) { - var vnode = vnodes[i] - if (vnode != null) removeNode(parent, vnode) - } - } - function removeNode(parent, vnode) { - var mask = 0 - var original = vnode.state - var stateResult, attrsResult - if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeremove === "function") { - var result = callHook.call(vnode.state.onbeforeremove, vnode) - if (result != null && typeof result.then === "function") { - mask = 1 - stateResult = result - } - } - if (vnode.attrs && typeof vnode.attrs.onbeforeremove === "function") { - var result = callHook.call(vnode.attrs.onbeforeremove, vnode) - if (result != null && typeof result.then === "function") { - // eslint-disable-next-line no-bitwise - mask |= 2 - attrsResult = result - } - } - checkState(vnode, original) - var generation - // If we can, try to fast-path it and avoid all the overhead of awaiting - if (!mask) { - onremove(vnode) - removeDOM(parent, vnode, generation) - } else { - generation = currentRender - for (var dom of domFor(vnode)) delayedRemoval.set(dom, generation) - if (stateResult != null) { - stateResult.finally(function () { - // eslint-disable-next-line no-bitwise - if (mask & 1) { - // eslint-disable-next-line no-bitwise - mask &= 2 - if (!mask) { - checkState(vnode, original) - onremove(vnode) - removeDOM(parent, vnode, generation) - } - } - }) - } - if (attrsResult != null) { - attrsResult.finally(function () { - // eslint-disable-next-line no-bitwise - if (mask & 2) { - // eslint-disable-next-line no-bitwise - mask &= 1 - if (!mask) { - checkState(vnode, original) - onremove(vnode) - removeDOM(parent, vnode, generation) - } - } - }) - } - } - } - function removeDOM(parent, vnode, generation) { - if (vnode.dom == null) return - if (vnode.domSize == null) { - // don't allocate for the common case - if (delayedRemoval.get(vnode.dom) === generation) parent.removeChild(vnode.dom) - } else { - for (var dom of domFor(vnode, {generation})) parent.removeChild(dom) - } - } - - function onremove(vnode) { - if (typeof vnode.tag !== "string" && typeof vnode.state.onremove === "function") callHook.call(vnode.state.onremove, vnode) - if (vnode.attrs && typeof vnode.attrs.onremove === "function") callHook.call(vnode.attrs.onremove, vnode) - if (typeof vnode.tag !== "string") { - if (vnode.instance != null) onremove(vnode.instance) - } else { - var children = vnode.children - if (Array.isArray(children)) { - for (var i = 0; i < children.length; i++) { - var child = children[i] - if (child != null) onremove(child) - } - } - } - } - - //attrs - function setAttrs(vnode, attrs, ns) { - for (var key in attrs) { - setAttr(vnode, key, null, attrs[key], ns) - } - } - function setAttr(vnode, key, old, value, ns) { - if (key === "key" || key === "is" || value == null || isLifecycleMethod(key) || (old === value && !isFormAttribute(vnode, key)) && typeof value !== "object") return - if (key[0] === "o" && key[1] === "n") return updateEvent(vnode, key, value) - if (key.slice(0, 6) === "xlink:") vnode.dom.setAttributeNS("http://www.w3.org/1999/xlink", key.slice(6), value) - else if (key === "style") updateStyle(vnode.dom, old, value) - else if (hasPropertyKey(vnode, key, ns)) { - if (key === "value") { - // Only do the coercion if we're actually going to check the value. - /* eslint-disable no-implicit-coercion */ - var isFileInput = vnode.tag === "input" && vnode.attrs.type === "file" - //setting input[value] to same value by typing on focused element moves cursor to end in Chrome - //setting input[type=file][value] to same value causes an error to be generated if it's non-empty - if ((vnode.tag === "input" || vnode.tag === "textarea") && vnode.dom.value === "" + value && (isFileInput || vnode.dom === activeElement(vnode.dom))) return - //setting select[value] to same value while having select open blinks select dropdown in Chrome - if (vnode.tag === "select" && old !== null && vnode.dom.value === "" + value) return - //setting option[value] to same value while having select open blinks select dropdown in Chrome - if (vnode.tag === "option" && old !== null && vnode.dom.value === "" + value) return - //setting input[type=file][value] to different value is an error if it's non-empty - // Not ideal, but it at least works around the most common source of uncaught exceptions for now. - if (isFileInput && "" + value !== "") { console.error("`value` is read-only on file inputs!"); return } - /* eslint-enable no-implicit-coercion */ - } - // If you assign an input type that is not supported by IE 11 with an assignment expression, an error will occur. - if (vnode.tag === "input" && key === "type") vnode.dom.setAttribute(key, value) - else vnode.dom[key] = value - } else { - if (typeof value === "boolean") { - if (value) vnode.dom.setAttribute(key, "") - else vnode.dom.removeAttribute(key) - } - else vnode.dom.setAttribute(key === "className" ? "class" : key, value) - } - } - function removeAttr(vnode, key, old, ns) { - if (key === "key" || key === "is" || old == null || isLifecycleMethod(key)) return - if (key[0] === "o" && key[1] === "n") updateEvent(vnode, key, undefined) - else if (key === "style") updateStyle(vnode.dom, old, null) - else if ( - hasPropertyKey(vnode, key, ns) - && key !== "className" - && key !== "title" // creates "null" as title - && !(key === "value" && ( - vnode.tag === "option" - || vnode.tag === "select" && vnode.dom.selectedIndex === -1 && vnode.dom === activeElement(vnode.dom) - )) - && !(vnode.tag === "input" && key === "type") - ) { - vnode.dom[key] = null - } else { - var nsLastIndex = key.indexOf(":") - if (nsLastIndex !== -1) key = key.slice(nsLastIndex + 1) - if (old !== false) vnode.dom.removeAttribute(key === "className" ? "class" : key) - } - } - function setLateSelectAttrs(vnode, attrs) { - if ("value" in attrs) { - if(attrs.value === null) { - if (vnode.dom.selectedIndex !== -1) vnode.dom.value = null - } else { - var normalized = "" + attrs.value // eslint-disable-line no-implicit-coercion - if (vnode.dom.value !== normalized || vnode.dom.selectedIndex === -1) { - vnode.dom.value = normalized - } - } - } - if ("selectedIndex" in attrs) setAttr(vnode, "selectedIndex", null, attrs.selectedIndex, undefined) - } - function updateAttrs(vnode, old, attrs, ns) { - if (old && old === attrs) { - console.warn("Don't reuse attrs object, use new object for every redraw, this will throw in next major") - } - if (attrs != null) { - for (var key in attrs) { - setAttr(vnode, key, old && old[key], attrs[key], ns) - } - } - var val - if (old != null) { - for (var key in old) { - if (((val = old[key]) != null) && (attrs == null || attrs[key] == null)) { - removeAttr(vnode, key, val, ns) - } - } - } - } - function isFormAttribute(vnode, attr) { - return attr === "value" || attr === "checked" || attr === "selectedIndex" || attr === "selected" && vnode.dom === activeElement(vnode.dom) || vnode.tag === "option" && vnode.dom.parentNode === activeElement(vnode.dom) - } - function isLifecycleMethod(attr) { - return attr === "oninit" || attr === "oncreate" || attr === "onupdate" || attr === "onremove" || attr === "onbeforeremove" || attr === "onbeforeupdate" - } - function hasPropertyKey(vnode, key, ns) { - // Filter out namespaced keys - return ns === undefined && ( - // If it's a custom element, just keep it. - vnode.tag.indexOf("-") > -1 || vnode.attrs != null && vnode.attrs.is || - // If it's a normal element, let's try to avoid a few browser bugs. - key !== "href" && key !== "list" && key !== "form" && key !== "width" && key !== "height"// && key !== "type" - // Defer the property check until *after* we check everything. - ) && key in vnode.dom - } - - //style - var uppercaseRegex = /[A-Z]/g - function toLowerCase(capital) { return "-" + capital.toLowerCase() } - function normalizeKey(key) { - return key[0] === "-" && key[1] === "-" ? key : - key === "cssFloat" ? "float" : - key.replace(uppercaseRegex, toLowerCase) - } - function updateStyle(element, old, style) { - if (old === style) { - // Styles are equivalent, do nothing. - } else if (style == null) { - // New style is missing, just clear it. - element.style = "" - } else if (typeof style !== "object") { - // New style is a string, let engine deal with patching. - element.style = style - } else if (old == null || typeof old !== "object") { - // `old` is missing or a string, `style` is an object. - element.style.cssText = "" - // Add new style properties - for (var key in style) { - var value = style[key] - if (value != null) element.style.setProperty(normalizeKey(key), String(value)) - } - } else { - // Both old & new are (different) objects. - // Update style properties that have changed - for (var key in style) { - var value = style[key] - if (value != null && (value = String(value)) !== String(old[key])) { - element.style.setProperty(normalizeKey(key), value) - } - } - // Remove style properties that no longer exist - for (var key in old) { - if (old[key] != null && style[key] == null) { - element.style.removeProperty(normalizeKey(key)) - } - } - } - } - - // Here's an explanation of how this works: - // 1. The event names are always (by design) prefixed by `on`. - // 2. The EventListener interface accepts either a function or an object - // with a `handleEvent` method. - // 3. The object does not inherit from `Object.prototype`, to avoid - // any potential interference with that (e.g. setters). - // 4. The event name is remapped to the handler before calling it. - // 5. In function-based event handlers, `ev.target === this`. We replicate - // that below. - // 6. In function-based event handlers, `return false` prevents the default - // action and stops event propagation. We replicate that below. - function EventDict() { - // Save this, so the current redraw is correctly tracked. - this._ = currentRedraw - } - EventDict.prototype = Object.create(null) - EventDict.prototype.handleEvent = function (ev) { - var handler = this["on" + ev.type] - var result - if (typeof handler === "function") result = handler.call(ev.currentTarget, ev) - else if (typeof handler.handleEvent === "function") handler.handleEvent(ev) - if (this._ && ev.redraw !== false) (0, this._)() - if (result === false) { - ev.preventDefault() - ev.stopPropagation() - } - } - - //event - function updateEvent(vnode, key, value) { - if (vnode.events != null) { - vnode.events._ = currentRedraw - if (vnode.events[key] === value) return - if (value != null && (typeof value === "function" || typeof value === "object")) { - if (vnode.events[key] == null) vnode.dom.addEventListener(key.slice(2), vnode.events, false) - vnode.events[key] = value - } else { - if (vnode.events[key] != null) vnode.dom.removeEventListener(key.slice(2), vnode.events, false) - vnode.events[key] = undefined - } - } else if (value != null && (typeof value === "function" || typeof value === "object")) { - vnode.events = new EventDict() - vnode.dom.addEventListener(key.slice(2), vnode.events, false) - vnode.events[key] = value - } - } - - //lifecycle - function initLifecycle(source, vnode, hooks) { - if (typeof source.oninit === "function") callHook.call(source.oninit, vnode) - if (typeof source.oncreate === "function") hooks.push(callHook.bind(source.oncreate, vnode)) - } - function updateLifecycle(source, vnode, hooks) { - if (typeof source.onupdate === "function") hooks.push(callHook.bind(source.onupdate, vnode)) - } - function shouldNotUpdate(vnode, old) { - do { - if (vnode.attrs != null && typeof vnode.attrs.onbeforeupdate === "function") { - var force = callHook.call(vnode.attrs.onbeforeupdate, vnode, old) - if (force !== undefined && !force) break - } - if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeupdate === "function") { - var force = callHook.call(vnode.state.onbeforeupdate, vnode, old) - if (force !== undefined && !force) break - } - return false - } while (false); // eslint-disable-line no-constant-condition - vnode.dom = old.dom - vnode.domSize = old.domSize - vnode.instance = old.instance - // One would think having the actual latest attributes would be ideal, - // but it doesn't let us properly diff based on our current internal - // representation. We have to save not only the old DOM info, but also - // the attributes used to create it, as we diff *that*, not against the - // DOM directly (with a few exceptions in `setAttr`). And, of course, we - // need to save the children and text as they are conceptually not - // unlike special "attributes" internally. - vnode.attrs = old.attrs - vnode.children = old.children - vnode.text = old.text - return true - } - - var currentDOM - - return function(dom, vnodes, redraw) { - if (!dom) throw new TypeError("DOM element being rendered to does not exist.") - if (currentDOM != null && dom.contains(currentDOM)) { - throw new TypeError("Node is currently being rendered to and thus is locked.") - } - var prevRedraw = currentRedraw - var prevDOM = currentDOM - var hooks = [] - var active = activeElement(dom) - var namespace = dom.namespaceURI - - currentDOM = dom - currentRedraw = typeof redraw === "function" ? redraw : undefined - currentRender = {} - try { - // First time rendering into a node clears it out - if (dom.vnodes == null) dom.textContent = "" - vnodes = Vnode.normalizeChildren(Array.isArray(vnodes) ? vnodes : [vnodes]) - updateNodes(dom, dom.vnodes, vnodes, hooks, null, namespace === "http://www.w3.org/1999/xhtml" ? undefined : namespace) - dom.vnodes = vnodes - // `document.activeElement` can return null: https://html.spec.whatwg.org/multipage/interaction.html#dom-document-activeelement - if (active != null && activeElement(dom) !== active && typeof active.focus === "function") active.focus() - for (var i = 0; i < hooks.length; i++) hooks[i]() - } finally { - currentRedraw = prevRedraw - currentDOM = prevDOM - } - } -} diff --git a/render/tests/manual/iframe.html b/render/tests/manual/iframe.html deleted file mode 100644 index 01cc505d6..000000000 --- a/render/tests/manual/iframe.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - -
- - - - diff --git a/render/tests/test-component.js b/render/tests/test-component.js deleted file mode 100644 index 74b1c87a1..000000000 --- a/render/tests/test-component.js +++ /dev/null @@ -1,943 +0,0 @@ -"use strict" - -var o = require("ospec") -var components = require("../../test-utils/components") -var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") -var m = require("../../render/hyperscript") - -o.spec("component", function() { - var $window, root, render - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - - render = vdom($window) - }) - - components.forEach(function(cmp){ - o.spec(cmp.kind, function(){ - var createComponent = cmp.create - - o.spec("basics", function() { - o("works", function() { - var component = createComponent({ - view: function() { - return m("div", {id: "a"}, "b") - } - }) - var node = m(component) - - render(root, node) - - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].value).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") - }) - o("receives arguments", function() { - var component = createComponent({ - view: function(vnode) { - return m("div", vnode.attrs, vnode.children) - } - }) - var node = m(component, {id: "a"}, "b") - - render(root, node) - - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].value).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") - }) - o("updates", function() { - var component = createComponent({ - view: function(vnode) { - return m("div", vnode.attrs, vnode.children) - } - }) - render(root, [m(component, {id: "a"}, "b")]) - render(root, [m(component, {id: "c"}, "d")]) - - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].value).equals("c") - o(root.firstChild.firstChild.nodeValue).equals("d") - }) - o("updates root from null", function() { - var visible = false - var component = createComponent({ - view: function() { - return visible ? m("div") : null - } - }) - render(root, m(component)) - visible = true - render(root, m(component)) - - o(root.firstChild.nodeName).equals("DIV") - }) - o("updates root from primitive", function() { - var visible = false - var component = createComponent({ - view: function() { - return visible ? m("div") : false - } - }) - render(root, m(component)) - visible = true - render(root, m(component)) - - o(root.firstChild.nodeName).equals("DIV") - }) - o("updates root to null", function() { - var visible = true - var component = createComponent({ - view: function() { - return visible ? m("div") : null - } - }) - render(root, m(component)) - visible = false - render(root, m(component)) - - o(root.childNodes.length).equals(0) - }) - o("updates root to primitive", function() { - var visible = true - var component = createComponent({ - view: function() { - return visible ? m("div") : false - } - }) - render(root, m(component)) - visible = false - render(root, m(component)) - - o(root.childNodes.length).equals(0) - }) - o("updates root from null to null", function() { - var component = createComponent({ - view: function() { - return null - } - }) - render(root, m(component)) - render(root, m(component)) - - o(root.childNodes.length).equals(0) - }) - o("removes", function() { - var component = createComponent({ - view: function() { - return m("div") - } - }) - var div = m("div", {key: 2}) - render(root, [m(component, {key: 1}), div]) - render(root, div) - - o(root.childNodes.length).equals(1) - o(root.firstChild).equals(div.dom) - }) - o("svg works when creating across component boundary", function() { - var component = createComponent({ - view: function() { - return m("g") - } - }) - render(root, m("svg", m(component))) - - o(root.firstChild.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") - }) - o("svg works when updating across component boundary", function() { - var component = createComponent({ - view: function() { - return m("g") - } - }) - render(root, m("svg", m(component))) - render(root, m("svg", m(component))) - - o(root.firstChild.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") - }) - }) - o.spec("return value", function() { - o("can return fragments", function() { - var component = createComponent({ - view: function() { - return [ - m("label"), - m("input"), - ] - } - }) - render(root, m(component)) - - o(root.childNodes.length).equals(2) - o(root.childNodes[0].nodeName).equals("LABEL") - o(root.childNodes[1].nodeName).equals("INPUT") - }) - o("can return string", function() { - var component = createComponent({ - view: function() { - return "a" - } - }) - render(root, m(component)) - - o(root.firstChild.nodeType).equals(3) - o(root.firstChild.nodeValue).equals("a") - }) - o("can return falsy string", function() { - var component = createComponent({ - view: function() { - return "" - } - }) - render(root, m(component)) - - o(root.firstChild.nodeType).equals(3) - o(root.firstChild.nodeValue).equals("") - }) - o("can return number", function() { - var component = createComponent({ - view: function() { - return 1 - } - }) - render(root, m(component)) - - o(root.firstChild.nodeType).equals(3) - o(root.firstChild.nodeValue).equals("1") - }) - o("can return falsy number", function() { - var component = createComponent({ - view: function() { - return 0 - } - }) - render(root, m(component)) - - o(root.firstChild.nodeType).equals(3) - o(root.firstChild.nodeValue).equals("0") - }) - o("can return `true`", function() { - var component = createComponent({ - view: function() { - return true - } - }) - render(root, m(component)) - - o(root.childNodes.length).equals(0) - }) - o("can return `false`", function() { - var component = createComponent({ - view: function() { - return false - } - }) - render(root, m(component)) - - o(root.childNodes.length).equals(0) - }) - o("can return null", function() { - var component = createComponent({ - view: function() { - return null - } - }) - render(root, m(component)) - - o(root.childNodes.length).equals(0) - }) - o("can return undefined", function() { - var component = createComponent({ - view: function() { - return undefined - } - }) - render(root, m(component)) - - o(root.childNodes.length).equals(0) - }) - o("throws a custom error if it returns itself when created", function() { - // A view that returns its vnode would otherwise trigger an infinite loop - var threw = false - var component = createComponent({ - view: function(vnode) { - return vnode - } - }) - try { - render(root, m(component)) - } - catch (e) { - threw = true - o(e instanceof Error).equals(true) - // Call stack exception is a RangeError - o(e instanceof RangeError).equals(false) - } - o(threw).equals(true) - }) - o("throws a custom error if it returns itself when updated", function() { - // A view that returns its vnode would otherwise trigger an infinite loop - var threw = false - var init = true - var oninit = o.spy() - var component = createComponent({ - oninit: oninit, - view: function(vnode) { - if (init) return init = false - else return vnode - } - }) - render(root, m(component)) - - o(root.childNodes.length).equals(0) - - try { - render(root, m(component)) - } - catch (e) { - threw = true - o(e instanceof Error).equals(true) - // Call stack exception is a RangeError - o(e instanceof RangeError).equals(false) - } - o(threw).equals(true) - o(oninit.callCount).equals(1) - }) - o("can update when returning fragments", function() { - var component = createComponent({ - view: function() { - return [ - m("label"), - m("input"), - ] - } - }) - render(root, m(component)) - render(root, m(component)) - - o(root.childNodes.length).equals(2) - o(root.childNodes[0].nodeName).equals("LABEL") - o(root.childNodes[1].nodeName).equals("INPUT") - }) - o("can update when returning primitive", function() { - var component = createComponent({ - view: function() { - return "a" - } - }) - render(root, m(component)) - render(root, m(component)) - - o(root.firstChild.nodeType).equals(3) - o(root.firstChild.nodeValue).equals("a") - }) - o("can update when returning null", function() { - var component = createComponent({ - view: function() { - return null - } - }) - render(root, m(component)) - render(root, m(component)) - - o(root.childNodes.length).equals(0) - }) - o("can remove when returning fragments", function() { - var component = createComponent({ - view: function() { - return [ - m("label"), - m("input"), - ] - } - }) - var div = m("div", {key: 2}) - render(root, [m(component, {key: 1}), div]) - - render(root, [m("div", {key: 2})]) - - o(root.childNodes.length).equals(1) - o(root.firstChild).equals(div.dom) - }) - o("can remove when returning primitive", function() { - var component = createComponent({ - view: function() { - return "a" - } - }) - var div = m("div", {key: 2}) - render(root, [m(component, {key: 1}), div]) - - render(root, [m("div", {key: 2})]) - - o(root.childNodes.length).equals(1) - o(root.firstChild).equals(div.dom) - }) - }) - o.spec("lifecycle", function() { - o("calls oninit", function() { - var called = 0 - var component = createComponent({ - oninit: function(vnode) { - called++ - - o(vnode.tag).equals(component) - o(vnode.dom).equals(undefined) - o(root.childNodes.length).equals(0) - }, - view: function() { - return m("div", {id: "a"}, "b") - } - }) - - render(root, m(component)) - - o(called).equals(1) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].value).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") - }) - o("calls oninit when returning fragment", function() { - var called = 0 - var component = createComponent({ - oninit: function(vnode) { - called++ - - o(vnode.tag).equals(component) - o(vnode.dom).equals(undefined) - o(root.childNodes.length).equals(0) - }, - view: function() { - return [m("div", {id: "a"}, "b")] - } - }) - - render(root, m(component)) - - o(called).equals(1) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].value).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") - }) - o("calls oninit before view", function() { - var viewCalled = false - var component = createComponent({ - view: function() { - viewCalled = true - return m("div", {id: "a"}, "b") - }, - oninit: function() { - o(viewCalled).equals(false) - }, - }) - - render(root, m(component)) - }) - o("does not calls oninit on redraw", function() { - var init = o.spy() - var component = createComponent({ - view: function() { - return m("div", {id: "a"}, "b") - }, - oninit: init, - }) - - function view() { - return m(component) - } - - render(root, view()) - render(root, view()) - - o(init.callCount).equals(1) - }) - o("calls oncreate", function() { - var called = 0 - var component = createComponent({ - oncreate: function(vnode) { - called++ - - o(vnode.dom).notEquals(undefined) - o(vnode.dom).equals(root.firstChild) - o(root.childNodes.length).equals(1) - }, - view: function() { - return m("div", {id: "a"}, "b") - } - }) - - render(root, m(component)) - - o(called).equals(1) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].value).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") - }) - o("does not calls oncreate on redraw", function() { - var create = o.spy() - var component = createComponent({ - view: function() { - return m("div", {id: "a"}, "b") - }, - oncreate: create, - }) - - function view() { - return m(component) - } - - render(root, view()) - render(root, view()) - - o(create.callCount).equals(1) - }) - o("calls oncreate when returning fragment", function() { - var called = 0 - var component = createComponent({ - oncreate: function(vnode) { - called++ - - o(vnode.dom).notEquals(undefined) - o(vnode.dom).equals(root.firstChild) - o(root.childNodes.length).equals(1) - }, - view: function() { - return m("div", {id: "a"}, "b") - } - }) - - render(root, m(component)) - - o(called).equals(1) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].value).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") - }) - o("calls onupdate", function() { - var called = 0 - var component = createComponent({ - onupdate: function(vnode) { - called++ - - o(vnode.dom).notEquals(undefined) - o(vnode.dom).equals(root.firstChild) - o(root.childNodes.length).equals(1) - }, - view: function() { - return m("div", {id: "a"}, "b") - } - }) - - render(root, m(component)) - - o(called).equals(0) - - render(root, m(component)) - - o(called).equals(1) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].value).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") - }) - o("calls onupdate when returning fragment", function() { - var called = 0 - var component = createComponent({ - onupdate: function(vnode) { - called++ - - o(vnode.dom).notEquals(undefined) - o(vnode.dom).equals(root.firstChild) - o(root.childNodes.length).equals(1) - }, - view: function() { - return [m("div", {id: "a"}, "b")] - } - }) - - render(root, m(component)) - - o(called).equals(0) - - render(root, m(component)) - - o(called).equals(1) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].value).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") - }) - o("calls onremove", function() { - var called = 0 - var component = createComponent({ - onremove: function(vnode) { - called++ - - o(vnode.dom).notEquals(undefined) - o(vnode.dom).equals(root.firstChild) - o(root.childNodes.length).equals(1) - }, - view: function() { - return m("div", {id: "a"}, "b") - } - }) - - render(root, m(component)) - - o(called).equals(0) - - render(root, []) - - o(called).equals(1) - o(root.childNodes.length).equals(0) - }) - o("calls onremove when returning fragment", function() { - var called = 0 - var component = createComponent({ - onremove: function(vnode) { - called++ - - o(vnode.dom).notEquals(undefined) - o(vnode.dom).equals(root.firstChild) - o(root.childNodes.length).equals(1) - }, - view: function() { - return [m("div", {id: "a"}, "b")] - } - }) - - render(root, m(component)) - - o(called).equals(0) - - render(root, []) - - o(called).equals(1) - o(root.childNodes.length).equals(0) - }) - o("calls onbeforeremove", function() { - var called = 0 - var component = createComponent({ - onbeforeremove: function(vnode) { - called++ - - o(vnode.dom).notEquals(undefined) - o(vnode.dom).equals(root.firstChild) - o(root.childNodes.length).equals(1) - }, - view: function() { - return m("div", {id: "a"}, "b") - } - }) - - render(root, m(component)) - - o(called).equals(0) - - render(root, []) - - o(called).equals(1) - o(root.childNodes.length).equals(0) - }) - o("calls onbeforeremove when returning fragment", function() { - var called = 0 - var component = createComponent({ - onbeforeremove: function(vnode) { - called++ - - o(vnode.dom).notEquals(undefined) - o(vnode.dom).equals(root.firstChild) - o(root.childNodes.length).equals(1) - }, - view: function() { - return [m("div", {id: "a"}, "b")] - } - }) - - render(root, m(component)) - - o(called).equals(0) - - render(root, []) - - o(called).equals(1) - o(root.childNodes.length).equals(0) - }) - o("does not recycle when there's an onupdate", function() { - var component = createComponent({ - onupdate: function() {}, - view: function() { - return m("div") - } - }) - var vnode = m(component, {key: 1}) - var updated = m(component, {key: 1}) - - render(root, vnode) - render(root, []) - render(root, updated) - - o(vnode.dom).notEquals(updated.dom) - }) - o("lifecycle timing megatest (for a single component)", function() { - var methods = { - view: o.spy(function() { - return "" - }) - } - var attrs = {} - var hooks = [ - "oninit", "oncreate", "onbeforeupdate", - "onupdate", "onbeforeremove", "onremove" - ] - hooks.forEach(function(hook) { - if (hook === "onbeforeupdate") { - // the component's `onbeforeupdate` is called after the `attrs`' one - attrs[hook] = o.spy(function() { - o(attrs[hook].callCount).equals(methods[hook].callCount + 1)(hook) - }) - methods[hook] = o.spy(function() { - o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) - }) - } else { - // the other component hooks are called before the `attrs` ones - methods[hook] = o.spy(function() { - o(attrs[hook].callCount).equals(methods[hook].callCount - 1)(hook) - }) - attrs[hook] = o.spy(function() { - o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) - }) - } - }) - - 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, [m(component, 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, [m(component, 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) { - o(this).equals(vnode.state) - return "" - }) - } - var attrs = {} - var hooks = [ - "oninit", "oncreate", "onbeforeupdate", - "onupdate", "onbeforeremove", "onremove" - ] - hooks.forEach(function(hook) { - attrs[hook] = o.spy(function(vnode){ - o(this).equals(vnode.state)(hook) - }) - methods[hook] = o.spy(function(vnode){ - o(this).equals(vnode.state) - }) - }) - - var component = createComponent(methods) - - render(root, [m(component, attrs)]) - render(root, [m(component, attrs)]) - render(root, []) - - hooks.forEach(function(hook) { - o(attrs[hook].this).equals(methods.view.this)(hook) - o(methods[hook].this).equals(methods.view.this)(hook) - }) - - o(methods.view.args.length).equals(1) - o(methods.oninit.args.length).equals(1) - o(methods.oncreate.args.length).equals(1) - o(methods.onbeforeupdate.args.length).equals(2) - o(methods.onupdate.args.length).equals(1) - o(methods.onbeforeremove.args.length).equals(1) - o(methods.onremove.args.length).equals(1) - - hooks.forEach(function(hook) { - o(methods[hook].args.length).equals(attrs[hook].args.length)(hook) - }) - }) - o("no recycling occurs (was: recycled components get a fresh state)", function() { - var step = 0 - var firstState - var view = o.spy(function(vnode) { - if (step === 0) { - firstState = vnode.state - } else { - o(vnode.state).notEquals(firstState) - } - return m("div") - }) - var component = createComponent({view: view}) - - render(root, [m("div", m(component, {key: 1}))]) - var child = root.firstChild.firstChild - render(root, []) - step = 1 - render(root, [m("div", m(component, {key: 1}))]) - - o(child).notEquals(root.firstChild.firstChild) // this used to be a recycling pool test - o(view.callCount).equals(2) - }) - }) - o.spec("state", function() { - o("initializes state", function() { - var data = {a: 1} - var component = createComponent(createComponent({ - data: data, - oninit: init, - view: function() { - return "" - } - })) - - render(root, m(component)) - - function init(vnode) { - o(vnode.state.data).equals(data) - } - }) - o("state proxies to the component object/prototype", function() { - var body = {a: 1} - var data = [body] - var component = createComponent(createComponent({ - data: data, - oninit: init, - view: function() { - return "" - } - })) - - render(root, m(component)) - - function init(vnode) { - o(vnode.state.data).equals(data) - o(vnode.state.data[0]).equals(body) - } - }) - }) - }) - }) - o.spec("Tests specific to certain component kinds", function() { - o.spec("state", function() { - o("POJO", function() { - var data = {} - var component = { - data: data, - oninit: init, - view: function() { - return "" - } - } - - render(root, m(component)) - - function init(vnode) { - o(vnode.state.data).equals(data) - - //inherits state via prototype - component.x = 1 - o(vnode.state.x).equals(1) - } - }) - o("Constructible", function() { - var oninit = o.spy() - var component = o.spy(function(vnode){ - o(vnode.state).equals(undefined) - o(oninit.callCount).equals(0) - }) - var view = o.spy(function(){ - o(this instanceof component).equals(true) - return "" - }) - component.prototype.view = view - component.prototype.oninit = oninit - - render(root, [m(component, {oninit: oninit})]) - render(root, [m(component, {oninit: oninit})]) - render(root, []) - - o(component.callCount).equals(1) - o(oninit.callCount).equals(2) - o(view.callCount).equals(2) - }) - o("Closure", function() { - var state - var oninit = o.spy() - var view = o.spy(function() { - o(this).equals(state) - return "" - }) - var component = o.spy(function(vnode) { - o(vnode.state).equals(undefined) - o(oninit.callCount).equals(0) - return state = { - view: view - } - }) - - render(root, [m(component, {oninit: oninit})]) - render(root, [m(component, {oninit: oninit})]) - render(root, []) - - o(component.callCount).equals(1) - o(oninit.callCount).equals(1) - o(view.callCount).equals(2) - }) - }) - }) -}) diff --git a/render/tests/test-createElement.js b/render/tests/test-createElement.js deleted file mode 100644 index 2eecc90fb..000000000 --- a/render/tests/test-createElement.js +++ /dev/null @@ -1,110 +0,0 @@ -"use strict" - -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") -var m = require("../../render/hyperscript") - -o.spec("createElement", function() { - var $window, root, render - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - render = vdom($window) - }) - - o("creates element", function() { - var vnode = m("div") - render(root, vnode) - - o(vnode.dom.nodeName).equals("DIV") - }) - o("creates attr", function() { - var vnode = m("div", {id: "a", title: "b"}) - render(root, vnode) - - o(vnode.dom.nodeName).equals("DIV") - o(vnode.dom.attributes["id"].value).equals("a") - o(vnode.dom.attributes["title"].value).equals("b") - }) - o("creates style", function() { - var vnode = m("div", {style: {backgroundColor: "red"}}) - render(root, vnode) - - o(vnode.dom.nodeName).equals("DIV") - o(vnode.dom.style.backgroundColor).equals("red") - }) - o("allows css vars in style", function() { - var vnode = m("div", {style: {"--css-var": "red"}}) - render(root, vnode) - - o(vnode.dom.style["--css-var"]).equals("red") - }) - o("allows css vars in style with uppercase letters", function() { - var vnode = m("div", {style: {"--cssVar": "red"}}) - render(root, vnode) - - o(vnode.dom.style["--cssVar"]).equals("red") - }) - o("censors cssFloat to float", function() { - var vnode = m("a", {style: {cssFloat: "left"}}) - - render(root, vnode) - - o(vnode.dom.style.float).equals("left") - }) - o("creates children", function() { - var vnode = m("div", m("a"), m("b")) - render(root, vnode) - - o(vnode.dom.nodeName).equals("DIV") - o(vnode.dom.childNodes.length).equals(2) - o(vnode.dom.childNodes[0].nodeName).equals("A") - o(vnode.dom.childNodes[1].nodeName).equals("B") - }) - o("creates attrs and children", function() { - var vnode = m("div", {id: "a", title: "b"}, m("a"), m("b")) - render(root, vnode) - - o(vnode.dom.nodeName).equals("DIV") - o(vnode.dom.attributes["id"].value).equals("a") - o(vnode.dom.attributes["title"].value).equals("b") - o(vnode.dom.childNodes.length).equals(2) - 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 = m("svg", - m("a", {"xlink:href": "javascript:;"}), - m("foreignObject", m("body", {xmlns: "http://www.w3.org/1999/xhtml"})) - ) - render(root, vnode) - - o(vnode.dom.nodeName).equals("svg") - o(vnode.dom.namespaceURI).equals("http://www.w3.org/2000/svg") - o(vnode.dom.firstChild.nodeName).equals("a") - o(vnode.dom.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") - o(vnode.dom.firstChild.attributes["href"].value).equals("javascript:;") - o(vnode.dom.firstChild.attributes["href"].namespaceURI).equals("http://www.w3.org/1999/xlink") - o(vnode.dom.childNodes[1].nodeName).equals("foreignObject") - 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 = m("svg", {viewBox: "0 0 100 100"}) - render(root, vnode) - - o(vnode.dom.attributes["viewBox"].value).equals("0 0 100 100") - }) - o("creates mathml", function() { - var vnode = m("math", m("mrow")) - render(root, vnode) - - o(vnode.dom.nodeName).equals("math") - o(vnode.dom.namespaceURI).equals("http://www.w3.org/1998/Math/MathML") - o(vnode.dom.firstChild.nodeName).equals("mrow") - o(vnode.dom.firstChild.namespaceURI).equals("http://www.w3.org/1998/Math/MathML") - }) -}) diff --git a/render/tests/test-createFragment.js b/render/tests/test-createFragment.js deleted file mode 100644 index 01f7de150..000000000 --- a/render/tests/test-createFragment.js +++ /dev/null @@ -1,52 +0,0 @@ -"use strict" - -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") -var m = require("../../render/hyperscript") -var fragment = require("../../render/fragment") - -o.spec("createFragment", function() { - var $window, root, render - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - render = vdom($window) - }) - - o("creates fragment", function() { - var vnode = fragment(m("a")) - render(root, vnode) - - o(vnode.dom.nodeName).equals("A") - }) - o("handles empty fragment", function() { - var vnode = fragment() - render(root, vnode) - - o(vnode.dom).equals(null) - o(vnode.domSize).equals(0) - }) - o("handles childless fragment", function() { - var vnode = fragment() - render(root, vnode) - - o(vnode.dom).equals(null) - o(vnode.domSize).equals(0) - }) - o("handles multiple children", function() { - var vnode = fragment(m("a"), m("b")) - render(root, vnode) - - o(vnode.domSize).equals(2) - o(vnode.dom.nodeName).equals("A") - o(vnode.dom.nextSibling.nodeName).equals("B") - }) - o("handles td", function() { - var vnode = fragment(m("td")) - render(root, vnode) - - o(vnode.dom).notEquals(null) - o(vnode.dom.nodeName).equals("TD") - }) -}) diff --git a/render/tests/test-createHTML.js b/render/tests/test-createHTML.js deleted file mode 100644 index ddfd499d7..000000000 --- a/render/tests/test-createHTML.js +++ /dev/null @@ -1,96 +0,0 @@ -"use strict" - -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") -var m = require("../../render/hyperscript") -var trust = require("../../render/trust") - -o.spec("createHTML", function() { - var $window, root, render - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - render = vdom($window) - }) - - o("creates HTML", function() { - var vnode = trust("") - render(root, vnode) - - o(vnode.dom.nodeName).equals("A") - }) - o("creates text HTML", function() { - var vnode = trust("a") - render(root, vnode) - - o(vnode.dom.nodeValue).equals("a") - }) - o("handles empty HTML", function() { - var vnode = trust("") - render(root, vnode) - - o(vnode.dom).equals(null) - o(vnode.domSize).equals(0) - }) - o("handles multiple children in HTML", function() { - var vnode = trust("") - render(root, vnode) - - o(vnode.domSize).equals(2) - o(vnode.dom.nodeName).equals("A") - o(vnode.dom.nextSibling.nodeName).equals("B") - }) - o("handles valid html tags", function() { - //FIXME body,head,html,frame,frameset are not supported - //FIXME keygen is broken in Firefox - var tags = ["a", "abbr", "acronym", "address", "applet", "area", "article", "aside", "audio", "b", "base", "basefont", "bdi", "bdo", "big", "blockquote", /*"body",*/ "br", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "datalist", "dd", "del", "details", "dfn", "dialog", "dir", "div", "dl", "dt", "em", "embed", "fieldset", "figcaption", "figure", "font", "footer", "form", /*"frame", "frameset",*/ "h1", "h2", "h3", "h4", "h5", "h6", /*"head",*/ "header", "hr", /*"html",*/ "i", "iframe", "img", "input", "ins", "kbd", /*"keygen", */"label", "legend", "li", "link", "main", "map", "mark", "menu", "menuitem", "meta", "meter", "nav", "noframes", "noscript", "object", "ol", "optgroup", "option", "output", "p", "param", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "script", "section", "select", "small", "source", "span", "strike", "strong", "style", "sub", "summary", "sup", "table", "tbody", "td", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "tt", "u", "ul", "var", "video", "wbr"] - - tags.forEach(function(tag) { - var vnode = trust("<" + tag + " />") - render(root, vnode) - - o(vnode.dom.nodeName).equals(tag.toUpperCase()) - }) - }) - o("creates SVG", function() { - var vnode = trust("") - render(root, m("svg", 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 = trust("a") - render(root, m("svg", vnode)) - - o(vnode.dom.nodeValue).equals("a") - }) - o("handles empty SVG", function() { - var vnode = trust("") - render(root, m("svg", vnode)) - - o(vnode.dom).equals(null) - o(vnode.domSize).equals(0) - }) - o("handles multiple children in SVG", function() { - var vnode = trust("") - render(root, m("svg", 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") - }) - o("creates the dom correctly with a contenteditable parent", function() { - var div = m("div", {contenteditable: true}, trust("")) - - render(root, div) - var tags = [] - for (var i = 0; i < div.dom.childNodes.length; i++) { - tags.push(div.dom.childNodes[i].nodeName) - } - o(tags).deepEquals(["A"]) - }) -}) diff --git a/render/tests/test-createNodes.js b/render/tests/test-createNodes.js deleted file mode 100644 index f1bfc4ab2..000000000 --- a/render/tests/test-createNodes.js +++ /dev/null @@ -1,65 +0,0 @@ -"use strict" - -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") -var m = require("../../render/hyperscript") -var fragment = require("../../render/fragment") -var trust = require("../../render/trust") - -o.spec("createNodes", function() { - var $window, root, render - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - render = vdom($window) - }) - - o("creates nodes", function() { - var vnodes = [ - m("a"), - "b", - trust("c"), - fragment("d"), - ] - render(root, vnodes) - - o(root.childNodes.length).equals(4) - o(root.childNodes[0].nodeName).equals("A") - o(root.childNodes[1].nodeValue).equals("b") - o(root.childNodes[2].nodeValue).equals("c") - o(root.childNodes[3].nodeValue).equals("d") - }) - o("ignores null", function() { - var vnodes = [ - m("a"), - "b", - null, - trust("c"), - fragment("d"), - ] - render(root, vnodes) - - o(root.childNodes.length).equals(4) - o(root.childNodes[0].nodeName).equals("A") - o(root.childNodes[1].nodeValue).equals("b") - o(root.childNodes[2].nodeValue).equals("c") - o(root.childNodes[3].nodeValue).equals("d") - }) - o("ignores undefined", function() { - var vnodes = [ - m("a"), - "b", - undefined, - trust("c"), - fragment("d"), - ] - render(root, vnodes) - - o(root.childNodes.length).equals(4) - o(root.childNodes[0].nodeName).equals("A") - o(root.childNodes[1].nodeValue).equals("b") - o(root.childNodes[2].nodeValue).equals("c") - o(root.childNodes[3].nodeValue).equals("d") - }) -}) diff --git a/render/tests/test-createText.js b/render/tests/test-createText.js deleted file mode 100644 index e7fd74ec9..000000000 --- a/render/tests/test-createText.js +++ /dev/null @@ -1,69 +0,0 @@ -"use strict" - -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") - -o.spec("createText", function() { - var $window, root, render - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - render = vdom($window) - }) - - o("creates string", function() { - var vnode = "a" - render(root, vnode) - - o(root.firstChild.nodeName).equals("#text") - o(root.firstChild.nodeValue).equals("a") - }) - o("creates falsy string", function() { - var vnode = "" - render(root, vnode) - - o(root.firstChild.nodeName).equals("#text") - o(root.firstChild.nodeValue).equals("") - }) - o("creates number", function() { - var vnode = 1 - render(root, vnode) - - o(root.firstChild.nodeName).equals("#text") - o(root.firstChild.nodeValue).equals("1") - }) - o("creates falsy number", function() { - var vnode = 0 - render(root, vnode) - - o(root.firstChild.nodeName).equals("#text") - o(root.firstChild.nodeValue).equals("0") - }) - o("ignores true boolean", function() { - var vnode = true - render(root, vnode) - - o(root.childNodes.length).equals(0) - }) - o("creates false boolean", function() { - var vnode = false - render(root, vnode) - - o(root.childNodes.length).equals(0) - }) - o("creates spaces", function() { - var vnode = " " - render(root, vnode) - - o(root.firstChild.nodeName).equals("#text") - o(root.firstChild.nodeValue).equals(" ") - }) - o("ignores html", function() { - var vnode = "™" - render(root, vnode) - - o(root.firstChild.nodeName).equals("#text") - o(root.firstChild.nodeValue).equals("™") - }) -}) diff --git a/render/tests/test-domFor.js b/render/tests/test-domFor.js deleted file mode 100644 index b0c3444fa..000000000 --- a/render/tests/test-domFor.js +++ /dev/null @@ -1,178 +0,0 @@ -"use strict" - -const o = require("ospec") -const components = require("../../test-utils/components") -const domMock = require("../../test-utils/domMock") -const vdom = require("../render") -const m = require("../hyperscript") -const fragment = require("../fragment") -const domFor = require("../../render/domFor").domFor - -o.spec("domFor(vnode)", function() { - let $window, root, render - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - render = vdom($window) - }) - o("works for simple vnodes", function() { - render(root, m("div", {oncreate(vnode){ - let n = 0 - for (const dom of domFor(vnode)) { - o(dom).equals(root.firstChild) - o(++n).equals(1) - } - }})) - }) - o("works for fragments", function () { - render(root, fragment({ - oncreate(vnode){ - let n = 0 - for (const dom of domFor(vnode)) { - o(dom).equals(root.childNodes[n]) - n++ - } - o(n).equals(2) - } - }, [ - m("a"), - m("b") - ])) - }) - o("works in fragments with children that have delayed removal", function() { - function oncreate(vnode){ - o(root.childNodes.length).equals(3) - o(root.childNodes[0].nodeName).equals("A") - o(root.childNodes[1].nodeName).equals("B") - o(root.childNodes[2].nodeName).equals("C") - - const iter = domFor(vnode) - o(iter.next()).deepEquals({done:false, value: root.childNodes[0]}) - o(iter.next()).deepEquals({done:false, value: root.childNodes[1]}) - o(iter.next()).deepEquals({done:false, value: root.childNodes[2]}) - o(iter.next().done).deepEquals(true) - o(root.childNodes.length).equals(3) - } - function onupdate(vnode) { - // the b node is still present in the DOM - o(root.childNodes.length).equals(3) - o(root.childNodes[0].nodeName).equals("A") - o(root.childNodes[1].nodeName).equals("B") - o(root.childNodes[2].nodeName).equals("C") - - const iter = domFor(vnode) - o(iter.next()).deepEquals({done:false, value: root.childNodes[0]}) - o(iter.next()).deepEquals({done:false, value: root.childNodes[2]}) - o(iter.next().done).deepEquals(true) - o(root.childNodes.length).equals(3) - } - - render(root, fragment( - {oncreate, onupdate}, - [ - m("a"), - m("b", {onbeforeremove(){return {then(){}, finally(){}}}}), - m("c") - ] - )) - render(root, fragment( - {oncreate, onupdate}, - [ - m("a"), - null, - m("c"), - ] - )) - - }) - components.forEach(function(cmp){ - const {kind, create: createComponent} = cmp - o.spec(kind, function(){ - o("works for components that return one element", function() { - const C = createComponent({ - view(){return m("div")}, - oncreate(vnode){ - let n = 0 - for (const dom of domFor(vnode)) { - o(dom).equals(root.firstChild) - o(++n).equals(1) - } - } - }) - render(root, m(C)) - }) - o("works for components that return fragments", function () { - const oncreate = o.spy(function oncreate(vnode){ - o(root.childNodes.length).equals(3) - o(root.childNodes[0].nodeName).equals("A") - o(root.childNodes[1].nodeName).equals("B") - o(root.childNodes[2].nodeName).equals("C") - - const iter = domFor(vnode) - o(iter.next()).deepEquals({done:false, value: root.childNodes[0]}) - o(iter.next()).deepEquals({done:false, value: root.childNodes[1]}) - o(iter.next()).deepEquals({done:false, value: root.childNodes[2]}) - o(iter.next().done).deepEquals(true) - o(root.childNodes.length).equals(3) - }) - const C = createComponent({ - view({children}){return children}, - oncreate - }) - render(root, m(C, [ - m("a"), - m("b"), - m("c") - ])) - o(oncreate.callCount).equals(1) - }) - o("works for components that return fragments with delayed removal", function () { - const onbeforeremove = o.spy(function onbeforeremove(){return {then(){}, finally(){}}}) - const oncreate = o.spy(function oncreate(vnode){ - o(root.childNodes.length).equals(3) - o(root.childNodes[0].nodeName).equals("A") - o(root.childNodes[1].nodeName).equals("B") - o(root.childNodes[2].nodeName).equals("C") - - const iter = domFor(vnode) - o(iter.next()).deepEquals({done:false, value: root.childNodes[0]}) - o(iter.next()).deepEquals({done:false, value: root.childNodes[1]}) - o(iter.next()).deepEquals({done:false, value: root.childNodes[2]}) - o(iter.next().done).deepEquals(true) - o(root.childNodes.length).equals(3) - }) - const onupdate = o.spy(function onupdate(vnode) { - o(root.childNodes.length).equals(3) - o(root.childNodes[0].nodeName).equals("A") - o(root.childNodes[1].nodeName).equals("B") - o(root.childNodes[2].nodeName).equals("C") - - const iter = domFor(vnode) - - o(iter.next()).deepEquals({done:false, value: root.childNodes[0]}) - o(iter.next()).deepEquals({done:false, value: root.childNodes[2]}) - o(iter.next().done).deepEquals(true) - o(root.childNodes.length).equals(3) - }) - const C = createComponent({ - view({children}){return children}, - oncreate, - onupdate - }) - render(root, m(C, [ - m("a"), - m("b", {onbeforeremove}), - m("c") - ])) - render(root, m(C, [ - m("a"), - null, - m("c") - ])) - o(oncreate.callCount).equals(1) - o(onupdate.callCount).equals(1) - o(onbeforeremove.callCount).equals(1) - }) - }) - }) -}) \ No newline at end of file diff --git a/render/tests/test-event.js b/render/tests/test-event.js deleted file mode 100644 index 5acc29f41..000000000 --- a/render/tests/test-event.js +++ /dev/null @@ -1,387 +0,0 @@ -"use strict" - -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") -var m = require("../../render/hyperscript") - -o.spec("event", function() { - var $window, root, redraw, render, reallyRender - o.beforeEach(function() { - $window = domMock() - root = $window.document.body - redraw = o.spy() - reallyRender = vdom($window) - render = function(dom, vnode) { - return reallyRender(dom, vnode, redraw) - } - }) - - function eventSpy(fn) { - function spy(e) { - spy.calls.push({ - this: this, type: e.type, - target: e.target, currentTarget: e.currentTarget, - }) - if (fn) return fn.apply(this, arguments) - } - spy.calls = [] - return spy - } - - o("handles onclick", function() { - var spyDiv = eventSpy() - var spyParent = eventSpy() - var div = m("div", {onclick: spyDiv}) - var parent = m("div", {onclick: spyParent}, div) - var e = $window.document.createEvent("MouseEvents") - e.initEvent("click", true, true) - - render(root, parent) - div.dom.dispatchEvent(e) - - o(spyDiv.calls.length).equals(1) - o(spyDiv.calls[0].this).equals(div.dom) - o(spyDiv.calls[0].type).equals("click") - o(spyDiv.calls[0].target).equals(div.dom) - o(spyDiv.calls[0].currentTarget).equals(div.dom) - o(spyParent.calls.length).equals(1) - o(spyParent.calls[0].this).equals(parent.dom) - o(spyParent.calls[0].type).equals("click") - o(spyParent.calls[0].target).equals(div.dom) - o(spyParent.calls[0].currentTarget).equals(parent.dom) - o(redraw.callCount).equals(2) - o(redraw.this).equals(undefined) - o(redraw.args.length).equals(0) - o(e.defaultPrevented).equals(false) - }) - - o("handles onclick returning false", function() { - var spyDiv = eventSpy(function() { return false }) - var spyParent = eventSpy() - var div = m("div", {onclick: spyDiv}) - var parent = m("div", {onclick: spyParent}, div) - var e = $window.document.createEvent("MouseEvents") - e.initEvent("click", true, true) - - render(root, parent) - div.dom.dispatchEvent(e) - - o(spyDiv.calls.length).equals(1) - o(spyDiv.calls[0].this).equals(div.dom) - o(spyDiv.calls[0].type).equals("click") - o(spyDiv.calls[0].target).equals(div.dom) - o(spyDiv.calls[0].currentTarget).equals(div.dom) - o(spyParent.calls.length).equals(0) - o(redraw.callCount).equals(1) - o(redraw.this).equals(undefined) - o(redraw.args.length).equals(0) - o(e.defaultPrevented).equals(true) - }) - - o("handles click EventListener object", function() { - var spyDiv = eventSpy() - var spyParent = eventSpy() - var listenerDiv = {handleEvent: spyDiv} - var listenerParent = {handleEvent: spyParent} - var div = m("div", {onclick: listenerDiv}) - var parent = m("div", {onclick: listenerParent}, div) - var e = $window.document.createEvent("MouseEvents") - e.initEvent("click", true, true) - - render(root, parent) - div.dom.dispatchEvent(e) - - o(spyDiv.calls.length).equals(1) - o(spyDiv.calls[0].this).equals(listenerDiv) - o(spyDiv.calls[0].type).equals("click") - o(spyDiv.calls[0].target).equals(div.dom) - o(spyDiv.calls[0].currentTarget).equals(div.dom) - o(spyParent.calls.length).equals(1) - o(spyParent.calls[0].this).equals(listenerParent) - o(spyParent.calls[0].type).equals("click") - o(spyParent.calls[0].target).equals(div.dom) - o(spyParent.calls[0].currentTarget).equals(parent.dom) - o(redraw.callCount).equals(2) - o(redraw.this).equals(undefined) - o(redraw.args.length).equals(0) - o(e.defaultPrevented).equals(false) - }) - - o("handles click EventListener object returning false", function() { - var spyDiv = eventSpy(function() { return false }) - var spyParent = eventSpy() - var listenerDiv = {handleEvent: spyDiv} - var listenerParent = {handleEvent: spyParent} - var div = m("div", {onclick: listenerDiv}) - var parent = m("div", {onclick: listenerParent}, div) - var e = $window.document.createEvent("MouseEvents") - e.initEvent("click", true, true) - - render(root, parent) - div.dom.dispatchEvent(e) - - o(spyDiv.calls.length).equals(1) - o(spyDiv.calls[0].this).equals(listenerDiv) - o(spyDiv.calls[0].type).equals("click") - o(spyDiv.calls[0].target).equals(div.dom) - o(spyDiv.calls[0].currentTarget).equals(div.dom) - o(spyParent.calls.length).equals(1) - o(spyParent.calls[0].this).equals(listenerParent) - o(spyParent.calls[0].type).equals("click") - o(spyParent.calls[0].target).equals(div.dom) - o(spyParent.calls[0].currentTarget).equals(parent.dom) - o(redraw.callCount).equals(2) - o(redraw.this).equals(undefined) - o(redraw.args.length).equals(0) - o(e.defaultPrevented).equals(false) - }) - - o("removes event", function() { - var spy = o.spy() - var vnode = m("a", {onclick: spy}) - var updated = m("a") - - 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 null", function() { - var spy = o.spy() - var vnode = m("a", {onclick: spy}) - var updated = m("a", {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 = m("a", {onclick: spy}) - var updated = m("a", {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 = m("a", {ontouchstart: spy}) - var updated = m("a", {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 = m("a", {ontouchstart: spy}) - var updated = m("a") - - 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 = m("a", {ontouchstart: spy}) - var updated = m("a", {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 = m("a", {onclick: listener}) - var updated = m("a") - - 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 = m("a", {onclick: listener}) - var updated = m("a", {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 = m("a", {onclick: listener}) - var updated = m("a", {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 = m("div", {id: "a", onclick: spy}) - var updated = m("div", {id: "b", onclick: spy}) - 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(div.dom) - o(spy.args[0].type).equals("click") - o(spy.args[0].target).equals(div.dom) - o(redraw.callCount).equals(1) - o(redraw.this).equals(undefined) - o(redraw.args.length).equals(0) - o(div.dom).equals(updated.dom) - 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 = m("div", {id: "a", onclick: listener}) - var updated = m("div", {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(redraw.callCount).equals(1) - o(redraw.this).equals(undefined) - o(redraw.args.length).equals(0) - o(div.dom).equals(updated.dom) - o(div.dom.attributes["id"].value).equals("b") - }) - - o("handles ontransitionend", function() { - var spy = o.spy() - var div = m("div", {ontransitionend: spy}) - 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(div.dom) - o(spy.args[0].type).equals("transitionend") - o(spy.args[0].target).equals(div.dom) - o(redraw.callCount).equals(1) - o(redraw.this).equals(undefined) - o(redraw.args.length).equals(0) - }) - - o("handles transitionend EventListener object", function() { - var spy = o.spy() - var listener = {handleEvent: spy} - var div = m("div", {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(redraw.callCount).equals(1) - o(redraw.this).equals(undefined) - o(redraw.args.length).equals(0) - }) - - o("handles changed spy", function() { - var div1 = m("div", {ontransitionend: function() {}}) - - reallyRender(root, [div1], redraw) - var e = $window.document.createEvent("HTMLEvents") - e.initEvent("transitionend", true, true) - div1.dom.dispatchEvent(e) - - o(redraw.callCount).equals(1) - o(redraw.this).equals(undefined) - o(redraw.args.length).equals(0) - - var replacementRedraw = o.spy() - var div2 = m("div", {ontransitionend: function() {}}) - - reallyRender(root, [div2], replacementRedraw) - var e = $window.document.createEvent("HTMLEvents") - e.initEvent("transitionend", true, true) - div2.dom.dispatchEvent(e) - - o(redraw.callCount).equals(1) - o(redraw.this).equals(undefined) - o(redraw.args.length).equals(0) - o(replacementRedraw.callCount).equals(1) - o(replacementRedraw.this).equals(undefined) - o(replacementRedraw.args.length).equals(0) - }) -}) diff --git a/render/tests/test-fragment.js b/render/tests/test-fragment.js deleted file mode 100644 index cea868c0d..000000000 --- a/render/tests/test-fragment.js +++ /dev/null @@ -1,197 +0,0 @@ -"use strict" - -var o = require("ospec") -var fragment = require("../../render/fragment") -var m = require("../../render/hyperscript") - -function fragmentStr() { - var args = [].slice.call(arguments); - args.unshift("["); - return m.apply(null, args) -} - -function runTest(name, fragment) { - o.spec(name, function() { - o("works", function() { - var attrs = {foo: 5} - var child = m("p") - var frag = fragment(attrs, child) - - o(frag.tag).equals("[") - - o(Array.isArray(frag.children)).equals(true) - o(frag.children.length).equals(1) - o(frag.children[0]).equals(child) - - o(frag.attrs).equals(attrs) - - o(frag.key).equals(undefined) - }) - o("supports keys", function() { - var attrs = {key: 7} - var frag = fragment(attrs, []) - o(frag.tag).equals("[") - - o(Array.isArray(frag.children)).equals(true) - o(frag.children.length).equals(0) - - o(frag.attrs).equals(attrs) - o(frag.attrs.key).equals(7) - - o(frag.key).equals(7) - }) - o.spec("children with no attrs", function() { - o("handles string single child", function() { - var vnode = fragment(["a"]) - - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("a") - }) - o("handles falsy string single child", function() { - var vnode = fragment([""]) - - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("") - }) - o("handles number single child", function() { - var vnode = fragment([1]) - - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("1") - }) - o("handles falsy number single child", function() { - var vnode = fragment([0]) - - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("0") - }) - o("handles boolean single child", function() { - var vnode = fragment([true]) - - o(vnode.children).deepEquals([null]) - }) - o("handles falsy boolean single child", function() { - var vnode = fragment([false]) - - o(vnode.children).deepEquals([null]) - }) - o("handles null single child", function() { - var vnode = fragment([null]) - - o(vnode.children[0]).equals(null) - }) - o("handles undefined single child", function() { - var vnode = fragment([undefined]) - - o(vnode.children).deepEquals([null]) - }) - o("handles multiple string children", function() { - var vnode = fragment(["", "a"]) - - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("") - o(vnode.children[1].tag).equals("#") - o(vnode.children[1].children).equals("a") - }) - o("handles multiple number children", function() { - var vnode = fragment([0, 1]) - - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("0") - o(vnode.children[1].tag).equals("#") - o(vnode.children[1].children).equals("1") - }) - o("handles multiple boolean children", function() { - var vnode = fragment([false, true]) - - o(vnode.children).deepEquals([null, null]) - }) - o("handles multiple null/undefined child", function() { - var vnode = fragment([null, undefined]) - - o(vnode.children).deepEquals([null, null]) - }) - o("handles falsy number single child without attrs", function() { - var vnode = fragment(0) - - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("0") - }) - }) - o.spec("children with attrs", function() { - o("handles string single child", function() { - var vnode = fragment({}, ["a"]) - - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("a") - }) - o("handles falsy string single child", function() { - var vnode = fragment({}, [""]) - - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("") - }) - o("handles number single child", function() { - var vnode = fragment({}, [1]) - - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("1") - }) - o("handles falsy number single child", function() { - var vnode = fragment({}, [0]) - - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("0") - }) - o("handles boolean single child", function() { - var vnode = fragment({}, [true]) - - o(vnode.children).deepEquals([null]) - }) - o("handles falsy boolean single child", function() { - var vnode = fragment({}, [false]) - - o(vnode.children).deepEquals([null]) - }) - o("handles null single child", function() { - var vnode = fragment({}, [null]) - - o(vnode.children).deepEquals([null]) - }) - o("handles undefined single child", function() { - var vnode = fragment({}, [undefined]) - - o(vnode.children).deepEquals([null]) - }) - o("handles multiple string children", function() { - var vnode = fragment({}, ["", "a"]) - - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("") - o(vnode.children[1].tag).equals("#") - o(vnode.children[1].children).equals("a") - }) - o("handles multiple number children", function() { - var vnode = fragment({}, [0, 1]) - - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("0") - o(vnode.children[1].tag).equals("#") - o(vnode.children[1].children).equals("1") - }) - o("handles multiple boolean children", function() { - var vnode = fragment({}, [false, true]) - - o(vnode.children).deepEquals([null, null]) - }) - o("handles multiple null/undefined child", function() { - var vnode = fragment({}, [null, undefined]) - - o(vnode.children).deepEquals([null, null]) - }) - }) - }) -} - -runTest("fragment", fragment); -runTest("fragment-string-selector", fragmentStr); diff --git a/render/tests/test-hyperscript.js b/render/tests/test-hyperscript.js deleted file mode 100644 index ed918fe1e..000000000 --- a/render/tests/test-hyperscript.js +++ /dev/null @@ -1,668 +0,0 @@ -"use strict" - -var o = require("ospec") -var m = require("../../render/hyperscript") - -o.spec("hyperscript", function() { - o.spec("selector", function() { - o("throws on null selector", function(done) { - try {m(null)} catch(e) {done()} - }) - o("throws on non-string selector w/o a view property", function(done) { - try {m({})} catch(e) {done()} - }) - o("handles tag in selector", function() { - var vnode = m("a") - - o(vnode.tag).equals("a") - }) - o("class and className normalization", function(){ - o(m("a", { - class: null - }).attrs).deepEquals({ - class: null - }) - o(m("a", { - class: undefined - }).attrs).deepEquals({ - class: null - }) - o(m("a", { - class: false - }).attrs).deepEquals({ - class: null, - className: false - }) - o(m("a", { - class: true - }).attrs).deepEquals({ - class: null, - className: true - }) - o(m("a.x", { - class: null - }).attrs).deepEquals({ - class: null, - className: "x" - }) - o(m("a.x", { - class: undefined - }).attrs).deepEquals({ - class: null, - className: "x" - }) - o(m("a.x", { - class: false - }).attrs).deepEquals({ - class: null, - className: "x false" - }) - o(m("a.x", { - class: true - }).attrs).deepEquals({ - class: null, - className: "x true" - }) - o(m("a", { - className: null - }).attrs).deepEquals({ - className: null - }) - o(m("a", { - className: undefined - }).attrs).deepEquals({ - className: undefined - }) - o(m("a", { - className: false - }).attrs).deepEquals({ - className: false - }) - o(m("a", { - className: true - }).attrs).deepEquals({ - className: true - }) - o(m("a.x", { - className: null - }).attrs).deepEquals({ - className: "x" - }) - o(m("a.x", { - className: undefined - }).attrs).deepEquals({ - className: "x" - }) - o(m("a.x", { - className: false - }).attrs).deepEquals({ - className: "x false" - }) - o(m("a.x", { - className: true - }).attrs).deepEquals({ - className: "x true" - }) - }) - o("handles class in selector", function() { - var vnode = m(".a") - - o(vnode.tag).equals("div") - o(vnode.attrs.className).equals("a") - }) - o("handles many classes in selector", function() { - var vnode = m(".a.b.c") - - o(vnode.tag).equals("div") - o(vnode.attrs.className).equals("a b c") - }) - o("handles id in selector", function() { - var vnode = m("#a") - - o(vnode.tag).equals("div") - o(vnode.attrs.id).equals("a") - }) - o("handles attr in selector", function() { - var vnode = m("[a=b]") - - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals("b") - }) - o("handles many attrs in selector", function() { - var vnode = m("[a=b][c=d]") - - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals("b") - o(vnode.attrs.c).equals("d") - }) - o("handles attr w/ spaces in selector", function() { - var vnode = m("[a = b]") - - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals("b") - }) - o("handles attr w/ quotes in selector", function() { - var vnode = m("[a='b']") - - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals("b") - }) - o("handles attr w/ quoted square bracket", function() { - var vnode = m("[x][a='[b]'].c") - - o(vnode.tag).equals("div") - o(vnode.attrs.x).equals(true) - o(vnode.attrs.a).equals("[b]") - o(vnode.attrs.className).equals("c") - }) - o("handles attr w/ unmatched square bracket", function() { - var vnode = m("[a=']'].c") - - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals("]") - o(vnode.attrs.className).equals("c") - }) - o("handles attr w/ quoted square bracket and quote", function() { - var vnode = m("[a='[b\"\\']'].c") // `[a='[b"\']']` - - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals("[b\"']") // `[b"']` - o(vnode.attrs.className).equals("c") - }) - o("handles attr w/ quoted square containing escaped square bracket", function() { - var vnode = m("[a='[\\]]'].c") // `[a='[\]]']` - - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals("[\\]]") // `[\]]` - o(vnode.attrs.className).equals("c") - }) - o("handles attr w/ backslashes", function() { - var vnode = m("[a='\\\\'].c") // `[a='\\']` - - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals("\\") - o(vnode.attrs.className).equals("c") - }) - o("handles attr w/ quotes and spaces in selector", function() { - var vnode = m("[a = 'b']") - - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals("b") - }) - o("handles many attr w/ quotes and spaces in selector", function() { - var vnode = m("[a = 'b'][c = 'd']") - - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals("b") - o(vnode.attrs.c).equals("d") - }) - o("handles tag, class, attrs in selector", function() { - var vnode = m("a.b[c = 'd']") - - o(vnode.tag).equals("a") - o(vnode.attrs.className).equals("b") - o(vnode.attrs.c).equals("d") - }) - o("handles tag, mixed classes, attrs in selector", function() { - var vnode = m("a.b[c = 'd'].e[f = 'g']") - - o(vnode.tag).equals("a") - o(vnode.attrs.className).equals("b e") - o(vnode.attrs.c).equals("d") - o(vnode.attrs.f).equals("g") - }) - o("handles attr without value", function() { - var vnode = m("[a]") - - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals(true) - }) - o("handles explicit empty string value for input", function() { - var vnode = m('input[value=""]') - - o(vnode.tag).equals("input") - o(vnode.attrs.value).equals("") - }) - o("handles explicit empty string value for option", function() { - var vnode = m('option[value=""]') - - o(vnode.tag).equals("option") - o(vnode.attrs.value).equals("") - }) - }) - o.spec("attrs", function() { - o("handles string attr", function() { - var vnode = m("div", {a: "b"}) - - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals("b") - }) - o("handles falsy string attr", function() { - var vnode = m("div", {a: ""}) - - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals("") - }) - o("handles number attr", function() { - var vnode = m("div", {a: 1}) - - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals(1) - }) - o("handles falsy number attr", function() { - var vnode = m("div", {a: 0}) - - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals(0) - }) - o("handles boolean attr", function() { - var vnode = m("div", {a: true}) - - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals(true) - }) - o("handles falsy boolean attr", function() { - var vnode = m("div", {a: false}) - - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals(false) - }) - o("handles only key in attrs", function() { - var vnode = m("div", {key:"a"}) - - o(vnode.tag).equals("div") - o(vnode.attrs).deepEquals({key:"a"}) - o(vnode.key).equals("a") - }) - o("handles many attrs", function() { - var vnode = m("div", {a: "b", c: "d"}) - - o(vnode.tag).equals("div") - o(vnode.attrs.a).equals("b") - o(vnode.attrs.c).equals("d") - }) - o("handles className attrs property", function() { - var vnode = m("div", {className: "a"}) - - o(vnode.attrs.className).equals("a") - }) - o("handles 'class' as a verbose attribute declaration", function() { - var vnode = m("[class=a]") - - o(vnode.attrs.className).equals("a") - }) - o("handles merging classes w/ class property", function() { - var vnode = m(".a", {class: "b"}) - - o(vnode.attrs.className).equals("a b") - }) - o("handles merging classes w/ className property", function() { - var vnode = m(".a", {className: "b"}) - - 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).deepEquals({key:"a"}) - 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("casts className using toString like browsers", function() { - const className = { - valueOf: () => ".valueOf", - toString: () => "toString" - } - var vnode = m("custom-element" + className, {className: className}) - - o(vnode.attrs.className).equals("valueOf toString") - }) - }) - o.spec("children", function() { - o("handles string single child", function() { - var vnode = m("div", {}, ["a"]) - - o(vnode.children[0].children).equals("a") - }) - o("handles falsy string single child", function() { - var vnode = m("div", {}, [""]) - - o(vnode.children[0].children).equals("") - }) - o("handles number single child", function() { - var vnode = m("div", {}, [1]) - - o(vnode.children[0].children).equals("1") - }) - o("handles falsy number single child", function() { - var vnode = m("div", {}, [0]) - - o(vnode.children[0].children).equals("0") - }) - o("handles boolean single child", function() { - var vnode = m("div", {}, [true]) - - o(vnode.children).deepEquals([null]) - }) - o("handles falsy boolean single child", function() { - var vnode = m("div", {}, [false]) - - o(vnode.children).deepEquals([null]) - }) - o("handles null single child", function() { - var vnode = m("div", {}, [null]) - - o(vnode.children).deepEquals([null]) - }) - o("handles undefined single child", function() { - var vnode = m("div", {}, [undefined]) - - o(vnode.children).deepEquals([null]) - }) - o("handles multiple string children", function() { - var vnode = m("div", {}, ["", "a"]) - - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("") - o(vnode.children[1].tag).equals("#") - o(vnode.children[1].children).equals("a") - }) - o("handles multiple number children", function() { - var vnode = m("div", {}, [0, 1]) - - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("0") - o(vnode.children[1].tag).equals("#") - o(vnode.children[1].children).equals("1") - }) - o("handles multiple boolean children", function() { - var vnode = m("div", {}, [false, true]) - - o(vnode.children).deepEquals([null, null]) - }) - o("handles multiple null/undefined child", function() { - var vnode = m("div", {}, [null, undefined]) - - o(vnode.children).deepEquals([null, null]) - }) - o("handles falsy number single child without attrs", function() { - var vnode = m("div", 0) - - o(vnode.children[0].children).equals("0") - }) - }) - o.spec("permutations", function() { - o("handles null attr and children", function() { - var vnode = m("div", null, [m("a"), m("b")]) - - o(vnode.children.length).equals(2) - o(vnode.children[0].tag).equals("a") - o(vnode.children[1].tag).equals("b") - }) - o("handles null attr and child unwrapped", function() { - var vnode = m("div", null, m("a")) - - o(vnode.children.length).equals(1) - o(vnode.children[0].tag).equals("a") - }) - o("handles null attr and children unwrapped", function() { - var vnode = m("div", null, m("a"), m("b")) - - o(vnode.children.length).equals(2) - o(vnode.children[0].tag).equals("a") - o(vnode.children[1].tag).equals("b") - }) - o("handles attr and children", function() { - var vnode = m("div", {a: "b"}, [m("i"), m("s")]) - - o(vnode.attrs.a).equals("b") - o(vnode.children[0].tag).equals("i") - o(vnode.children[1].tag).equals("s") - }) - o("handles attr and child unwrapped", function() { - var vnode = m("div", {a: "b"}, m("i")) - - o(vnode.attrs.a).equals("b") - o(vnode.children[0].tag).equals("i") - }) - o("handles attr and children unwrapped", function() { - var vnode = m("div", {a: "b"}, m("i"), m("s")) - - o(vnode.attrs.a).equals("b") - o(vnode.children[0].tag).equals("i") - o(vnode.children[1].tag).equals("s") - }) - o("handles attr and text children", function() { - var vnode = m("div", {a: "b"}, ["c", "d"]) - - o(vnode.attrs.a).equals("b") - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("c") - o(vnode.children[1].tag).equals("#") - o(vnode.children[1].children).equals("d") - }) - o("handles attr and single string text child", function() { - var vnode = m("div", {a: "b"}, ["c"]) - - o(vnode.attrs.a).equals("b") - o(vnode.children[0].children).equals("c") - }) - o("handles attr and single falsy string text child", function() { - var vnode = m("div", {a: "b"}, [""]) - - o(vnode.attrs.a).equals("b") - o(vnode.children[0].children).equals("") - }) - o("handles attr and single number text child", function() { - var vnode = m("div", {a: "b"}, [1]) - - o(vnode.attrs.a).equals("b") - o(vnode.children[0].children).equals("1") - }) - o("handles attr and single falsy number text child", function() { - var vnode = m("div", {a: "b"}, [0]) - - o(vnode.attrs.a).equals("b") - o(vnode.children[0].children).equals("0") - }) - o("handles attr and single boolean text child", function() { - var vnode = m("div", {a: "b"}, [true]) - - o(vnode.attrs.a).equals("b") - o(vnode.children).deepEquals([null]) - }) - o("handles attr and single falsy boolean text child", function() { - var vnode = m("div", {a: "b"}, [0]) - - o(vnode.attrs.a).equals("b") - o(vnode.children[0].children).equals("0") - }) - o("handles attr and single false boolean text child", function() { - var vnode = m("div", {a: "b"}, [false]) - - o(vnode.attrs.a).equals("b") - o(vnode.children).deepEquals([null]) - }) - o("handles attr and single text child unwrapped", function() { - var vnode = m("div", {a: "b"}, "c") - - o(vnode.attrs.a).equals("b") - o(vnode.children[0].children).equals("c") - }) - o("handles attr and text children unwrapped", function() { - var vnode = m("div", {a: "b"}, "c", "d") - - o(vnode.attrs.a).equals("b") - o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals("c") - o(vnode.children[1].tag).equals("#") - o(vnode.children[1].children).equals("d") - }) - o("handles children without attr", function() { - var vnode = m("div", [m("i"), m("s")]) - - o(vnode.attrs).deepEquals({}) - 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).deepEquals({}) - 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).deepEquals({}) - o(vnode.children[0].tag).equals("i") - o(vnode.children[1].tag).equals("s") - }) - o("handles shared attrs", function() { - var attrs = {a: "b"} - - var nodeA = m(".a", attrs) - var nodeB = m(".b", attrs) - - o(nodeA.attrs.className).equals("a") - o(nodeA.attrs.a).equals("b") - - o(nodeB.attrs.className).equals("b") - o(nodeB.attrs.a).equals("b") - }) - o("handles shared empty attrs (#2821)", function() { - var attrs = {} - - var nodeA = m(".a", attrs) - var nodeB = m(".b", attrs) - - o(nodeA.attrs.className).equals("a") - o(nodeB.attrs.className).equals("b") - }) - o("doesnt modify passed attributes object", function() { - var attrs = {a: "b"} - 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")]) - - o(vnode.children[0].tag).equals("[") - o(vnode.children[0].children[0].tag).equals("i") - o(vnode.children[1].tag).equals("[") - o(vnode.children[1].children[0].tag).equals("s") - }) - o("handles children with nested array", function() { - var vnode = m("div", [[m("i"), m("s")]]) - - o(vnode.children[0].tag).equals("[") - o(vnode.children[0].children[0].tag).equals("i") - o(vnode.children[0].children[1].tag).equals("s") - }) - o("handles children with deeply nested array", function() { - var vnode = m("div", [[[m("i"), m("s")]]]) - - o(vnode.children[0].tag).equals("[") - o(vnode.children[0].children[0].tag).equals("[") - o(vnode.children[0].children[0].children[0].tag).equals("i") - o(vnode.children[0].children[0].children[1].tag).equals("s") - }) - }) - o.spec("components", function() { - o("works with POJOs", function() { - var component = { - 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]).equals("b") - }) - 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") - - 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") - }) - }) -}) diff --git a/render/tests/test-normalize.js b/render/tests/test-normalize.js deleted file mode 100644 index 3a656dd86..000000000 --- a/render/tests/test-normalize.js +++ /dev/null @@ -1,55 +0,0 @@ -"use strict" - -var o = require("ospec") -var Vnode = require("../../render/vnode") - -o.spec("normalize", function() { - o("normalizes array into fragment", function() { - var node = Vnode.normalize([]) - - o(node.tag).equals("[") - o(node.children.length).equals(0) - }) - o("normalizes nested array into fragment", function() { - var node = Vnode.normalize([[]]) - - o(node.tag).equals("[") - o(node.children.length).equals(1) - o(node.children[0].tag).equals("[") - o(node.children[0].children.length).equals(0) - }) - o("normalizes string into text node", function() { - var node = Vnode.normalize("a") - - o(node.tag).equals("#") - o(node.children).equals("a") - }) - o("normalizes falsy string into text node", function() { - var node = Vnode.normalize("") - - o(node.tag).equals("#") - o(node.children).equals("") - }) - o("normalizes number into text node", function() { - var node = Vnode.normalize(1) - - o(node.tag).equals("#") - o(node.children).equals("1") - }) - o("normalizes falsy number into text node", function() { - var node = Vnode.normalize(0) - - o(node.tag).equals("#") - o(node.children).equals("0") - }) - o("normalizes `true` to `null`", function() { - var node = Vnode.normalize(true) - - o(node).equals(null) - }) - o("normalizes `false` to `null`", function() { - var node = Vnode.normalize(false) - - o(node).equals(null) - }) -}) diff --git a/render/tests/test-normalizeChildren.js b/render/tests/test-normalizeChildren.js deleted file mode 100644 index a7cd5ba5b..000000000 --- a/render/tests/test-normalizeChildren.js +++ /dev/null @@ -1,56 +0,0 @@ -"use strict" - -var o = require("ospec") -var Vnode = require("../../render/vnode") - -o.spec("normalizeChildren", function() { - o("normalizes arrays into fragments", function() { - var children = Vnode.normalizeChildren([[]]) - - o(children[0].tag).equals("[") - o(children[0].children.length).equals(0) - }) - o("normalizes strings into text nodes", function() { - var children = Vnode.normalizeChildren(["a"]) - - o(children[0].tag).equals("#") - o(children[0].children).equals("a") - }) - o("normalizes `false` values into `null`s", function() { - var children = Vnode.normalizeChildren([false]) - - o(children[0]).equals(null) - }) - o("allows all keys", function() { - var children = Vnode.normalizeChildren([ - {key: 1}, - {key: 2}, - ]) - - o(children).deepEquals([{key: 1}, {key: 2}]) - }) - o("allows no keys", function() { - var children = Vnode.normalizeChildren([ - {data: 1}, - {data: 2}, - ]) - - o(children).deepEquals([{data: 1}, {data: 2}]) - }) - o("disallows mixed keys, starting with key", function() { - o(function() { - Vnode.normalizeChildren([ - {key: 1}, - {data: 2}, - ]) - }).throws(TypeError) - }) - o("disallows mixed keys, starting with no key", function() { - o(function() { - Vnode.normalizeChildren([ - {data: 1}, - {key: 2}, - ]) - }).throws(TypeError) - }) -}) diff --git a/render/tests/test-normalizeComponentChildren.js b/render/tests/test-normalizeComponentChildren.js deleted file mode 100644 index 0ceabdf4f..000000000 --- a/render/tests/test-normalizeComponentChildren.js +++ /dev/null @@ -1,34 +0,0 @@ -"use strict" - -var o = require("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) - - 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") - }) - }) -}) diff --git a/render/tests/test-onbeforeremove.js b/render/tests/test-onbeforeremove.js deleted file mode 100644 index b5621e421..000000000 --- a/render/tests/test-onbeforeremove.js +++ /dev/null @@ -1,165 +0,0 @@ -"use strict" - -var o = require("ospec") -var callAsync = require("../../test-utils/callAsync") -var components = require("../../test-utils/components") -var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") -var m = require("../../render/hyperscript") -var fragment = require("../../render/fragment") - -o.spec("onbeforeremove", function() { - var $window, root, render - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - render = vdom($window) - }) - - o("does not call onbeforeremove when creating", function() { - var create = o.spy() - var vnode = m("div", {onbeforeremove: create}) - - render(root, vnode) - - o(create.callCount).equals(0) - }) - o("does not call onbeforeremove when updating", function() { - var create = o.spy() - var update = o.spy() - var vnode = m("div", {onbeforeremove: create}) - var updated = m("div", {onbeforeremove: update}) - - render(root, vnode) - render(root, updated) - - o(create.callCount).equals(0) - o(update.callCount).equals(0) - }) - o("calls onbeforeremove when removing element", function(done) { - var vnode = m("div", {onbeforeremove: remove}) - - render(root, vnode) - render(root, []) - - 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) - - callAsync(function() { - o(root.childNodes.length).equals(0) - - done() - }) - } - }) - o("calls onbeforeremove when removing fragment", function(done) { - var vnode = fragment({onbeforeremove: remove}, m("div")) - - render(root, vnode) - render(root, []) - - function remove(node) { - o(node).equals(vnode) - o(root.childNodes.length).equals(1) - o(root.firstChild).equals(vnode.dom) - - callAsync(function() { - o(root.childNodes.length).equals(0) - - done() - }) - } - }) - o("calls onremove after onbeforeremove resolves", function(done) { - var spy = o.spy() - var vnode = fragment({onbeforeremove: onbeforeremove, onremove: spy}, "a") - - render(root, vnode) - render(root, []) - - function onbeforeremove(node) { - o(node).equals(vnode) - o(root.childNodes.length).equals(1) - o(root.firstChild).equals(vnode.dom) - o(spy.callCount).equals(0) - - callAsync(function() { - o(root.childNodes.length).equals(0) - o(spy.callCount).equals(1) - - done() - }) - } - }) - o("does not set onbeforeremove as an event handler", function() { - var remove = o.spy() - var vnode = m("div", {onbeforeremove: remove}) - - render(root, vnode) - - o(vnode.dom.onbeforeremove).equals(undefined) - o(vnode.dom.attributes["onbeforeremove"]).equals(undefined) - }) - o("does not leave elements out of order during removal", function(done) { - var remove = function() {return Promise.resolve()} - var vnodes = [m("div", {key: 1, onbeforeremove: remove}, "1"), m("div", {key: 2, onbeforeremove: remove}, "2")] - var updated = m("div", {key: 2, onbeforeremove: remove}, "2") - - render(root, vnodes) - render(root, updated) - - o(root.childNodes.length).equals(2) - o(root.firstChild.firstChild.nodeValue).equals("1") - - callAsync(function() { - o(root.childNodes.length).equals(1) - o(root.firstChild.firstChild.nodeValue).equals("2") - - done() - }) - }) - components.forEach(function(cmp){ - o.spec(cmp.kind, function(){ - var createComponent = cmp.create - o("finalizes the remove phase asynchronously when promise is returned synchronously from both attrs- and tag.onbeforeremove", function(done) { - var onremove = o.spy() - var onbeforeremove = function(){return Promise.resolve()} - var component = createComponent({ - onbeforeremove: onbeforeremove, - onremove: onremove, - view: function() {}, - }) - render(root, m(component, {onbeforeremove: onbeforeremove, onremove: onremove})) - render(root, []) - callAsync(function() { - o(onremove.callCount).equals(2) // once for `tag`, once for `attrs` - done() - }) - }) - o("awaits promise resolution before removing the node", function(done) { - var view = o.spy() - var onremove = o.spy() - var onbeforeremove = function(){return new Promise(function(resolve){callAsync(resolve)})} - var component = createComponent({ - onbeforeremove: onbeforeremove, - onremove: onremove, - view: view, - }) - render(root, m(component)) - render(root, []) - - o(onremove.callCount).equals(0) - callAsync(function(){ - callAsync(function() { - o(onremove.callCount).equals(1) - done() - }) - }) - }) - }) - }) -}) diff --git a/render/tests/test-onbeforeupdate.js b/render/tests/test-onbeforeupdate.js deleted file mode 100644 index 7bdcfe9ed..000000000 --- a/render/tests/test-onbeforeupdate.js +++ /dev/null @@ -1,385 +0,0 @@ -"use strict" - -var o = require("ospec") -var components = require("../../test-utils/components") -var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") -var m = require("../../render/hyperscript") -var fragment = require("../../render/fragment") - -o.spec("onbeforeupdate", function() { - var $window, root, render - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - render = vdom($window) - }) - - o("prevents update in element", function() { - var onbeforeupdate = function() {return false} - var vnode = m("div", {id: "a", onbeforeupdate: onbeforeupdate}) - var updated = m("div", {id: "b", onbeforeupdate: onbeforeupdate}) - - render(root, vnode) - render(root, updated) - - o(root.firstChild.attributes["id"].value).equals("a") - }) - - o("prevents update in fragment", function() { - var onbeforeupdate = function() {return false} - var vnode = fragment({onbeforeupdate: onbeforeupdate}, "a") - var updated = fragment({onbeforeupdate: onbeforeupdate}, "b") - - render(root, vnode) - render(root, updated) - - o(root.firstChild.nodeValue).equals("a") - }) - - o("does not prevent update if returning true", function() { - var onbeforeupdate = function() {return true} - var vnode = m("div", {id: "a", onbeforeupdate: onbeforeupdate}) - var updated = m("div", {id: "b", onbeforeupdate: onbeforeupdate}) - - render(root, vnode) - render(root, updated) - - o(root.firstChild.attributes["id"].value).equals("b") - }) - - o("accepts arguments for comparison", function() { - var count = 0 - var vnode = m("div", {id: "a", onbeforeupdate: onbeforeupdate}) - var updated = m("div", {id: "b", onbeforeupdate: onbeforeupdate}) - - render(root, vnode) - render(root, updated) - - function onbeforeupdate(vnode, old) { - count++ - - o(old.attrs.id).equals("a") - o(vnode.attrs.id).equals("b") - - return old.attrs.id !== vnode.attrs.id - } - - o(count).equals(1) - o(root.firstChild.attributes["id"].value).equals("b") - }) - - o("is not called on creation", function() { - var count = 0 - var vnode = m("div", {id: "a", onbeforeupdate: onbeforeupdate}) - - render(root, vnode) - - function onbeforeupdate() { - count++ - return true - } - - o(count).equals(0) - }) - - o("is called only once on update", function() { - var count = 0 - var vnode = m("div", {id: "a", onbeforeupdate: onbeforeupdate}) - var updated = m("div", {id: "b", onbeforeupdate: onbeforeupdate}) - - render(root, vnode) - render(root, updated) - - function onbeforeupdate() { - count++ - return true - } - - o(count).equals(1) - }) - - o("doesn't fire on recycled nodes", function() { - var onbeforeupdate = o.spy() - var vnodes = [m("div", {key: 1})] - var temp = [] - var updated = [m("div", {key: 1, onbeforeupdate: onbeforeupdate})] - - render(root, vnodes) - render(root, temp) - render(root, updated) - - 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) - }) - - components.forEach(function(cmp){ - o.spec(cmp.kind, function(){ - var createComponent = cmp.create - - o("prevents update in component", function() { - var component = createComponent({ - onbeforeupdate: function() {return false}, - view: function(vnode) { - return m("div", vnode.children) - }, - }) - var vnode = m(component, "a") - var updated = m(component, "b") - - render(root, vnode) - render(root, updated) - - o(root.firstChild.firstChild.nodeValue).equals("a") - }) - - o("prevents update if returning false in component and false in vnode", function() { - var component = createComponent({ - onbeforeupdate: function() {return false}, - view: function(vnode) { - return m("div", {id: vnode.attrs.id}) - }, - }) - var vnode = m(component, {id: "a", onbeforeupdate: function() {return false}}) - var updated = m(component, {id: "b", onbeforeupdate: function() {return false}}) - - render(root, vnode) - render(root, updated) - - o(root.firstChild.attributes["id"].value).equals("a") - }) - - o("does not prevent update if returning true in component and true in vnode", function() { - var component = createComponent({ - onbeforeupdate: function() {return true}, - view: function(vnode) { - return m("div", {id: vnode.attrs.id}) - }, - }) - var vnode = m(component, {id: "a", onbeforeupdate: function() {return true}}) - var updated = m(component, {id: "b", onbeforeupdate: function() {return true}}) - - render(root, vnode) - render(root, updated) - - o(root.firstChild.attributes["id"].value).equals("b") - }) - - o("prevents update if returning false in component but true in vnode", function() { - var component = createComponent({ - onbeforeupdate: function() {return false}, - view: function(vnode) { - return m("div", {id: vnode.attrs.id}) - }, - }) - var vnode = m(component, {id: "a", onbeforeupdate: function() {return true}}) - var updated = m(component, {id: "b", onbeforeupdate: function() {return true}}) - - render(root, vnode) - render(root, updated) - - o(root.firstChild.attributes["id"].value).equals("a") - }) - - o("prevents update if returning true in component but false in vnode", function() { - var component = createComponent({ - onbeforeupdate: function() {return true}, - view: function(vnode) { - return m("div", {id: vnode.attrs.id}) - }, - }) - var vnode = m(component, {id: "a", onbeforeupdate: function() {return false}}) - var updated = m(component, {id: "b", onbeforeupdate: function() {return false}}) - - render(root, vnode) - render(root, updated) - - o(root.firstChild.attributes["id"].value).equals("a") - }) - - o("does not prevent update if returning true from component", function() { - var component = createComponent({ - onbeforeupdate: function() {return true}, - view: function(vnode) { - return m("div", vnode.attrs) - }, - }) - var vnode = m(component, {id: "a"}) - var updated = m(component, {id: "b"}) - - render(root, vnode) - render(root, updated) - - o(root.firstChild.attributes["id"].value).equals("b") - }) - - o("accepts arguments for comparison in component", function() { - var component = createComponent({ - onbeforeupdate: onbeforeupdate, - view: function(vnode) { - return m("div", vnode.attrs) - }, - }) - var count = 0 - var vnode = m(component, {id: "a"}) - var updated = m(component, {id: "b"}) - - render(root, vnode) - render(root, updated) - - function onbeforeupdate(vnode, old) { - count++ - - o(old.attrs.id).equals("a") - o(vnode.attrs.id).equals("b") - - return old.attrs.id !== vnode.attrs.id - } - - o(count).equals(1) - o(root.firstChild.attributes["id"].value).equals("b") - }) - - o("is not called on component creation", function() { - createComponent({ - onbeforeupdate: onbeforeupdate, - view: function(vnode) { - return m("div", vnode.attrs) - }, - }) - - var count = 0 - var vnode = m("div", {id: "a"}) - - render(root, vnode) - - function onbeforeupdate() { - count++ - return true - } - - o(count).equals(0) - }) - - o("is called only once on component update", function() { - var component = createComponent({ - onbeforeupdate: onbeforeupdate, - view: function(vnode) { - return m("div", vnode.attrs) - }, - }) - - var count = 0 - var vnode = m(component, {id: "a"}) - var updated = m(component, {id: "b"}) - - render(root, vnode) - render(root, updated) - - function onbeforeupdate() { - count++ - return true - } - - o(count).equals(1) - }) - }) - }) - - // https://github.com/MithrilJS/mithril.js/issues/2067 - o.spec("after prevented update", function() { - o("old attributes are retained", function() { - render(root, [ - m("div", {"id": "foo", onbeforeupdate: function() { return true }}) - ]) - render(root, [ - m("div", {"id": "bar", onbeforeupdate: function() { return false }}) - ]) - render(root, [ - m("div", {"id": "bar", onbeforeupdate: function() { return true }}) - ]) - o(root.firstChild.attributes["id"].value).equals("bar") - }) - o("old children is retained", function() { - render(root, - m("div", {onbeforeupdate: function() { return true }}, - m("div") - ) - ) - render(root, - m("div", {onbeforeupdate: function() { return false }}, - m("div", m("div")) - ) - ) - render(root, - m("div", {onbeforeupdate: function() { return true }}, - m("div", m("div")) - ) - ) - o(root.firstChild.firstChild.childNodes.length).equals(1) - }) - o("old text is retained", function() { - render(root, - m("div", {onbeforeupdate: function() { return true }}, - m("div") - ) - ) - render(root, - m("div", {onbeforeupdate: function() { return false }}, - m("div", "foo") - ) - ) - render(root, - m("div", {onbeforeupdate: function() { return true }}, - m("div", "foo") - ) - ) - o(root.firstChild.firstChild.firstChild.nodeValue).equals("foo") - }) - o("updating component children doesn't error", function() { - var Child = { - view(v) { - return m("div", - v.attrs.foo ? m("div") : null - ) - } - } - - render(root, - m("div", {onbeforeupdate: function() { return true }}, - m(Child, {foo: false}) - ) - ) - render(root, - m("div", {onbeforeupdate: function() { return false }}, - m(Child, {foo: false}) - ) - ) - render(root, - m("div", {onbeforeupdate: function() { return true }}, - m(Child, {foo: true}) - ) - ) - o(root.firstChild.firstChild.childNodes.length).equals(1) - }) - o("adding dom children doesn't error", function() { - render(root, - m("div", {onbeforeupdate: function() { return true }}, - m("div") - ) - ) - render(root, - m("div", {onbeforeupdate: function() { return false }}, - m("div") - ) - ) - render(root, - m("div", {onbeforeupdate: function() { return true }}, - m("div", m("div")) - ) - ) - o(root.firstChild.firstChild.childNodes.length).equals(1) - }) - }) -}) diff --git a/render/tests/test-oncreate.js b/render/tests/test-oncreate.js deleted file mode 100644 index 21f25bea8..000000000 --- a/render/tests/test-oncreate.js +++ /dev/null @@ -1,211 +0,0 @@ -"use strict" - -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") -var m = require("../../render/hyperscript") -var fragment = require("../../render/fragment") - -o.spec("oncreate", function() { - var $window, root, render - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - render = vdom($window) - }) - - o("calls oncreate when creating element", function() { - var callback = o.spy() - var vnode = m("div", {oncreate: callback}) - - render(root, vnode) - - o(callback.callCount).equals(1) - o(callback.this).equals(vnode.state) - o(callback.args[0]).equals(vnode) - }) - o("calls oncreate when creating fragment", function() { - var callback = o.spy() - var vnode = fragment({oncreate: callback}) - - render(root, vnode) - - o(callback.callCount).equals(1) - o(callback.this).equals(vnode.state) - o(callback.args[0]).equals(vnode) - }) - o("calls oncreate when replacing keyed", function() { - var createDiv = o.spy() - var createA = o.spy() - var vnode = m("div", {key: 1, oncreate: createDiv}) - var updated = m("a", {key: 1, oncreate: createA}) - - render(root, vnode) - render(root, updated) - - o(createDiv.callCount).equals(1) - o(createDiv.this).equals(vnode.state) - o(createDiv.args[0]).equals(vnode) - o(createA.callCount).equals(1) - o(createA.this).equals(updated.state) - o(createA.args[0]).equals(updated) - }) - o("does not call oncreate when noop", function() { - var create = o.spy() - var update = o.spy() - var vnode = m("div", {oncreate: create}) - var updated = m("div", {oncreate: update}) - - render(root, vnode) - render(root, updated) - - o(create.callCount).equals(1) - o(create.this).equals(vnode.state) - o(create.args[0]).equals(vnode) - o(update.callCount).equals(0) - }) - o("does not call oncreate when updating attr", function() { - var create = o.spy() - var update = o.spy() - var vnode = m("div", {oncreate: create}) - var updated = m("div", {oncreate: update, id: "a"}) - - render(root, vnode) - render(root, updated) - - o(create.callCount).equals(1) - o(create.this).equals(vnode.state) - o(create.args[0]).equals(vnode) - o(update.callCount).equals(0) - }) - o("does not call oncreate when updating children", function() { - var create = o.spy() - var update = o.spy() - var vnode = m("div", {oncreate: create}, m("a")) - var updated = m("div", {oncreate: update}, m("b")) - - render(root, vnode) - render(root, updated) - - o(create.callCount).equals(1) - o(create.this).equals(vnode.state) - o(create.args[0]).equals(vnode) - o(update.callCount).equals(0) - }) - o("does not call oncreate when updating keyed", function() { - var create = o.spy() - var update = o.spy() - var vnode = m("div", {key: 1, oncreate: create}) - var otherVnode = m("a", {key: 2}) - var updated = m("div", {key: 1, oncreate: update}) - var otherUpdated = m("a", {key: 2}) - - render(root, [vnode, otherVnode]) - render(root, [otherUpdated, updated]) - - o(create.callCount).equals(1) - o(create.this).equals(vnode.state) - o(create.args[0]).equals(vnode) - o(update.callCount).equals(0) - }) - o("does not call oncreate when removing", function() { - var create = o.spy() - var vnode = m("div", {oncreate: create}) - - render(root, vnode) - render(root, []) - - o(create.callCount).equals(1) - o(create.this).equals(vnode.state) - o(create.args[0]).equals(vnode) - }) - o("does not recycle when there's an oncreate", function() { - var create = o.spy() - var update = o.spy() - var vnode = m("div", {key: 1, oncreate: create}) - var updated = m("div", {key: 1, oncreate: update}) - - render(root, vnode) - render(root, []) - render(root, updated) - - o(vnode.dom).notEquals(updated.dom) - o(create.callCount).equals(1) - o(create.this).equals(vnode.state) - o(create.args[0]).equals(vnode) - o(update.callCount).equals(1) - o(update.this).equals(updated.state) - o(update.args[0]).equals(updated) - }) - o("calls oncreate at the same step as onupdate", function() { - var create = o.spy() - var update = o.spy() - var callback = o.spy() - var vnode = m("div", {onupdate: create}) - var updated = m("div", {onupdate: update}, m("a", {oncreate: callback})) - - render(root, vnode) - render(root, updated) - - o(create.callCount).equals(0) - o(update.callCount).equals(1) - o(update.this).equals(vnode.state) - o(update.args[0]).equals(updated) - o(callback.callCount).equals(1) - o(callback.this).equals(updated.children[0].state) - o(callback.args[0]).equals(updated.children[0]) - }) - o("calls oncreate on unkeyed that falls into reverse list diff code path", function() { - var create = o.spy() - render(root, m("p", m("div"))) - render(root, m("div", {oncreate: create}, m("div"))) - - o(create.callCount).equals(1) - }) - o("calls oncreate on unkeyed that falls into forward list diff code path", function() { - var create = o.spy() - render(root, [m("div"), m("p")]) - render(root, [m("div"), m("div", {oncreate: create})]) - - o(create.callCount).equals(1) - }) - o("calls oncreate after full DOM creation", function() { - var created = false - var vnode = m("div", - m("a", {oncreate: create}, - m("b") - ) - ) - - render(root, vnode) - - function create(vnode) { - created = true - - o(vnode.dom.parentNode).notEquals(null) - o(vnode.dom.childNodes.length).equals(1) - } - o(created).equals(true) - }) - o("does not set oncreate as an event handler", function() { - var create = o.spy() - var vnode = m("div", {oncreate: create}) - - render(root, vnode) - - o(vnode.dom.oncreate).equals(undefined) - o(vnode.dom.attributes["oncreate"]).equals(undefined) - }) - o("calls oncreate on recycle", function() { - var create = o.spy() - var vnodes = m("div", {key: 1, oncreate: create}) - var temp = [] - var updated = m("div", {key: 1, oncreate: create}) - - render(root, vnodes) - render(root, temp) - render(root, updated) - - o(create.callCount).equals(2) - }) -}) diff --git a/render/tests/test-oninit.js b/render/tests/test-oninit.js deleted file mode 100644 index b287fa1ea..000000000 --- a/render/tests/test-oninit.js +++ /dev/null @@ -1,207 +0,0 @@ -"use strict" - -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") -var m = require("../../render/hyperscript") -var fragment = require("../../render/fragment") - -o.spec("oninit", function() { - var $window, root, render - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - render = vdom($window) - }) - - o("calls oninit when creating element", function() { - var callback = o.spy() - var vnode = m("div", {oninit: callback}) - - render(root, vnode) - - o(callback.callCount).equals(1) - o(callback.this).equals(vnode.state) - o(callback.args[0]).equals(vnode) - }) - o("calls oninit when creating fragment", function() { - var callback = o.spy() - var vnode = fragment({oninit: callback}) - - render(root, vnode) - - o(callback.callCount).equals(1) - o(callback.this).equals(vnode.state) - o(callback.args[0]).equals(vnode) - }) - o("calls oninit when replacing keyed", function() { - var createDiv = o.spy() - var createA = o.spy() - var vnode = m("div", {key: 1, oninit: createDiv}) - var updated = m("a", {key: 1, oninit: createA}) - - render(root, vnode) - render(root, updated) - - o(createDiv.callCount).equals(1) - o(createDiv.this).equals(vnode.state) - o(createDiv.args[0]).equals(vnode) - o(createA.callCount).equals(1) - o(createA.this).equals(updated.state) - o(createA.args[0]).equals(updated) - }) - o("does not call oninit when noop", function() { - var create = o.spy() - var update = o.spy() - var vnode = m("div", {oninit: create}) - var updated = m("div", {oninit: update}) - - render(root, vnode) - render(root, updated) - - o(create.callCount).equals(1) - o(create.this).equals(vnode.state) - o(create.args[0]).equals(vnode) - o(update.callCount).equals(0) - }) - o("does not call oninit when updating attr", function() { - var create = o.spy() - var update = o.spy() - var vnode = m("div", {oninit: create}) - var updated = m("div", {oninit: update, id: "a"}) - - render(root, vnode) - render(root, updated) - - o(create.callCount).equals(1) - o(create.this).equals(vnode.state) - o(create.args[0]).equals(vnode) - o(update.callCount).equals(0) - }) - o("does not call oninit when updating children", function() { - var create = o.spy() - var update = o.spy() - var vnode = m("div", {oninit: create}, m("a")) - var updated = m("div", {oninit: update}, m("b")) - - render(root, vnode) - render(root, updated) - - o(create.callCount).equals(1) - o(create.this).equals(vnode.state) - o(create.args[0]).equals(vnode) - o(update.callCount).equals(0) - }) - o("does not call oninit when updating keyed", function() { - var create = o.spy() - var update = o.spy() - var vnode = m("div", {key: 1, oninit: create}) - var otherVnode = m("a", {key: 2}) - var updated = m("div", {key: 1, oninit: update}) - var otherUpdated = m("a", {key: 2}) - - render(root, [vnode, otherVnode]) - render(root, [otherUpdated, updated]) - - o(create.callCount).equals(1) - o(create.this).equals(vnode.state) - o(create.args[0]).equals(vnode) - o(update.callCount).equals(0) - }) - o("does not call oninit when removing", function() { - var create = o.spy() - var vnode = m("div", {oninit: create}) - - render(root, vnode) - render(root, []) - - o(create.callCount).equals(1) - o(create.this).equals(vnode.state) - o(create.args[0]).equals(vnode) - }) - o("calls oninit when recycling", function() { - var create = o.spy() - var update = o.spy() - var vnode = m("div", {key: 1, oninit: create}) - var updated = m("div", {key: 1, oninit: update}) - - render(root, vnode) - render(root, []) - render(root, updated) - - o(create.callCount).equals(1) - o(create.this).equals(vnode.state) - o(create.args[0]).equals(vnode) - o(update.callCount).equals(1) - o(update.this).equals(updated.state) - o(update.args[0]).equals(updated) - }) - o("calls oninit at the same step as onupdate", function() { - var create = o.spy() - var update = o.spy() - var callback = o.spy() - var vnode = m("div", {onupdate: create}) - var updated = m("div", {onupdate: update}, m("a", {oninit: callback})) - - render(root, vnode) - render(root, updated) - - o(create.callCount).equals(0) - o(update.callCount).equals(1) - o(update.this).equals(vnode.state) - o(update.args[0]).equals(updated) - o(callback.callCount).equals(1) - o(callback.this).equals(updated.children[0].state) - o(callback.args[0]).equals(updated.children[0]) - }) - o("calls oninit before full DOM creation", function() { - var called = false - var vnode = m("div", - m("a", {oninit: create}, - m("b") - ) - ) - - render(root, vnode) - - function create(vnode) { - called = true - - o(vnode.dom).equals(undefined) - o(root.childNodes.length).equals(1) - } - o(called).equals(true) - }) - o("does not set oninit as an event handler", function() { - var create = o.spy() - var vnode = m("div", {oninit: create}) - - render(root, vnode) - - 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, [ - m("p", {key: 1, oninit: oninit1}), - m("p", {key: 2, oninit: oninit2}), - m("p", {key: 3, oninit: oninit3}), - ]) - render(root, [ - m("p", {key: 1, oninit: oninit1}), - m("p", {key: 3, oninit: oninit3}), - ]) - render(root, [ - m("p", {key: 3, oninit: oninit3}), - ]) - - o(oninit1.callCount).equals(1) - o(oninit2.callCount).equals(1) - o(oninit3.callCount).equals(1) - }) -}) diff --git a/render/tests/test-onremove.js b/render/tests/test-onremove.js deleted file mode 100644 index bf4800ffd..000000000 --- a/render/tests/test-onremove.js +++ /dev/null @@ -1,379 +0,0 @@ -"use strict" - -var o = require("ospec") -var components = require("../../test-utils/components") -var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") -var m = require("../../render/hyperscript") -var fragment = require("../../render/fragment") - -o.spec("onremove", function() { - var $window, root, render - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - render = vdom($window) - }) - - o("does not call onremove when creating", function() { - var create = o.spy() - var update = o.spy() - var vnode = m("div", {onremove: create}) - var updated = m("div", {onremove: update}) - - render(root, vnode) - render(root, updated) - - o(create.callCount).equals(0) - }) - o("does not call onremove when updating", function() { - var create = o.spy() - var update = o.spy() - var vnode = m("div", {onremove: create}) - var updated = m("div", {onremove: update}) - - render(root, vnode) - render(root, updated) - - o(create.callCount).equals(0) - o(update.callCount).equals(0) - }) - o("calls onremove when removing element", function() { - var remove = o.spy() - var vnode = m("div", {onremove: remove}) - - render(root, vnode) - render(root, []) - - o(remove.callCount).equals(1) - o(remove.this).equals(vnode.state) - o(remove.args[0]).equals(vnode) - }) - o("calls onremove when removing fragment", function() { - var remove = o.spy() - var vnode = fragment({onremove: remove}) - - render(root, vnode) - render(root, []) - - o(remove.callCount).equals(1) - o(remove.this).equals(vnode.state) - o(remove.args[0]).equals(vnode) - }) - o("does not set onremove as an event handler", function() { - var remove = o.spy() - var vnode = m("div", {onremove: remove}) - - render(root, vnode) - - o(vnode.dom.onremove).equals(undefined) - o(vnode.dom.attributes["onremove"]).equals(undefined) - o(vnode.events).equals(undefined) - }) - o("calls onremove on keyed nodes", function() { - var remove = o.spy() - var vnodes = [m("div", {key: 1})] - var temp = [m("div", {key: 2, onremove: remove})] - var updated = [m("div", {key: 1})] - - render(root, vnodes) - 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() { - var remove = o.spy() - var vnode = m("div", {key: 1, onremove: remove}) - var updated = m("div", {key: 1, onremove: remove}) - - render(root, vnode) - render(root, []) - render(root, updated) - - o(vnode.dom).notEquals(updated.dom) - }) - components.forEach(function(cmp){ - o.spec(cmp.kind, function(){ - var createComponent = cmp.create - - o("calls onremove on nested component", function() { - var spy = o.spy() - var comp = createComponent({ - view: function() {return m(outer)} - }) - var outer = createComponent({ - view: function() {return m(inner)} - }) - var inner = createComponent({ - onremove: spy, - view: function() {return m("div")} - }) - render(root, m(comp)) - render(root, null) - - o(spy.callCount).equals(1) - }) - o("calls onremove on nested component child", function() { - var spy = o.spy() - var comp = createComponent({ - view: function() {return m(outer)} - }) - var outer = createComponent({ - view: function() {return m(inner, m("a", {onremove: spy}))} - }) - var inner = createComponent({ - view: function(vnode) {return m("div", vnode.children)} - }) - render(root, m(comp)) - render(root, null) - - o(spy.callCount).equals(1) - }) - o("doesn't call onremove on children when the corresponding view returns null (after removing the parent)", function() { - var threw = false - var spy = o.spy() - var parent = createComponent({ - view: function() {} - }) - var child = createComponent({ - view: function() {}, - onremove: spy - }) - render(root, m(parent, m(child))) - try { - render(root, null) - } catch (e) { - threw = e - } - - o(spy.callCount).equals(0) - o(threw).equals(false) - }) - o("doesn't call onremove on children when the corresponding view returns null (after removing the children)", function() { - var threw = false - var spy = o.spy() - var parent = createComponent({ - view: function() {} - }) - var child = createComponent({ - view: function() {}, - onremove: spy - }) - render(root, m(parent, m(child))) - try { - render(root, m(parent)) - } catch (e) { - threw = true - } - - 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 = m("div", {key: 1}, m("div", {onremove: onremove})) - var temp = m("div", {key: 2}) - var updated = m("div", {key: 1}, m("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) - }) - // Warning: this test is complicated because it's replicating a race condition. - o("removes correct nodes in fragment when child delays removal, parent removes, then child resolves", function () { - // Custom assertion - we need to test the entire tree for consistency. - - const template = (tpl) => (root) => { - var expected = [] - - for (var i = 0; i < tpl.length; i++) { - var name = tpl[i][0] - var text = tpl[i][1] - expected.push({ - name: name, - firstType: name === "#text" ? null : "#text", - text: text, - }) - } - - var actual = [] - var list = root.firstChild.childNodes - for (var i = 0; i < list.length; i++) { - var current = list[i] - var textNode = current.childNodes.length === 1 - ? current.firstChild - : current - actual.push({ - name: current.nodeName, - firstType: textNode === current ? null : textNode.nodeName, - text: textNode.nodeValue, - }) - } - actual = JSON.stringify(actual, null, " ") - expected = JSON.stringify(expected, null, " ") - return { - pass: actual === expected, - message: -`${expected} - expected, got -${actual}` - } - } - var finallyCB1 - var finallyCB2 - var C = createComponent({ - view({children}){return children}, - onbeforeremove(){ - return {then(){}, finally: function (fcb) { finallyCB1 = fcb }} - } - }) - function update(id, showParent, showChild) { - const removeParent = o.spy() - const removeSyncChild = o.spy() - const removeAsyncChild = o.spy() - - render(root, - m("div", - showParent && fragment( - {onremove: removeParent}, - m("a", {onremove: removeSyncChild}, "sync child"), - showChild && m(C, { - onbeforeremove: function () { - return {then(){}, finally: function (fcb) { finallyCB2 = fcb }} - }, - onremove: removeAsyncChild - }, m("div", id)) - ) - ) - ) - return {removeAsyncChild,removeParent, removeSyncChild} - } - - const hooks1 = update("1", true, true) - o(root).satisfies(template([ - ["A", "sync child"], - ["DIV", "1"], - ])) - o(finallyCB1).equals(undefined) - o(finallyCB2).equals(undefined) - - const hooks2 = update("2", true, false) - - o(root).satisfies(template([ - ["A", "sync child"], - ["DIV", "1"], - ])) - - o(typeof finallyCB1).equals("function") - o(typeof finallyCB2).equals("function") - - var original1 = finallyCB1 - var original2 = finallyCB2 - - const hooks3 = update("3", true, true) - - o(root).satisfies(template([ - ["A", "sync child"], - ["DIV", "1"], - ["DIV", "3"], - ])) - - o(hooks3.removeParent.callCount).equals(0) - o(hooks3.removeSyncChild.callCount).equals(0) - o(hooks3.removeAsyncChild.callCount).equals(0) - o(finallyCB1).equals(original1) - o(finallyCB2).equals(original2) - - const hooks4 = update("4", false, true) - - o(root).satisfies(template([ - ["DIV", "1"], - ])) - - o(hooks3.removeParent.callCount).equals(1) - o(hooks3.removeSyncChild.callCount).equals(1) - o(hooks3.removeAsyncChild.callCount).equals(1) - o(hooks3.removeParent.args[0].tag).equals("[") - o(finallyCB1).equals(original1) - o(finallyCB2).equals(original2) - - const hooks5 = update("5", true, true) - - - o(root).satisfies(template([ - ["DIV", "1"], - ["A", "sync child"], - ["DIV", "5"], - ])) - o(finallyCB1).equals(original1) - o(finallyCB2).equals(original2) - - o(hooks1.removeAsyncChild.callCount).equals(0) - - finallyCB1() - - o(hooks1.removeAsyncChild.callCount).equals(0) - - finallyCB2() - - o(hooks1.removeAsyncChild.callCount).equals(1) - - o(root).satisfies(template([ - ["A", "sync child"], - ["DIV", "5"], - ])) - o(finallyCB1).equals(original1) - o(finallyCB2).equals(original2) - - const hooks6 = update("6", true, true) - - o(root).satisfies(template([ - ["A", "sync child"], - ["DIV", "6"], - ])) - o(finallyCB1).equals(original1) - o(finallyCB2).equals(original2) - - // final tally - o(hooks1.removeParent.callCount).equals(0) - o(hooks1.removeSyncChild.callCount).equals(0) - o(hooks1.removeAsyncChild.callCount).equals(1) - - o(hooks2.removeParent.callCount).equals(0) - o(hooks2.removeSyncChild.callCount).equals(0) - o(hooks2.removeAsyncChild.callCount).equals(0) - - o(hooks3.removeParent.callCount).equals(1) - o(hooks3.removeSyncChild.callCount).equals(1) - o(hooks3.removeAsyncChild.callCount).equals(1) - - o(hooks4.removeParent.callCount).equals(0) - o(hooks4.removeSyncChild.callCount).equals(0) - o(hooks4.removeAsyncChild.callCount).equals(0) - - o(hooks5.removeParent.callCount).equals(0) - o(hooks5.removeSyncChild.callCount).equals(0) - o(hooks5.removeAsyncChild.callCount).equals(0) - - o(hooks6.removeParent.callCount).equals(0) - o(hooks6.removeSyncChild.callCount).equals(0) - o(hooks6.removeAsyncChild.callCount).equals(0) - - }) - }) - }) -}) diff --git a/render/tests/test-onupdate.js b/render/tests/test-onupdate.js deleted file mode 100644 index 436b2d42f..000000000 --- a/render/tests/test-onupdate.js +++ /dev/null @@ -1,162 +0,0 @@ -"use strict" - -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") -var m = require("../../render/hyperscript") -var fragment = require("../../render/fragment") - -o.spec("onupdate", function() { - var $window, root, render - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - render = vdom($window) - }) - - o("does not call onupdate when creating element", function() { - var create = o.spy() - var update = o.spy() - var vnode = m("div", {onupdate: create}) - var updated = m("div", {onupdate: update}) - - render(root, vnode) - render(root, updated) - - o(create.callCount).equals(0) - o(update.callCount).equals(1) - o(update.this).equals(vnode.state) - o(update.args[0]).equals(updated) - }) - o("does not call onupdate when removing element", function() { - var create = o.spy() - var vnode = m("div", {onupdate: create}) - - render(root, vnode) - render(root, []) - - o(create.callCount).equals(0) - }) - o("does not call onupdate when replacing keyed element", function() { - var create = o.spy() - var update = o.spy() - var vnode = m("div", {key: 1, onupdate: create}) - var updated = m("a", {key: 1, onupdate: update}) - render(root, vnode) - render(root, updated) - - o(create.callCount).equals(0) - o(update.callCount).equals(0) - }) - o("does not recycle when there's an onupdate", function() { - var update = o.spy() - var vnode = m("div", {key: 1, onupdate: update}) - var updated = m("div", {key: 1, onupdate: update}) - - render(root, vnode) - render(root, []) - render(root, updated) - - o(vnode.dom).notEquals(updated.dom) - }) - o("does not call old onupdate when removing the onupdate property in new vnode", function() { - var create = o.spy() - var vnode = m("a", {onupdate: create}) - var updated = m("a") - - render(root, vnode) - render(root, updated) - - o(create.callCount).equals(0) - }) - o("calls onupdate when noop", function() { - var create = o.spy() - var update = o.spy() - var vnode = m("div", {onupdate: create}) - var updated = m("div", {onupdate: update}) - - render(root, vnode) - render(root, updated) - - o(create.callCount).equals(0) - o(update.callCount).equals(1) - o(update.this).equals(vnode.state) - o(update.args[0]).equals(updated) - }) - o("calls onupdate when updating attr", function() { - var create = o.spy() - var update = o.spy() - var vnode = m("div", {onupdate: create}) - var updated = m("div", {onupdate: update, id: "a"}) - - render(root, vnode) - render(root, updated) - - o(create.callCount).equals(0) - o(update.callCount).equals(1) - o(update.this).equals(vnode.state) - o(update.args[0]).equals(updated) - }) - o("calls onupdate when updating children", function() { - var create = o.spy() - var update = o.spy() - var vnode = m("div", {onupdate: create}, m("a")) - var updated = m("div", {onupdate: update}, m("b")) - - render(root, vnode) - render(root, updated) - - o(create.callCount).equals(0) - o(update.callCount).equals(1) - o(update.this).equals(vnode.state) - o(update.args[0]).equals(updated) - }) - o("calls onupdate when updating fragment", function() { - var create = o.spy() - var update = o.spy() - var vnode = fragment({onupdate: create}) - var updated = fragment({onupdate: update}) - - render(root, vnode) - render(root, updated) - - o(create.callCount).equals(0) - o(update.callCount).equals(1) - o(update.this).equals(vnode.state) - o(update.args[0]).equals(updated) - }) - o("calls onupdate after full DOM update", function() { - var called = false - var vnode = m("div", {id: "1"}, - m("a", {id: "2"}, - m("b", {id: "3"}) - ) - ) - var updated = m("div", {id: "11"}, - m("a", {id: "22", onupdate: update}, - m("b", {id: "33"}) - ) - ) - - render(root, vnode) - render(root, updated) - - function update(vnode) { - called = true - - o(vnode.dom.parentNode.attributes["id"].value).equals("11") - o(vnode.dom.attributes["id"].value).equals("22") - o(vnode.dom.childNodes[0].attributes["id"].value).equals("33") - } - o(called).equals(true) - }) - o("does not set onupdate as an event handler", function() { - var update = o.spy() - var vnode = m("div", {onupdate: update}) - - render(root, vnode) - - o(vnode.dom.onupdate).equals(undefined) - o(vnode.dom.attributes["onupdate"]).equals(undefined) - }) -}) diff --git a/render/tests/test-render-hyperscript-integration.js b/render/tests/test-render-hyperscript-integration.js deleted file mode 100644 index 7a94eb879..000000000 --- a/render/tests/test-render-hyperscript-integration.js +++ /dev/null @@ -1,614 +0,0 @@ -"use strict" - -var o = require("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) - }) - 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") - }) - }) - }) -}) diff --git a/render/tests/test-render.js b/render/tests/test-render.js deleted file mode 100644 index 005828d50..000000000 --- a/render/tests/test-render.js +++ /dev/null @@ -1,394 +0,0 @@ -"use strict" - -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") -var m = require("../../render/hyperscript") - -o.spec("render", function() { - var $window, root, render - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - render = vdom($window) - }) - - o("initializes without DOM", function() { - vdom() - }) - - o("renders plain text", function() { - render(root, "a") - o(root.childNodes.length).equals(1) - o(root.childNodes[0].nodeValue).equals("a") - }) - - o("updates plain text", function() { - render(root, "a") - render(root, "b") - o(root.childNodes.length).equals(1) - o(root.childNodes[0].nodeValue).equals("b") - }) - - o("renders a number", function() { - render(root, 1) - o(root.childNodes.length).equals(1) - o(root.childNodes[0].nodeValue).equals("1") - }) - - o("updates a number", function() { - render(root, 1) - render(root, 2) - o(root.childNodes.length).equals(1) - o(root.childNodes[0].nodeValue).equals("2") - }) - - o("overwrites existing content", function() { - var vnodes = [] - - root.appendChild($window.document.createElement("div")); - - render(root, vnodes) - - o(root.childNodes.length).equals(0) - }) - - o("throws on invalid root node", function() { - var threw = false - try { - render(null, []) - } catch (e) { - threw = true - } - o(threw).equals(true) - }) - - o("does not enter infinite loop when oninit triggers render and view throws with an object literal component", function(done) { - var A = { - oninit: init, - view: function() {throw new Error("error")} - } - function run() { - render(root, m(A)) - } - function init() { - setTimeout(function() { - var threwInner = false - try {run()} catch (e) {threwInner = true} - - o(threwInner).equals(false) - done() - }, 0) - } - - var threwOuter = false - try {run()} catch (e) {threwOuter = true} - - o(threwOuter).equals(true) - }) - o("does not try to re-initialize a constructibe component whose view has thrown", function() { - var oninit = o.spy() - var onbeforeupdate = o.spy() - function A(){} - A.prototype.view = function() {throw new Error("error")} - A.prototype.oninit = oninit - A.prototype.onbeforeupdate = onbeforeupdate - var throwCount = 0 - - try {render(root, m(A))} catch (e) {throwCount++} - - o(throwCount).equals(1) - o(oninit.callCount).equals(1) - o(onbeforeupdate.callCount).equals(0) - - try {render(root, m(A))} catch (e) {throwCount++} - - o(throwCount).equals(1) - o(oninit.callCount).equals(1) - o(onbeforeupdate.callCount).equals(0) - }) - o("does not try to re-initialize a constructible component whose oninit has thrown", function() { - var oninit = o.spy(function(){throw new Error("error")}) - var onbeforeupdate = o.spy() - function A(){} - A.prototype.view = function(){} - A.prototype.oninit = oninit - A.prototype.onbeforeupdate = onbeforeupdate - var throwCount = 0 - - try {render(root, m(A))} catch (e) {throwCount++} - - o(throwCount).equals(1) - o(oninit.callCount).equals(1) - o(onbeforeupdate.callCount).equals(0) - - try {render(root, m(A))} catch (e) {throwCount++} - - o(throwCount).equals(1) - o(oninit.callCount).equals(1) - o(onbeforeupdate.callCount).equals(0) - }) - o("does not try to re-initialize a constructible component whose constructor has thrown", function() { - var oninit = o.spy() - var onbeforeupdate = o.spy() - function A(){throw new Error("error")} - A.prototype.view = function() {} - A.prototype.oninit = oninit - A.prototype.onbeforeupdate = onbeforeupdate - var throwCount = 0 - - try {render(root, m(A))} catch (e) {throwCount++} - - o(throwCount).equals(1) - o(oninit.callCount).equals(0) - o(onbeforeupdate.callCount).equals(0) - - try {render(root, m(A))} catch (e) {throwCount++} - - o(throwCount).equals(1) - o(oninit.callCount).equals(0) - o(onbeforeupdate.callCount).equals(0) - }) - o("does not try to re-initialize a closure component whose view has thrown", function() { - var oninit = o.spy() - var onbeforeupdate = o.spy() - function A() { - return { - view: function() {throw new Error("error")}, - oninit: oninit, - onbeforeupdate: onbeforeupdate - } - } - var throwCount = 0 - try {render(root, m(A))} catch (e) {throwCount++} - - o(throwCount).equals(1) - o(oninit.callCount).equals(1) - o(onbeforeupdate.callCount).equals(0) - - try {render(root, m(A))} catch (e) {throwCount++} - - o(throwCount).equals(1) - o(oninit.callCount).equals(1) - o(onbeforeupdate.callCount).equals(0) - }) - o("does not try to re-initialize a closure component whose oninit has thrown", function() { - var oninit = o.spy(function() {throw new Error("error")}) - var onbeforeupdate = o.spy() - function A() { - return { - view: function() {}, - oninit: oninit, - onbeforeupdate: onbeforeupdate - } - } - var throwCount = 0 - try {render(root, m(A))} catch (e) {throwCount++} - - o(throwCount).equals(1) - o(oninit.callCount).equals(1) - o(onbeforeupdate.callCount).equals(0) - - try {render(root, m(A))} catch (e) {throwCount++} - - o(throwCount).equals(1) - o(oninit.callCount).equals(1) - o(onbeforeupdate.callCount).equals(0) - }) - o("does not try to re-initialize a closure component whose closure has thrown", function() { - function A() { - throw new Error("error") - } - var throwCount = 0 - try {render(root, m(A))} catch (e) {throwCount++} - - o(throwCount).equals(1) - - try {render(root, m(A))} catch (e) {throwCount++} - - o(throwCount).equals(1) - }) - o("lifecycle methods work in keyed children of recycled keyed", function() { - var createA = o.spy() - var updateA = o.spy() - var removeA = o.spy() - var createB = o.spy() - var updateB = o.spy() - var removeB = o.spy() - var a = function() { - return m("div", {key: 1}, - m("div", {key: 11, oncreate: createA, onupdate: updateA, onremove: removeA}), - m("div", {key: 12}) - ) - } - var b = function() { - return m("div", {key: 2}, - m("div", {key: 21, oncreate: createB, onupdate: updateB, onremove: removeB}), - m("div", {key: 22}) - ) - } - render(root, a()) - render(root, b()) - render(root, a()) - - o(createA.callCount).equals(2) - o(updateA.callCount).equals(0) - o(removeA.callCount).equals(1) - o(createB.callCount).equals(1) - o(updateB.callCount).equals(0) - o(removeB.callCount).equals(1) - }) - o("lifecycle methods work in unkeyed children of recycled keyed", function() { - var createA = o.spy() - var updateA = o.spy() - var removeA = o.spy() - var createB = o.spy() - var updateB = o.spy() - var removeB = o.spy() - var a = function() { - return m("div", {key: 1}, - m("div", {oncreate: createA, onupdate: updateA, onremove: removeA}) - ) - } - var b = function() { - return m("div", {key: 2}, - m("div", {oncreate: createB, onupdate: updateB, onremove: removeB}) - ) - } - render(root, a()) - render(root, b()) - render(root, a()) - - o(createA.callCount).equals(2) - o(updateA.callCount).equals(0) - o(removeA.callCount).equals(1) - o(createB.callCount).equals(1) - o(updateB.callCount).equals(0) - o(removeB.callCount).equals(1) - }) - o("update lifecycle methods work on children of recycled keyed", function() { - var createA = o.spy() - var updateA = o.spy() - var removeA = o.spy() - var createB = o.spy() - var updateB = o.spy() - var removeB = o.spy() - - var a = function() { - return m("div", {key: 1}, - m("div", {oncreate: createA, onupdate: updateA, onremove: removeA}) - ) - } - var b = function() { - return m("div", {key: 2}, - m("div", {oncreate: createB, onupdate: updateB, onremove: removeB}) - ) - } - render(root, a()) - render(root, a()) - o(createA.callCount).equals(1) - o(updateA.callCount).equals(1) - o(removeA.callCount).equals(0) - - render(root, b()) - o(createA.callCount).equals(1) - o(updateA.callCount).equals(1) - o(removeA.callCount).equals(1) - - render(root, a()) - render(root, a()) - - o(createA.callCount).equals(2) - o(updateA.callCount).equals(2) - o(removeA.callCount).equals(1) - }) - o("svg namespace is preserved in keyed diff (#1820)", function(){ - // note that this only exerciese one branch of the keyed diff algo - var svg = m("svg", - m("g", {key: 0}), - m("g", {key: 1}) - ) - render(root, svg) - - o(svg.dom.namespaceURI).equals("http://www.w3.org/2000/svg") - o(svg.dom.childNodes[0].namespaceURI).equals("http://www.w3.org/2000/svg") - o(svg.dom.childNodes[1].namespaceURI).equals("http://www.w3.org/2000/svg") - - svg = m("svg", - m("g", {key: 1, x: 1}), - m("g", {key: 2, x: 2}) - ) - render(root, svg) - - o(svg.dom.namespaceURI).equals("http://www.w3.org/2000/svg") - o(svg.dom.childNodes[0].namespaceURI).equals("http://www.w3.org/2000/svg") - o(svg.dom.childNodes[1].namespaceURI).equals("http://www.w3.org/2000/svg") - }) - o("the namespace of the root is passed to children", function() { - render(root, m("svg")) - o(root.childNodes[0].namespaceURI).equals("http://www.w3.org/2000/svg") - render(root.childNodes[0], m("g")) - o(root.childNodes[0].childNodes[0].namespaceURI).equals("http://www.w3.org/2000/svg") - }) - o("does not allow reentrant invocations", function() { - var thrown = [] - function A() { - var updated = false - try {render(root, m(A))} catch (e) {thrown.push("construct")} - return { - oninit: function() { - try {render(root, m(A))} catch (e) {thrown.push("oninit")} - }, - oncreate: function() { - try {render(root, m(A))} catch (e) {thrown.push("oncreate")} - }, - onbeforeupdate: function() { - try {render(root, m(A))} catch (e) {thrown.push("onbeforeupdate")} - }, - onupdate: function() { - if (updated) return - updated = true - try {render(root, m(A))} catch (e) {thrown.push("onupdate")} - }, - onbeforeremove: function() { - try {render(root, m(A))} catch (e) {thrown.push("onbeforeremove")} - }, - onremove: function() { - try {render(root, m(A))} catch (e) {thrown.push("onremove")} - }, - view: function() { - try {render(root, m(A))} catch (e) {thrown.push("view")} - }, - } - } - render(root, m(A)) - o(thrown).deepEquals([ - "construct", - "oninit", - "view", - "oncreate", - ]) - render(root, m(A)) - o(thrown).deepEquals([ - "construct", - "oninit", - "view", - "oncreate", - "onbeforeupdate", - "view", - "onupdate", - ]) - render(root, []) - o(thrown).deepEquals([ - "construct", - "oninit", - "view", - "oncreate", - "onbeforeupdate", - "view", - "onupdate", - "onbeforeremove", - "onremove", - ]) - }) -}) diff --git a/render/tests/test-textContent.js b/render/tests/test-textContent.js deleted file mode 100644 index 31d911753..000000000 --- a/render/tests/test-textContent.js +++ /dev/null @@ -1,197 +0,0 @@ -"use strict" - -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") -var m = require("../../render/hyperscript") - -o.spec("textContent", function() { - var $window, root, render - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - render = vdom($window) - }) - - o("ignores null", function() { - var vnode = m("a", null) - - render(root, vnode) - - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(0) - o(vnode.dom).equals(root.childNodes[0]) - }) - o("ignores undefined", function() { - var vnode = m("a", undefined) - - render(root, vnode) - - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(0) - o(vnode.dom).equals(root.childNodes[0]) - }) - o("creates string", function() { - var vnode = m("a", "a") - - render(root, vnode) - - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(1) - o(vnode.dom.childNodes[0].nodeValue).equals("a") - o(vnode.dom).equals(root.childNodes[0]) - }) - o("creates falsy string", function() { - var vnode = m("a", "") - - render(root, vnode) - - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(1) - o(vnode.dom.childNodes[0].nodeValue).equals("") - o(vnode.dom).equals(root.childNodes[0]) - }) - o("creates number", function() { - var vnode = m("a", 1) - - render(root, vnode) - - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(1) - o(vnode.dom.childNodes[0].nodeValue).equals("1") - o(vnode.dom).equals(root.childNodes[0]) - }) - o("creates falsy number", function() { - var vnode = m("a", 0) - - render(root, vnode) - - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(1) - o(vnode.dom.childNodes[0].nodeValue).equals("0") - o(vnode.dom).equals(root.childNodes[0]) - }) - o("creates boolean", function() { - var vnode = m("a", true) - - render(root, vnode) - - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(0) - o(vnode.dom).equals(root.childNodes[0]) - }) - o("creates falsy boolean", function() { - var vnode = m("a", false) - - render(root, vnode) - - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(0) - o(vnode.dom).equals(root.childNodes[0]) - }) - o("updates to string", function() { - var vnode = m("a", "a") - var updated = m("a", "b") - - render(root, vnode) - render(root, updated) - - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(1) - o(vnode.dom.childNodes[0].nodeValue).equals("b") - o(updated.dom).equals(root.childNodes[0]) - }) - o("updates to falsy string", function() { - var vnode = m("a", "a") - var updated = m("a", "") - - render(root, vnode) - render(root, updated) - - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(1) - o(vnode.dom.childNodes[0].nodeValue).equals("") - o(updated.dom).equals(root.childNodes[0]) - }) - o("updates to number", function() { - var vnode = m("a", "a") - var updated = m("a", 1) - - render(root, vnode) - render(root, updated) - - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(1) - o(vnode.dom.childNodes[0].nodeValue).equals("1") - o(updated.dom).equals(root.childNodes[0]) - }) - o("updates to falsy number", function() { - var vnode = m("a", "a") - var updated = m("a", 0) - - render(root, vnode) - render(root, updated) - - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(1) - o(vnode.dom.childNodes[0].nodeValue).equals("0") - o(updated.dom).equals(root.childNodes[0]) - }) - o("updates true to nothing", function() { - var vnode = m("a", "a") - var updated = m("a", true) - - render(root, vnode) - render(root, updated) - - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(0) - o(updated.dom).equals(root.childNodes[0]) - }) - o("updates false to nothing", function() { - var vnode = m("a", "a") - var updated = m("a", false) - - render(root, vnode) - render(root, updated) - - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(0) - o(updated.dom).equals(root.childNodes[0]) - }) - o("updates with typecasting", function() { - var vnode = m("a", "1") - var updated = m("a", 1) - - render(root, vnode) - render(root, updated) - - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(1) - o(vnode.dom.childNodes[0].nodeValue).equals("1") - o(updated.dom).equals(root.childNodes[0]) - }) - o("updates from without text to with text", function() { - var vnode = m("a") - var updated = m("a", "b") - - render(root, vnode) - render(root, updated) - - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(1) - o(vnode.dom.childNodes[0].nodeValue).equals("b") - o(updated.dom).equals(root.childNodes[0]) - }) - o("updates from with text to without text", function() { - var vnode = m("a", "a") - var updated = m("a") - - render(root, vnode) - render(root, updated) - - o(root.childNodes.length).equals(1) - o(vnode.dom.childNodes.length).equals(0) - o(updated.dom).equals(root.childNodes[0]) - }) -}) diff --git a/render/tests/test-trust.js b/render/tests/test-trust.js deleted file mode 100644 index 304708cc6..000000000 --- a/render/tests/test-trust.js +++ /dev/null @@ -1,31 +0,0 @@ -"use strict" - -var o = require("ospec") -var trust = require("../../render/trust") - -o.spec("trust", function() { - o("works with html", function() { - var vnode = trust("") - - o(vnode.tag).equals("<") - o(vnode.children).equals("") - }) - o("works with text", function() { - var vnode = trust("abc") - - o(vnode.tag).equals("<") - o(vnode.children).equals("abc") - }) - o("casts null to empty string", function() { - var vnode = trust(null) - - o(vnode.tag).equals("<") - o(vnode.children).equals("") - }) - o("casts undefined to empty string", function() { - var vnode = trust(undefined) - - o(vnode.tag).equals("<") - o(vnode.children).equals("") - }) -}) diff --git a/render/tests/test-updateElement.js b/render/tests/test-updateElement.js deleted file mode 100644 index eb41e06f4..000000000 --- a/render/tests/test-updateElement.js +++ /dev/null @@ -1,322 +0,0 @@ -"use strict" - -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") -var m = require("../../render/hyperscript") - -o.spec("updateElement", function() { - var $window, root, render - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - render = vdom($window) - }) - - o("updates attr", function() { - var vnode = m("a", {id: "b"}) - var updated = m("a", {id: "c"}) - - render(root, vnode) - render(root, updated) - - o(updated.dom).equals(vnode.dom) - o(updated.dom).equals(root.firstChild) - o(updated.dom.attributes["id"].value).equals("c") - }) - o("adds attr", function() { - var vnode = m("a", {id: "b"}) - var updated = m("a", {id: "c", title: "d"}) - - render(root, vnode) - render(root, updated) - - o(updated.dom).equals(vnode.dom) - o(updated.dom).equals(root.firstChild) - o(updated.dom.attributes["title"].value).equals("d") - }) - o("adds attr from empty attrs", function() { - var vnode = m("a") - var updated = m("a", {title: "d"}) - - render(root, vnode) - render(root, updated) - - o(updated.dom).equals(vnode.dom) - o(updated.dom).equals(root.firstChild) - o(updated.dom.attributes["title"].value).equals("d") - }) - o("removes attr", function() { - var vnode = m("a", {id: "b", title: "d"}) - var updated = m("a", {id: "c"}) - - render(root, vnode) - render(root, updated) - - o(updated.dom).equals(vnode.dom) - o(updated.dom).equals(root.firstChild) - o("title" in updated.dom.attributes).equals(false) - }) - o("removes class", function() { - var vnode = m("a", {id: "b", className: "d"}) - var updated = m("a", {id: "c"}) - - render(root, vnode) - render(root, updated) - - o(updated.dom).equals(vnode.dom) - o(updated.dom).equals(root.firstChild) - o("class" in updated.dom.attributes).equals(false) - }) - o("creates style object", function() { - var vnode = m("a") - var updated = m("a", {style: {backgroundColor: "green"}}) - - render(root, vnode) - render(root, updated) - - o(updated.dom.style.backgroundColor).equals("green") - }) - o("creates style string", function() { - var vnode = m("a") - var updated = m("a", {style: "background-color:green"}) - - render(root, vnode) - render(root, updated) - - o(updated.dom.style.backgroundColor).equals("green") - }) - o("updates style from object to object", function() { - var vnode = m("a", {style: {backgroundColor: "red"}}) - var updated = m("a", {style: {backgroundColor: "green"}}) - - render(root, vnode) - render(root, updated) - - o(updated.dom.style.backgroundColor).equals("green") - }) - o("updates style from object to string", function() { - var vnode = m("a", {style: {backgroundColor: "red"}}) - var updated = m("a", {style: "background-color:green;"}) - - render(root, vnode) - render(root, updated) - - o(updated.dom.style.backgroundColor).equals("green") - }) - o("handles noop style change when style is string", function() { - var vnode = m("a", {style: "background-color:green;"}) - var updated = m("a", {style: "background-color:green;"}) - - render(root, vnode) - render(root, updated) - - o(updated.dom.style.backgroundColor).equals("green") - }) - o("handles noop style change when style is object", function() { - var vnode = m("a", {style: {backgroundColor: "red"}}) - var updated = m("a", {style: {backgroundColor: "red"}}) - - render(root, vnode) - render(root, updated) - - o(updated.dom.style.backgroundColor).equals("red") - }) - o("updates style from string to object", function() { - var vnode = m("a", {style: "background-color:red;"}) - var updated = m("a", {style: {backgroundColor: "green"}}) - - render(root, vnode) - render(root, updated) - - o(updated.dom.style.backgroundColor).equals("green") - }) - o("updates style from string to string", function() { - var vnode = m("a", {style: "background-color:red;"}) - var updated = m("a", {style: "background-color:green;"}) - - render(root, vnode) - render(root, updated) - - o(updated.dom.style.backgroundColor).equals("green") - }) - o("removes style from object to object", function() { - var vnode = m("a", {style: {backgroundColor: "red", border: "1px solid red"}}) - var updated = m("a", {style: {backgroundColor: "red"}}) - - render(root, vnode) - render(root, updated) - - o(updated.dom.style.backgroundColor).equals("red") - o(updated.dom.style.border).equals("") - }) - o("removes style from string to object", function() { - var vnode = m("a", {style: "background-color:red;border:1px solid red"}) - var updated = m("a", {style: {backgroundColor: "red"}}) - - render(root, vnode) - render(root, updated) - - o(updated.dom.style.backgroundColor).equals("red") - o(updated.dom.style.border).notEquals("1px solid red") - }) - o("removes style from object to string", function() { - var vnode = m("a", {style: {backgroundColor: "red", border: "1px solid red"}}) - var updated = m("a", {style: "background-color:red"}) - - render(root, vnode) - render(root, updated) - - o(updated.dom.style.backgroundColor).equals("red") - o(updated.dom.style.border).equals("") - }) - o("removes style from string to string", function() { - var vnode = m("a", {style: "background-color:red;border:1px solid red"}) - var updated = m("a", {style: "background-color:red"}) - - render(root, vnode) - render(root, updated) - - o(updated.dom.style.backgroundColor).equals("red") - o(updated.dom.style.border).equals("") - }) - o("does not re-render element styles for equivalent style objects", function() { - var style = {color: "gold"} - var vnode = m("a", {style: style}) - - render(root, vnode) - - root.firstChild.style.color = "red" - style = {color: "gold"} - var updated = m("a", {style: style}) - render(root, updated) - - o(updated.dom.style.color).equals("red") - }) - o("setting style to `null` removes all styles", function() { - var vnode = m("p", {style: "background-color: red"}) - var updated = m("p", {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 = m("p", {style: "background-color: red"}) - var updated = m("p", {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 = m("p", {style: "background-color: red"}) - var updated = m("p") - - 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 = m("a") - var updated = m("b") - - render(root, vnode) - render(root, updated) - - o(updated.dom).equals(root.firstChild) - o(updated.dom.nodeName).equals("B") - }) - o("updates svg class", function() { - var vnode = m("svg", {className: "a"}) - var updated = m("svg", {className: "b"}) - - render(root, vnode) - render(root, updated) - - o(updated.dom.attributes["class"].value).equals("b") - }) - o("updates svg child", function() { - var vnode = m("svg", m("circle")) - var updated = m("svg", m("line")) - - render(root, vnode) - render(root, updated) - - o(updated.dom.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") - }) - o("doesn't restore since we're not recycling", function() { - var vnode = m("div", {key: 1}) - var updated = m("div", {key: 2}) - - render(root, vnode) - var a = vnode.dom - - render(root, updated) - - render(root, vnode) - var c = vnode.dom - - o(root.childNodes.length).equals(1) - o(a).notEquals(c) // this used to be a recycling pool test - }) - o("doesn't restore since we're not recycling (via map)", function() { - var a = m("div", {key: 1}) - var b = m("div", {key: 2}) - var c = m("div", {key: 3}) - var d = m("div", {key: 4}) - var e = m("div", {key: 5}) - var f = m("div", {key: 6}) - - render(root, [a, b, c]) - var x = root.childNodes[1] - - render(root, d) - - render(root, [e, b, f]) - var y = root.childNodes[1] - - o(root.childNodes.length).equals(3) - o(x).notEquals(y) // this used to be a recycling pool test - }) -}) diff --git a/render/tests/test-updateFragment.js b/render/tests/test-updateFragment.js deleted file mode 100644 index e1274fd14..000000000 --- a/render/tests/test-updateFragment.js +++ /dev/null @@ -1,71 +0,0 @@ -"use strict" - -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") -var m = require("../../render/hyperscript") -var fragment = require("../../render/fragment") - -o.spec("updateFragment", function() { - var $window, root, render - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - render = vdom($window) - }) - - o("updates fragment", function() { - var vnode = fragment(m("a")) - var updated = fragment(m("b")) - - render(root, vnode) - render(root, updated) - - o(updated.dom).equals(root.firstChild) - o(updated.dom.nodeName).equals("B") - }) - o("adds els", function() { - var vnode = fragment() - var updated = fragment(m("a"), m("b")) - - render(root, vnode) - render(root, updated) - - o(updated.dom).equals(root.firstChild) - o(updated.domSize).equals(2) - o(root.childNodes.length).equals(2) - o(root.childNodes[0].nodeName).equals("A") - o(root.childNodes[1].nodeName).equals("B") - }) - o("removes els", function() { - var vnode = fragment(m("a"), m("b")) - var updated = fragment() - - render(root, vnode) - render(root, updated) - - o(updated.dom).equals(null) - o(updated.domSize).equals(0) - o(root.childNodes.length).equals(0) - }) - o("updates from childless fragment", function() { - var vnode = fragment() - var updated = fragment(m("a")) - - render(root, vnode) - render(root, updated) - - o(updated.dom).equals(root.firstChild) - o(updated.dom.nodeName).equals("A") - }) - o("updates to childless fragment", function() { - var vnode = fragment(m("a")) - var updated = fragment() - - render(root, vnode) - render(root, updated) - - o(updated.dom).equals(null) - o(root.childNodes.length).equals(0) - }) -}) diff --git a/render/tests/test-updateHTML.js b/render/tests/test-updateHTML.js deleted file mode 100644 index c0f9a6c39..000000000 --- a/render/tests/test-updateHTML.js +++ /dev/null @@ -1,116 +0,0 @@ -"use strict" - -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") -var m = require("../../render/hyperscript") -var trust = require("../../render/trust") - -o.spec("updateHTML", function() { - var $window, root, render - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - render = vdom($window) - }) - - o("updates html", function() { - var vnode = trust("a") - var updated = trust("b") - - render(root, vnode) - render(root, updated) - - o(updated.dom).equals(root.firstChild) - o(updated.domSize).equals(1) - o(updated.dom.nodeValue).equals("b") - }) - o("adds html", function() { - var vnode = trust("") - var updated = trust("") - - render(root, vnode) - render(root, updated) - - o(updated.domSize).equals(2) - o(updated.dom).equals(root.firstChild) - o(root.childNodes.length).equals(2) - o(root.childNodes[0].nodeName).equals("A") - o(root.childNodes[1].nodeName).equals("B") - }) - o("removes html", function() { - var vnode = trust("") - var updated = trust("") - - render(root, vnode) - render(root, updated) - - o(updated.dom).equals(null) - o(updated.domSize).equals(0) - o(root.childNodes.length).equals(0) - }) - function childKeysOf(elem, key) { - var keys = key.split(".") - var result = [] - for (var i = 0; i < elem.childNodes.length; i++) { - var child = elem.childNodes[i] - for (var j = 0; j < keys.length; j++) child = child[keys[j]] - result.push(child) - } - return result - } - o("updates the dom correctly with a contenteditable parent", function() { - var div = m("div", {contenteditable: true}, trust("")) - - render(root, div) - o(childKeysOf(div.dom, "nodeName")).deepEquals(["A"]) - }) - o("updates dom with multiple text children", function() { - var vnode = ["a", trust(""), trust("")] - var replacement = ["a", trust(""), trust("")] - - render(root, vnode) - render(root, replacement) - - o(childKeysOf(root, "nodeName")).deepEquals(["#text", "C", "D"]) - }) - o("updates dom with multiple text children in other parents", function() { - var vnode = [ - m("div", "a", trust("")), - m("div", "b", trust("")), - ] - var replacement = [ - m("div", "c", trust("")), - m("div", "d", trust("")), - ] - - render(root, vnode) - render(root, replacement) - - o(childKeysOf(root, "nodeName")).deepEquals(["DIV", "DIV"]) - o(childKeysOf(root.childNodes[0], "nodeName")).deepEquals(["#text", "C"]) - o(root.childNodes[0].firstChild.nodeValue).equals("c") - o(childKeysOf(root.childNodes[1], "nodeName")).deepEquals(["#text", "D"]) - o(root.childNodes[1].firstChild.nodeValue).equals("d") - }) - o("correctly diffs if followed by another trusted vnode", function() { - render(root, [ - trust("A"), - trust("A"), - ]) - o(childKeysOf(root, "nodeName")).deepEquals(["SPAN", "SPAN"]) - o(childKeysOf(root, "firstChild.nodeValue")).deepEquals(["A", "A"]) - render(root, [ - trust("B"), - trust("A"), - ]) - o(childKeysOf(root, "nodeName")).deepEquals(["SPAN", "SPAN"]) - o(childKeysOf(root, "firstChild.nodeValue")).deepEquals(["B", "A"]) - render(root, [ - trust("B"), - trust("B"), - ]) - o(childKeysOf(root, "nodeName")).deepEquals(["SPAN", "SPAN"]) - o(childKeysOf(root, "firstChild.nodeValue")).deepEquals(["B", "B"]) - }) -}) diff --git a/render/tests/test-updateNodes.js b/render/tests/test-updateNodes.js deleted file mode 100644 index 3f1258148..000000000 --- a/render/tests/test-updateNodes.js +++ /dev/null @@ -1,1220 +0,0 @@ -"use strict" - -var o = require("ospec") -var components = require("../../test-utils/components") -var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") -var m = require("../../render/hyperscript") -var fragment = require("../../render/fragment") -var trust = require("../../render/trust") - -function vnodify(str) { - return str.split(",").map(function(k) {return m(k, {key: k})}) -} - -o.spec("updateNodes", function() { - var $window, root, render - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - render = vdom($window) - }) - - o("handles el noop", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2})] - var updated = [m("a", {key: 1}), m("b", {key: 2})] - - render(root, vnodes) - render(root, updated) - - o(root.childNodes.length).equals(2) - o(updated[0].dom.nodeName).equals("A") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("B") - o(updated[1].dom).equals(root.childNodes[1]) - }) - o("handles el noop without key", function() { - var vnodes = [m("a"), m("b")] - var updated = [m("a"), m("b")] - - render(root, vnodes) - render(root, updated) - - o(root.childNodes.length).equals(2) - o(updated[0].dom.nodeName).equals("A") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("B") - o(updated[1].dom).equals(root.childNodes[1]) - }) - o("handles text noop", function() { - var vnodes = "a" - var updated = "a" - - render(root, vnodes) - render(root, updated) - - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeValue).equals("a") - }) - o("handles text noop w/ type casting", function() { - var vnodes = 1 - var updated = "1" - - render(root, vnodes) - render(root, updated) - - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeValue).equals("1") - }) - o("handles falsy text noop w/ type casting", function() { - var vnodes = 0 - var updated = "0" - - render(root, vnodes) - render(root, updated) - - o(root.childNodes.length).equals(1) - o(root.childNodes[0].nodeValue).equals("0") - }) - o("handles html noop", function() { - var vnodes = trust("a") - var updated = trust("a") - - render(root, vnodes) - render(root, updated) - - o(root.childNodes.length).equals(1) - o(root.childNodes[0].nodeValue).equals("a") - o(updated.dom).equals(root.childNodes[0]) - }) - o("handles fragment noop", function() { - var vnodes = fragment(m("a")) - var updated = fragment(m("a")) - - render(root, vnodes) - render(root, updated) - - o(root.childNodes.length).equals(1) - o(updated.dom.nodeName).equals("A") - o(updated.dom).equals(root.childNodes[0]) - }) - o("handles fragment noop w/ text child", function() { - var vnodes = fragment("a") - var updated = fragment("a") - - render(root, vnodes) - render(root, updated) - - o(root.childNodes.length).equals(1) - o(updated.dom.nodeValue).equals("a") - o(updated.dom).equals(root.childNodes[0]) - }) - o("handles undefined to null noop", function() { - var vnodes = [null, m("div")] - var updated = [undefined, m("div")] - - render(root, vnodes) - render(root, updated) - - o(root.childNodes.length).equals(1) - }) - o("reverses els w/ even count", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2}), m("i", {key: 3}), m("s", {key: 4})] - var updated = [m("s", {key: 4}), m("i", {key: 3}), m("b", {key: 2}), m("a", {key: 1})] - - render(root, vnodes) - render(root, updated) - - o(root.childNodes.length).equals(4) - o(updated[0].dom.nodeName).equals("S") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("I") - o(updated[1].dom).equals(root.childNodes[1]) - o(updated[2].dom.nodeName).equals("B") - o(updated[2].dom).equals(root.childNodes[2]) - o(updated[3].dom.nodeName).equals("A") - o(updated[3].dom).equals(root.childNodes[3]) - }) - o("reverses els w/ odd count", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2}), m("i", {key: 3})] - var updated = [m("i", {key: 3}), m("b", {key: 2}), m("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[1].dom.nodeName).equals("B") - o(updated[2].dom.nodeName).equals("A") - o(tagNames).deepEquals(expectedTags) - }) - o("creates el at start", function() { - var vnodes = [m("a", {key: 1})] - var updated = [m("b", {key: 2}), m("a", {key: 1})] - - render(root, vnodes) - render(root, updated) - - o(root.childNodes.length).equals(2) - 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("creates el at end", function() { - var vnodes = [m("a", {key: 1})] - var updated = [m("a", {key: 1}), m("b", {key: 2})] - - render(root, vnodes) - render(root, updated) - - o(root.childNodes.length).equals(2) - o(updated[0].dom.nodeName).equals("A") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("B") - o(updated[1].dom).equals(root.childNodes[1]) - }) - o("creates el in middle", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2})] - var updated = [m("a", {key: 1}), m("i", {key: 3}), m("b", {key: 2})] - - render(root, vnodes) - render(root, updated) - - o(updated[0].dom.nodeName).equals("A") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("I") - o(updated[1].dom).equals(root.childNodes[1]) - o(updated[2].dom.nodeName).equals("B") - o(updated[2].dom).equals(root.childNodes[2]) - }) - o("creates el while reversing", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2})] - var updated = [m("b", {key: 2}), m("i", {key: 3}), m("a", {key: 1})] - - 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("I") - o(updated[1].dom).equals(root.childNodes[1]) - o(updated[2].dom.nodeName).equals("A") - o(updated[2].dom).equals(root.childNodes[2]) - }) - o("deletes el at start", function() { - var vnodes = [m("b", {key: 2}), m("a", {key: 1})] - var updated = [m("a", {key: 1})] - - render(root, vnodes) - render(root, updated) - - o(root.childNodes.length).equals(1) - o(updated[0].dom.nodeName).equals("A") - o(updated[0].dom).equals(root.childNodes[0]) - }) - o("deletes el at end", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2})] - var updated = [m("a", {key: 1})] - - render(root, vnodes) - render(root, updated) - - o(root.childNodes.length).equals(1) - o(updated[0].dom.nodeName).equals("A") - o(updated[0].dom).equals(root.childNodes[0]) - }) - o("deletes el at middle", function() { - var vnodes = [m("a", {key: 1}), m("i", {key: 3}), m("b", {key: 2})] - var updated = [m("a", {key: 1}), m("b", {key: 2})] - - render(root, vnodes) - render(root, updated) - - o(root.childNodes.length).equals(2) - o(updated[0].dom.nodeName).equals("A") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("B") - o(updated[1].dom).equals(root.childNodes[1]) - }) - o("deletes el while reversing", function() { - var vnodes = [m("a", {key: 1}), m("i", {key: 3}), m("b", {key: 2})] - var updated = [m("b", {key: 2}), m("a", {key: 1})] - - render(root, vnodes) - render(root, updated) - - o(root.childNodes.length).equals(2) - 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("creates, deletes, reverses els at same time", function() { - var vnodes = [m("a", {key: 1}), m("i", {key: 3}), m("b", {key: 2})] - var updated = [m("b", {key: 2}), m("a", {key: 1}), m("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("creates, deletes, reverses els at same time with '__proto__' key", function() { - var vnodes = [m("a", {key: "__proto__"}), m("i", {key: 3}), m("b", {key: 2})] - var updated = [m("b", {key: 2}), m("a", {key: "__proto__"}), m("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 fragment followed by el", function() { - var vnodes = [fragment({key: 1}), m("b", {key: 2})] - var updated = [fragment({key: 1}, m("a")), m("b", {key: 2})] - - render(root, vnodes) - render(root, updated) - - o(root.childNodes.length).equals(2) - o(updated[0].children[0].dom.nodeName).equals("A") - o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("B") - o(updated[1].dom).equals(root.childNodes[1]) - }) - o("reverses followed by el", function() { - var vnodes = [fragment({key: 1}, m("a", {key: 2}), m("b", {key: 3})), m("i", {key: 4})] - var updated = [fragment({key: 1}, m("b", {key: 3}), m("a", {key: 2})), m("i", {key: 4})] - - render(root, vnodes) - render(root, updated) - - o(root.childNodes.length).equals(3) - o(updated[0].children[0].dom.nodeName).equals("B") - o(updated[0].children[0].dom).equals(root.childNodes[0]) - o(updated[0].children[1].dom.nodeName).equals("A") - o(updated[0].children[1].dom).equals(root.childNodes[1]) - o(updated[1].dom.nodeName).equals("I") - o(updated[1].dom).equals(root.childNodes[2]) - }) - o("updates empty fragment to html without key", function() { - var vnodes = fragment() - var updated = trust("") - - render(root, vnodes) - render(root, updated) - - o(root.childNodes.length).equals(2) - o(updated.dom.nodeName).equals("A") - o(updated.dom).equals(root.childNodes[0]) - o(updated.domSize).equals(2) - o(updated.dom.nextSibling.nodeName).equals("B") - o(updated.dom.nextSibling).equals(root.childNodes[1]) - }) - o("updates empty html to fragment without key", function() { - var vnodes = trust() - var updated = fragment(m("a"), m("b")) - - render(root, vnodes) - render(root, updated) - - o(root.childNodes.length).equals(2) - o(updated.dom.nodeName).equals("A") - o(updated.dom).equals(root.childNodes[0]) - o(updated.domSize).equals(2) - o(updated.dom.nextSibling.nodeName).equals("B") - o(updated.dom.nextSibling).equals(root.childNodes[1]) - }) - o("updates fragment to html without key", function() { - var vnodes = fragment(m("a"), m("b")) - var updated = trust("") - - render(root, vnodes) - render(root, updated) - - o(root.childNodes.length).equals(2) - o(updated.dom.nodeName).equals("I") - o(updated.dom).equals(root.childNodes[0]) - o(updated.domSize).equals(2) - o(updated.dom.nextSibling.nodeName).equals("S") - o(updated.dom.nextSibling).equals(root.childNodes[1]) - }) - o("updates html to fragment without key", function() { - var vnodes = trust("") - var updated = fragment(m("i"), m("s")) - - render(root, vnodes) - render(root, updated) - - o(root.childNodes.length).equals(2) - o(updated.dom.nodeName).equals("I") - o(updated.dom).equals(root.childNodes[0]) - o(updated.domSize).equals(2) - o(updated.dom.nextSibling.nodeName).equals("S") - o(updated.dom.nextSibling).equals(root.childNodes[1]) - }) - o("populates fragment followed by el keyed", function() { - var vnodes = [fragment({key: 1}), m("i", {key: 2})] - var updated = [fragment({key: 1}, m("a"), m("b")), m("i", {key: 2})] - - render(root, vnodes) - render(root, updated) - - o(root.childNodes.length).equals(3) - o(updated[0].dom.nodeName).equals("A") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[0].domSize).equals(2) - o(updated[0].dom.nextSibling.nodeName).equals("B") - o(updated[0].dom.nextSibling).equals(root.childNodes[1]) - o(updated[1].dom.nodeName).equals("I") - o(updated[1].dom).equals(root.childNodes[2]) - }) - o("throws if fragment followed by null then el on first render keyed", function() { - var vnodes = [fragment({key: 1}), null, m("i", {key: 2})] - - o(function () { render(root, vnodes) }).throws(TypeError) - }) - o("throws if fragment followed by null then el on next render keyed", function() { - var vnodes = [fragment({key: 1}), m("i", {key: 2})] - var updated = [fragment({key: 1}, m("a"), m("b")), null, m("i", {key: 2})] - - render(root, vnodes) - o(function () { render(root, updated) }).throws(TypeError) - }) - o("populates childless fragment replaced followed by el keyed", function() { - var vnodes = [fragment({key: 1}), m("i", {key: 2})] - var updated = [fragment({key: 1}, m("a"), m("b")), m("i", {key: 2})] - - render(root, vnodes) - render(root, updated) - - o(root.childNodes.length).equals(3) - o(updated[0].dom.nodeName).equals("A") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[0].domSize).equals(2) - o(updated[0].dom.nextSibling.nodeName).equals("B") - o(updated[0].dom.nextSibling).equals(root.childNodes[1]) - o(updated[1].dom.nodeName).equals("I") - o(updated[1].dom).equals(root.childNodes[2]) - }) - o("throws if childless fragment replaced followed by null then el keyed", function() { - var vnodes = [fragment({key: 1}), m("i", {key: 2})] - var updated = [fragment({key: 1}, m("a"), m("b")), null, m("i", {key: 2})] - - render(root, vnodes) - o(function () { render(root, updated) }).throws(TypeError) - }) - o("moves from end to start", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2}), m("i", {key: 3}), m("s", {key: 4})] - var updated = [m("s", {key: 4}), m("a", {key: 1}), m("b", {key: 2}), m("i", {key: 3})] - - render(root, vnodes) - render(root, updated) - - o(root.childNodes.length).equals(4) - o(updated[0].dom.nodeName).equals("S") - 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("B") - o(updated[2].dom).equals(root.childNodes[2]) - o(updated[3].dom.nodeName).equals("I") - o(updated[3].dom).equals(root.childNodes[3]) - }) - o("moves from start to end", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2}), m("i", {key: 3}), m("s", {key: 4})] - var updated = [m("b", {key: 2}), m("i", {key: 3}), m("s", {key: 4}), m("a", {key: 1})] - - render(root, vnodes) - render(root, updated) - - o(root.childNodes.length).equals(4) - o(updated[0].dom.nodeName).equals("B") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("I") - o(updated[1].dom).equals(root.childNodes[1]) - o(updated[2].dom.nodeName).equals("S") - o(updated[2].dom).equals(root.childNodes[2]) - o(updated[3].dom.nodeName).equals("A") - o(updated[3].dom).equals(root.childNodes[3]) - }) - o("removes then recreate", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2}), m("i", {key: 3}), m("s", {key: 4})] - var temp = [] - var updated = [m("a", {key: 1}), m("b", {key: 2}), m("i", {key: 3}), m("s", {key: 4})] - - render(root, vnodes) - render(root, temp) - render(root, updated) - - o(root.childNodes.length).equals(4) - o(updated[0].dom.nodeName).equals("A") - 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("I") - o(updated[2].dom).equals(root.childNodes[2]) - o(updated[3].dom.nodeName).equals("S") - o(updated[3].dom).equals(root.childNodes[3]) - }) - o("removes then recreate reversed", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2}), m("i", {key: 3}), m("s", {key: 4})] - var temp = [] - var updated = [m("s", {key: 4}), m("i", {key: 3}), m("b", {key: 2}), m("a", {key: 1})] - - render(root, vnodes) - render(root, temp) - render(root, updated) - - o(root.childNodes.length).equals(4) - o(updated[0].dom.nodeName).equals("S") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("I") - o(updated[1].dom).equals(root.childNodes[1]) - o(updated[2].dom.nodeName).equals("B") - o(updated[2].dom).equals(root.childNodes[2]) - o(updated[3].dom.nodeName).equals("A") - o(updated[3].dom).equals(root.childNodes[3]) - }) - o("removes then recreate smaller", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2})] - var temp = [] - var updated = [m("a", {key: 1})] - - render(root, vnodes) - render(root, temp) - render(root, updated) - - o(root.childNodes.length).equals(1) - o(updated[0].dom.nodeName).equals("A") - o(updated[0].dom).equals(root.childNodes[0]) - }) - o("removes then recreate bigger", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2})] - var temp = [] - var updated = [m("a", {key: 1}), m("b", {key: 2}), m("i", {key: 3})] - - render(root, vnodes) - render(root, temp) - render(root, updated) - - o(root.childNodes.length).equals(3) - o(updated[0].dom.nodeName).equals("A") - 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("I") - o(updated[2].dom).equals(root.childNodes[2]) - }) - o("removes then create different", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2})] - var temp = [] - var updated = [m("i", {key: 3}), m("s", {key: 4})] - - render(root, vnodes) - render(root, temp) - render(root, updated) - - o(root.childNodes.length).equals(2) - o(updated[0].dom.nodeName).equals("I") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("S") - o(updated[1].dom).equals(root.childNodes[1]) - }) - o("removes then create different smaller", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2})] - var temp = [] - var updated = [m("i", {key: 3})] - - render(root, vnodes) - render(root, temp) - render(root, updated) - - o(root.childNodes.length).equals(1) - 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 = m("a", {key: "a"}) - var b = m("b", {key: "b"}) - var c = m("c", {key: "c"}) - var d = m("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 = m("a", {key: "a", onupdate: onupdate}) - var b = m("b", {key: "b", onupdate: onupdate}) - var c = m("c", {key: "c", onupdate: onupdate}) - var d = m("d", {key: "d", 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 = [m("a", {key: 1}), m("b", {key: 2})] - var temp = [] - var updated = [m("i", {key: 3}), m("s", {key: 4}), m("div", {key: 5})] - - render(root, vnodes) - render(root, temp) - render(root, updated) - - 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("S") - o(updated[1].dom).equals(root.childNodes[1]) - o(updated[2].dom.nodeName).equals("DIV") - o(updated[2].dom).equals(root.childNodes[2]) - }) - o("removes then create mixed", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2})] - var temp = [] - var updated = [m("a", {key: 1}), m("s", {key: 4})] - - render(root, vnodes) - render(root, temp) - render(root, updated) - - o(root.childNodes.length).equals(2) - o(updated[0].dom.nodeName).equals("A") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("S") - o(updated[1].dom).equals(root.childNodes[1]) - }) - o("removes then create mixed reversed", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2})] - var temp = [] - var updated = [m("s", {key: 4}), m("a", {key: 1})] - - render(root, vnodes) - render(root, temp) - render(root, updated) - - o(root.childNodes.length).equals(2) - o(updated[0].dom.nodeName).equals("S") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("A") - o(updated[1].dom).equals(root.childNodes[1]) - }) - o("removes then create mixed smaller", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2}), m("i", {key: 3})] - var temp = [] - var updated = [m("a", {key: 1}), m("s", {key: 4})] - - render(root, vnodes) - render(root, temp) - render(root, updated) - - o(root.childNodes.length).equals(2) - o(updated[0].dom.nodeName).equals("A") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("S") - o(updated[1].dom).equals(root.childNodes[1]) - }) - o("removes then create mixed smaller reversed", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2}), m("i", {key: 3})] - var temp = [] - var updated = [m("s", {key: 4}), m("a", {key: 1})] - - render(root, vnodes) - render(root, temp) - render(root, updated) - - o(root.childNodes.length).equals(2) - o(updated[0].dom.nodeName).equals("S") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("A") - o(updated[1].dom).equals(root.childNodes[1]) - }) - o("removes then create mixed bigger", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2})] - var temp = [] - var updated = [m("a", {key: 1}), m("i", {key: 3}), m("s", {key: 4})] - - render(root, vnodes) - render(root, temp) - render(root, updated) - - o(root.childNodes.length).equals(3) - o(updated[0].dom.nodeName).equals("A") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("I") - o(updated[1].dom).equals(root.childNodes[1]) - o(updated[2].dom.nodeName).equals("S") - o(updated[2].dom).equals(root.childNodes[2]) - }) - o("removes then create mixed bigger reversed", function() { - var vnodes = [m("a", {key: 1}), m("b", {key: 2})] - var temp = [] - var updated = [m("s", {key: 4}), m("i", {key: 3}), m("a", {key: 1})] - - render(root, vnodes) - render(root, temp) - render(root, updated) - - o(root.childNodes.length).equals(3) - o(updated[0].dom.nodeName).equals("S") - o(updated[0].dom).equals(root.childNodes[0]) - o(updated[1].dom.nodeName).equals("I") - o(updated[1].dom).equals(root.childNodes[1]) - o(updated[2].dom.nodeName).equals("A") - o(updated[2].dom).equals(root.childNodes[2]) - }) - o("change type, position and length", function() { - var vnodes = m("div", - undefined, - m("#", "a") - ) - var updated = m("div", - fragment(m("#", "b")), - undefined, - undefined - ) - - render(root, vnodes) - render(root, updated) - - o(root.firstChild.childNodes.length).equals(1) - }) - o("removes then recreates then reverses children", function() { - var vnodes = [m("a", {key: 1}, m("i", {key: 3}), m("s", {key: 4})), m("b", {key: 2})] - var temp1 = [] - var temp2 = [m("a", {key: 1}, m("i", {key: 3}), m("s", {key: 4})), m("b", {key: 2})] - var updated = [m("a", {key: 1}, m("s", {key: 4}), m("i", {key: 3})), m("b", {key: 2})] - - render(root, vnodes) - render(root, temp1) - render(root, temp2) - render(root, updated) - - o(root.childNodes.length).equals(2) - o(updated[0].dom.nodeName).equals("A") - 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[0].dom.childNodes.length).equals(2) - o(updated[0].dom.childNodes[0].nodeName).equals("S") - o(updated[0].dom.childNodes[1].nodeName).equals("I") - }) - o("removes then recreates nested", function() { - var vnodes = [m("a", {key: 1}, m("a", {key: 3}, m("a", {key: 5})), m("a", {key: 4}, m("a", {key: 5}))), m("a", {key: 2})] - var temp = [] - var updated = [m("a", {key: 1}, m("a", {key: 3}, m("a", {key: 5})), m("a", {key: 4}, m("a", {key: 5}))), m("a", {key: 2})] - - render(root, vnodes) - render(root, temp) - render(root, updated) - - o(root.childNodes.length).equals(2) - o(root.childNodes[0].childNodes.length).equals(2) - o(root.childNodes[0].childNodes[0].childNodes.length).equals(1) - o(root.childNodes[0].childNodes[1].childNodes.length).equals(1) - o(root.childNodes[1].childNodes.length).equals(0) - }) - o("doesn't recycle", function() { - var vnodes = [m("div", {key: 1})] - var temp = [] - var updated = [m("div", {key: 1})] - - render(root, vnodes) - render(root, temp) - render(root, updated) - - o(vnodes[0].dom).notEquals(updated[0].dom) // this used to be a recycling pool test - o(updated[0].dom.nodeName).equals("DIV") - }) - o("doesn't recycle when not keyed", function() { - var vnodes = [m("div")] - var temp = [] - var updated = [m("div")] - - render(root, vnodes) - render(root, temp) - render(root, updated) - - o(root.childNodes.length).equals(1) - o(vnodes[0].dom).notEquals(updated[0].dom) // this used to be a recycling pool test - o(updated[0].dom.nodeName).equals("DIV") - }) - o("doesn't recycle deep", function() { - var vnodes = [m("div", m("a", {key: 1}))] - var temp = [m("div")] - var updated = [m("div", m("a", {key: 1}))] - - render(root, vnodes) - - var oldChild = vnodes[0].dom.firstChild - - render(root, temp) - render(root, updated) - - 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() { - var vnodes = [m("a"), m("b")] - var temp = [m("b")] - var updated = [m("a"), m("b")] - - render(root, vnodes) - render(root, temp) - render(root, updated) - - o(root.childNodes.length).equals(2) - o(root.childNodes[0].nodeName).equals("A") - o(root.childNodes[1].nodeName).equals("B") - }) - o("mixed unkeyed vnode types are not broken by recycle", function() { - var vnodes = [fragment(m("a")), m("b")] - var temp = [m("b")] - var updated = [fragment(m("a")), m("b")] - - render(root, vnodes) - render(root, temp) - render(root, updated) - - o(root.childNodes.length).equals(2) - 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, [ - m("div", m("div", {onremove: onremove})), - m("div", m("div", {onremove: onremove})) - ]) - render(root, [ - m("div", m("div", {onremove: onremove})) - ]) - render(root,[]) - - o(onremove.callCount).equals(2) - }) - o("cached, non-keyed nodes skip diff", function () { - var onupdate = o.spy(); - var cached = m("a", {onupdate: onupdate}) - - render(root, cached) - render(root, cached) - - o(onupdate.callCount).equals(0) - }) - o("cached, keyed nodes skip diff", function () { - var onupdate = o.spy() - var cached = m("a", {key: "a", onupdate: onupdate}) - - render(root, cached) - render(root, cached) - - 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 = m("B", {key: 1}, - m("A", {oncreate: oncreate, onupdate: onupdate}, "A") - ) - render(root, m("div", cached)) - render(root, []) - render(root, m("div", 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 = m("B", - m("A", {oncreate: oncreate, onupdate: onupdate}, "A") - ) - render(root, m("div", cached)) - render(root, []) - render(root, m("div", 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 = m("B", {key: 1}, - m("A", {oncreate: oncreate, onupdate: onupdate}, "A") - ) - render(root, m("div", cached)) - render(root, m("div")) - render(root, []) - render(root, m("div", 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 = m("B", - m("A", {oncreate: oncreate, onupdate: onupdate}, "A") - ) - render(root, m("div", cached)) - render(root, m("div")) - render(root, []) - render(root, m("div", 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() - var remove = o.spy() - var vnodes = [m("div"), m("a", {oncreate: create, onupdate: update, onremove: remove})] - var temp = [null, m("a", {oncreate: create, onupdate: update, onremove: remove})] - var updated = [m("div"), m("a", {oncreate: create, onupdate: update, onremove: remove})] - - render(root, vnodes) - var before = vnodes[1].dom - render(root, temp) - render(root, updated) - var after = updated[1].dom - - o(before).equals(after) - o(create.callCount).equals(1) - o(update.callCount).equals(2) - o(remove.callCount).equals(0) - }) - o("null stays in place if not first", function() { - var create = o.spy() - var update = o.spy() - var remove = o.spy() - var vnodes = [m("b"), m("div"), m("a", {oncreate: create, onupdate: update, onremove: remove})] - var temp = [m("b"), null, m("a", {oncreate: create, onupdate: update, onremove: remove})] - var updated = [m("b"), m("div"), m("a", {oncreate: create, onupdate: update, onremove: remove})] - - render(root, vnodes) - var before = vnodes[2].dom - render(root, temp) - render(root, updated) - var after = updated[2].dom - - o(before).equals(after) - o(create.callCount).equals(1) - o(update.callCount).equals(2) - o(remove.callCount).equals(0) - }) - o("node is recreated if key changes to undefined", function () { - var vnode = m("b", {key: 1}) - var updated = m("b") - - render(root, vnode) - render(root, updated) - - o(vnode.dom).notEquals(updated.dom) - }) - o("don't add back elements from fragments that are restored from the pool #1991", function() { - render(root, [ - fragment(), - fragment() - ]) - render(root, [ - fragment(), - fragment( - m("div") - ) - ]) - render(root, [ - fragment(null) - ]) - render(root, [ - fragment(), - fragment() - ]) - - o(root.childNodes.length).equals(0) - }) - o("don't add back elements from fragments that are being removed #1991", function() { - render(root, [ - fragment(), - m("p"), - ]) - render(root, [ - fragment( - m("div", 5) - ) - ]) - render(root, [ - fragment(), - fragment() - ]) - - 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() - - render(root, [m("div", {oncreate: oncreate, onremove: onremove, onupdate: onupdate}), null]) - render(root, [null, m("div", {oncreate: oncreate, onremove: onremove, onupdate: onupdate}), 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, [m("a", {key: 2})]) - render(root, [m("b", {key: 1}), m("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, [m("x", {key: 1}), m("y", {key: 2}), m("z", {key: 3})]) - render(root, [m("b", {key: 2}), m("c", {key: 1}), m("d", {key: 4}), m("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, [fragment(m("div", {key: 1}), m("div", {key: 2})), m("p")]) - render(root, [fragment(), m("p")]) - render(root, [fragment(m("div", {key: 2}), m("div", {key: 1})), m("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 = vnodify("a,b,c,d") - var updated = vnodify("b,a,d,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 = vnodify("a,b,c,d") - var updated = vnodify("d,c,b,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 = vnodify("a,b,c") - var updated = vnodify("c,b,a") - var vnodes = [m("a", {key: "a"}), m("b", {key: "b"}), m("c", {key: "c"})] - var updated = [m("c", {key: "c"}), m("b", {key: "b"}), m("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 = vnodify("i,a,b,c,d,j") - var updated = vnodify("i,b,a,d,c,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 = vnodify("i,a,b,c,d,j") - var updated = vnodify("i,d,c,b,a,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 = vnodify("i,a,b,c,j") - var updated = vnodify("i,c,b,a,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() { - 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() { - 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 - - o("fragment child toggles from null when followed by null component then tag", function() { - var component = createComponent({view: function() {return null}}) - var vnodes = [fragment(m("a"), m(component), m("b"))] - var temp = [fragment(null, m(component), m("b"))] - var updated = [fragment(m("a"), m(component), m("b"))] - - render(root, vnodes) - render(root, temp) - render(root, updated) - - o(root.childNodes.length).equals(2) - o(root.childNodes[0].nodeName).equals("A") - o(root.childNodes[1].nodeName).equals("B") - }) - o("fragment child toggles from null in component when followed by null component then tag", function() { - var flag = true - var a = createComponent({view: function() {return flag ? m("a") : null}}) - var b = createComponent({view: function() {return null}}) - var vnodes = [fragment(m(a), m(b), m("s"))] - var temp = [fragment(m(a), m(b), m("s"))] - var updated = [fragment(m(a), m(b), m("s"))] - - render(root, vnodes) - flag = false - render(root, temp) - flag = true - render(root, updated) - - o(root.childNodes.length).equals(2) - 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 fragment(m("a"), m("b"))} - }) - try { - render(root, [m(component)]) - render(root, []) - - o(root.childNodes.length).equals(0) - } catch (e) { - o(e).equals(null) - } - }) - }) - }) -}) diff --git a/render/tests/test-updateNodesFuzzer.js b/render/tests/test-updateNodesFuzzer.js deleted file mode 100644 index 0155258a7..000000000 --- a/render/tests/test-updateNodesFuzzer.js +++ /dev/null @@ -1,157 +0,0 @@ -"use strict" - -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") -var m = require("../../render/hyperscript") - -// 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) - }) - - - 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 m(x, {key: x})})) - addSpies(root) - render(root, test.updated.map(function(x){return m(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) -} diff --git a/render/tests/test-updateText.js b/render/tests/test-updateText.js deleted file mode 100644 index de6b55efc..000000000 --- a/render/tests/test-updateText.js +++ /dev/null @@ -1,96 +0,0 @@ -"use strict" - -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") - -o.spec("updateText", function() { - var $window, root, render - o.beforeEach(function() { - $window = domMock() - root = $window.document.createElement("div") - render = vdom($window) - }) - - o("updates to string", function() { - var vnode = "a" - var updated = "b" - - render(root, vnode) - render(root, updated) - - o(root.firstChild.nodeValue).equals("b") - }) - o("updates to falsy string", function() { - var vnode = "a" - var updated = "" - - render(root, vnode) - render(root, updated) - - o(root.firstChild.nodeValue).equals("") - }) - o("updates from falsy string", function() { - var vnode = "" - var updated = "b" - - render(root, vnode) - render(root, updated) - - o(root.firstChild.nodeValue).equals("b") - }) - o("updates to number", function() { - var vnode = "a" - var updated = 1 - - render(root, vnode) - render(root, updated) - - o(root.firstChild.nodeValue).equals("1") - }) - o("updates to falsy number", function() { - var vnode = "a" - var updated = 0 - - render(root, vnode) - render(root, updated) - - o(root.firstChild.nodeValue).equals("0") - }) - o("updates from falsy number", function() { - var vnode = 0 - var updated = "b" - - render(root, vnode) - render(root, updated) - - o(root.firstChild.nodeValue).equals("b") - }) - o("updates to boolean", function() { - var vnode = "a" - var updated = true - - render(root, vnode) - render(root, updated) - - o(root.childNodes.length).equals(0) - }) - o("updates to falsy boolean", function() { - var vnode = "a" - var updated = false - - render(root, vnode) - render(root, updated) - - o(root.childNodes.length).equals(0) - }) - o("updates from falsy boolean", function() { - var vnode = false - var updated = "b" - - render(root, vnode) - render(root, updated) - - o(root.firstChild.nodeValue).equals("b") - }) -}) diff --git a/render/trust.js b/render/trust.js deleted file mode 100644 index 5995e287e..000000000 --- a/render/trust.js +++ /dev/null @@ -1,8 +0,0 @@ -"use strict" - -var Vnode = require("../render/vnode") - -module.exports = function(html) { - if (html == null) html = "" - return Vnode("<", undefined, undefined, html, undefined, undefined) -} diff --git a/render/vnode.js b/render/vnode.js deleted file mode 100644 index ec19b174f..000000000 --- a/render/vnode.js +++ /dev/null @@ -1,35 +0,0 @@ -"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, 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 === "boolean") return null - if (typeof node === "object") return node - return Vnode("#", undefined, undefined, String(node), undefined, undefined) -} -Vnode.normalizeChildren = function(input) { - var children = [] - if (input.length) { - var isKeyed = input[0] != null && input[0].key != null - // Note: this is a *very* perf-sensitive check. - // Fun fact: merging the loop like this is somehow faster than splitting - // it, noticeably so. - for (var i = 1; i < input.length; i++) { - if ((input[i] != null && input[i].key != null) !== isKeyed) { - throw new TypeError( - isKeyed && (input[i] != null || typeof input[i] === "boolean") - ? "In fragments, vnodes must either all have keys or none have keys. You may wish to consider using an explicit keyed empty fragment, m.fragment({key: ...}), instead of a hole." - : "In fragments, vnodes must either all have keys or none have keys." - ) - } - } - for (var i = 0; i < input.length; i++) { - children[i] = Vnode.normalize(input[i]) - } - } - return children -} - -module.exports = Vnode diff --git a/request.js b/request.js deleted file mode 100644 index 740119bf5..000000000 --- a/request.js +++ /dev/null @@ -1,5 +0,0 @@ -"use strict" - -var mountRedraw = require("./mount-redraw") - -module.exports = require("./request/request")(typeof window !== "undefined" ? window : null, mountRedraw.redraw) diff --git a/request/request.js b/request/request.js deleted file mode 100644 index 7252ccade..000000000 --- a/request/request.js +++ /dev/null @@ -1,199 +0,0 @@ -"use strict" - -var buildPathname = require("../pathname/build") -var hasOwn = require("../util/hasOwn") - -module.exports = function($window, oncompletion) { - function PromiseProxy(executor) { - return new Promise(executor) - } - - function makeRequest(url, args) { - return new Promise(function(resolve, reject) { - url = buildPathname(url, args.params) - var method = args.method != null ? args.method.toUpperCase() : "GET" - var body = args.body - var assumeJSON = (args.serialize == null || args.serialize === JSON.serialize) && !(body instanceof $window.FormData || body instanceof $window.URLSearchParams) - var responseType = args.responseType || (typeof args.extract === "function" ? "" : "json") - - var xhr = new $window.XMLHttpRequest(), aborted = false, isTimeout = false - var original = xhr, replacedAbort - var abort = xhr.abort - - xhr.abort = function() { - aborted = true - abort.call(this) - } - - xhr.open(method, url, args.async !== false, typeof args.user === "string" ? args.user : undefined, typeof args.password === "string" ? args.password : undefined) - - if (assumeJSON && body != null && !hasHeader(args, "content-type")) { - xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8") - } - if (typeof args.deserialize !== "function" && !hasHeader(args, "accept")) { - xhr.setRequestHeader("Accept", "application/json, text/*") - } - if (args.withCredentials) xhr.withCredentials = args.withCredentials - if (args.timeout) xhr.timeout = args.timeout - xhr.responseType = responseType - - for (var key in args.headers) { - if (hasOwn.call(args.headers, key)) { - xhr.setRequestHeader(key, args.headers[key]) - } - } - - xhr.onreadystatechange = function(ev) { - // Don't throw errors on xhr.abort(). - if (aborted) return - - if (ev.target.readyState === 4) { - try { - var success = (ev.target.status >= 200 && ev.target.status < 300) || ev.target.status === 304 || (/^file:\/\//i).test(url) - // When the response type isn't "" or "text", - // `xhr.responseText` is the wrong thing to use. - // Browsers do the right thing and throw here, and we - // should honor that and do the right thing by - // preferring `xhr.response` where possible/practical. - var response = ev.target.response, message - - if (responseType === "json") { - // For IE and Edge, which don't implement - // `responseType: "json"`. - if (!ev.target.responseType && typeof args.extract !== "function") { - // Handle no-content which will not parse. - try { response = JSON.parse(ev.target.responseText) } - catch (e) { response = null } - } - } else if (!responseType || responseType === "text") { - // Only use this default if it's text. If a parsed - // document is needed on old IE and friends (all - // unsupported), the user should use a custom - // `config` instead. They're already using this at - // their own risk. - if (response == null) response = ev.target.responseText - } - - if (typeof args.extract === "function") { - response = args.extract(ev.target, args) - success = true - } else if (typeof args.deserialize === "function") { - response = args.deserialize(response) - } - - if (success) { - if (typeof args.type === "function") { - if (Array.isArray(response)) { - for (var i = 0; i < response.length; i++) { - response[i] = new args.type(response[i]) - } - } - else response = new args.type(response) - } - resolve(response) - } - else { - var completeErrorResponse = function() { - try { message = ev.target.responseText } - catch (e) { message = response } - var error = new Error(message) - error.code = ev.target.status - error.response = response - reject(error) - } - - if (xhr.status === 0) { - // Use setTimeout to push this code block onto the event queue - // This allows `xhr.ontimeout` to run in the case that there is a timeout - // Without this setTimeout, `xhr.ontimeout` doesn't have a chance to reject - // as `xhr.onreadystatechange` will run before it - setTimeout(function() { - if (isTimeout) return - completeErrorResponse() - }) - } else completeErrorResponse() - } - } - catch (e) { - reject(e) - } - } - } - - xhr.ontimeout = function (ev) { - isTimeout = true - var error = new Error("Request timed out") - error.code = ev.target.status - reject(error) - } - - if (typeof args.config === "function") { - xhr = args.config(xhr, args, url) || xhr - - // Propagate the `abort` to any replacement XHR as well. - if (xhr !== original) { - replacedAbort = xhr.abort - xhr.abort = function() { - aborted = true - replacedAbort.call(this) - } - } - } - - if (body == null) xhr.send() - else if (typeof args.serialize === "function") xhr.send(args.serialize(body)) - else if (body instanceof $window.FormData || body instanceof $window.URLSearchParams) xhr.send(body) - else xhr.send(JSON.stringify(body)) - }) - } - - // In case the global Promise is some userland library's where they rely on - // `foo instanceof this.constructor`, `this.constructor.resolve(value)`, or - // similar. Let's *not* break them. - PromiseProxy.prototype = Promise.prototype - PromiseProxy.__proto__ = Promise // eslint-disable-line no-proto - - function hasHeader(args, name) { - for (var key in args.headers) { - if (hasOwn.call(args.headers, key) && key.toLowerCase() === name) return true - } - return false - } - - return { - request: function(url, args) { - if (typeof url !== "string") { args = url; url = url.url } - else if (args == null) args = {} - var promise = makeRequest(url, args) - if (args.background === true) return promise - var count = 0 - function complete() { - if (--count === 0 && typeof oncompletion === "function") oncompletion() - } - - return wrap(promise) - - function wrap(promise) { - var then = promise.then - // Set the constructor, so engines know to not await or resolve - // this as a native promise. At the time of writing, this is - // only necessary for V8, but their behavior is the correct - // behavior per spec. See this spec issue for more details: - // https://github.com/tc39/ecma262/issues/1577. Also, see the - // corresponding comment in `request/tests/test-request.js` for - // a bit more background on the issue at hand. - promise.constructor = PromiseProxy - promise.then = function() { - count++ - var next = then.apply(promise, arguments) - next.then(complete, function(e) { - complete() - if (count === 0) throw e - }) - return wrap(next) - } - return promise - } - } - } -} diff --git a/request/tests/test-request.js b/request/tests/test-request.js deleted file mode 100644 index a5e5d9801..000000000 --- a/request/tests/test-request.js +++ /dev/null @@ -1,909 +0,0 @@ -"use strict" - -var o = require("ospec") -var callAsync = require("../../test-utils/callAsync") -var xhrMock = require("../../test-utils/xhrMock") -var Request = require("../../request/request") - -o.spec("request", function() { - var mock, request, complete - o.beforeEach(function() { - mock = xhrMock() - complete = o.spy() - request = Request(mock, complete).request - }) - - o.spec("success", function() { - o("works via GET", function(done) { - mock.$defineRoutes({ - "GET /item": function() { - return {status: 200, responseText: JSON.stringify({a: 1})} - } - }) - request({method: "GET", url: "/item"}).then(function(data) { - o(data).deepEquals({a: 1}) - }).then(function() { - done() - }) - }) - o("implicit GET method", function(done){ - mock.$defineRoutes({ - "GET /item": function() { - return {status: 200, responseText: JSON.stringify({a: 1})} - } - }) - request({url: "/item"}).then(function(data) { - o(data).deepEquals({a: 1}) - }).then(function() { - done() - }) - }) - o("first argument can be a string aliasing url property", function(done){ - mock.$defineRoutes({ - "GET /item": function() { - return {status: 200, responseText: JSON.stringify({a: 1})} - } - }) - request("/item").then(function(data) { - o(data).deepEquals({a: 1}) - }).then(function() { - done() - }) - }) - o("works via POST", function(done) { - mock.$defineRoutes({ - "POST /item": function() { - return {status: 200, responseText: JSON.stringify({a: 1})} - } - }) - request({method: "POST", url: "/item"}).then(function(data) { - o(data).deepEquals({a: 1}) - }).then(done) - }) - o("first argument can act as URI with second argument providing options", function(done) { - mock.$defineRoutes({ - "POST /item": function() { - return {status: 200, responseText: JSON.stringify({a: 1})} - } - }) - request("/item", {method: "POST"}).then(function(data) { - o(data).deepEquals({a: 1}) - }).then(done) - }) - o("first argument keeps protocol", function(done) { - mock.$defineRoutes({ - "POST /item": function(request) { - o(request.rawUrl).equals("https://example.com/item") - return {status: 200, responseText: JSON.stringify({a: 1})} - } - }) - request("https://example.com/item", {method: "POST"}).then(function(data) { - o(data).deepEquals({a: 1}) - }).then(done) - }) - o("works w/ parameterized data via GET", function(done) { - mock.$defineRoutes({ - "GET /item": function(request) { - return {status: 200, responseText: JSON.stringify({a: request.query})} - } - }) - request({method: "GET", url: "/item", params: {x: "y"}}).then(function(data) { - o(data).deepEquals({a: "?x=y"}) - }).then(done) - }) - o("works w/ parameterized data via POST", function(done) { - mock.$defineRoutes({ - "POST /item": function(request) { - return {status: 200, responseText: JSON.stringify({a: JSON.parse(request.body)})} - } - }) - request({method: "POST", url: "/item", body: {x: "y"}}).then(function(data) { - o(data).deepEquals({a: {x: "y"}}) - }).then(done) - }) - o("works w/ parameterized data containing colon via GET", function(done) { - mock.$defineRoutes({ - "GET /item": function(request) { - return {status: 200, responseText: JSON.stringify({a: request.query})} - } - }) - request({method: "GET", url: "/item", params: {x: ":y"}}).then(function(data) { - o(data).deepEquals({a: "?x=%3Ay"}) - }).then(done) - }) - o("works w/ parameterized data containing colon via POST", function(done) { - mock.$defineRoutes({ - "POST /item": function(request) { - return {status: 200, responseText: JSON.stringify({a: JSON.parse(request.body)})} - } - }) - request({method: "POST", url: "/item", body: {x: ":y"}}).then(function(data) { - o(data).deepEquals({a: {x: ":y"}}) - }).then(done) - }) - o("works w/ parameterized url via GET", function(done) { - mock.$defineRoutes({ - "GET /item/y": function(request) { - return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: request.body})} - } - }) - request({method: "GET", url: "/item/:x", params: {x: "y"}}).then(function(data) { - o(data).deepEquals({a: "/item/y", b: {}, c: null}) - }).then(done) - }) - o("works w/ parameterized url via POST", function(done) { - mock.$defineRoutes({ - "POST /item/y": function(request) { - return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: request.body})} - } - }) - request({method: "POST", url: "/item/:x", params: {x: "y"}}).then(function(data) { - o(data).deepEquals({a: "/item/y", b: {}, c: null}) - }).then(done) - }) - o("works w/ parameterized url + body via GET", function(done) { - mock.$defineRoutes({ - "GET /item/y": function(request) { - return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: JSON.parse(request.body)})} - } - }) - request({method: "GET", url: "/item/:x", params: {x: "y"}, body: {a: "b"}}).then(function(data) { - o(data).deepEquals({a: "/item/y", b: {}, c: {a: "b"}}) - }).then(done) - }) - o("works w/ parameterized url + body via POST", function(done) { - mock.$defineRoutes({ - "POST /item/y": function(request) { - return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: JSON.parse(request.body)})} - } - }) - request({method: "POST", url: "/item/:x", params: {x: "y"}, body: {a: "b"}}).then(function(data) { - o(data).deepEquals({a: "/item/y", b: {}, c: {a: "b"}}) - }).then(done) - }) - o("works w/ parameterized url + query via GET", function(done) { - mock.$defineRoutes({ - "GET /item/y": function(request) { - return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: request.body})} - } - }) - request({method: "GET", url: "/item/:x", params: {x: "y", q: "term"}}).then(function(data) { - o(data).deepEquals({a: "/item/y", b: "?q=term", c: null}) - }).then(done) - }) - o("works w/ parameterized url + query via POST", function(done) { - mock.$defineRoutes({ - "POST /item/y": function(request) { - return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: request.body})} - } - }) - request({method: "POST", url: "/item/:x", params: {x: "y", q: "term"}}).then(function(data) { - o(data).deepEquals({a: "/item/y", b: "?q=term", c: null}) - }).then(done) - }) - o("works w/ parameterized url + query + body via GET", function(done) { - mock.$defineRoutes({ - "GET /item/y": function(request) { - return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: JSON.parse(request.body)})} - } - }) - request({method: "GET", url: "/item/:x", params: {x: "y", q: "term"}, body: {a: "b"}}).then(function(data) { - o(data).deepEquals({a: "/item/y", b: "?q=term", c: {a: "b"}}) - }).then(done) - }) - o("works w/ parameterized url + query + body via POST", function(done) { - mock.$defineRoutes({ - "POST /item/y": function(request) { - return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: JSON.parse(request.body)})} - } - }) - request({method: "POST", url: "/item/:x", params: {x: "y", q: "term"}, body: {a: "b"}}).then(function(data) { - o(data).deepEquals({a: "/item/y", b: "?q=term", c: {a: "b"}}) - }).then(done) - }) - o("works w/ array", function(done) { - mock.$defineRoutes({ - "POST /items": function(request) { - return {status: 200, responseText: JSON.stringify({a: request.url, b: JSON.parse(request.body)})} - } - }) - request({method: "POST", url: "/items", body: [{x: "y"}]}).then(function(data) { - o(data).deepEquals({a: "/items", b: [{x: "y"}]}) - }).then(done) - }) - o("works w/ URLSearchParams body", function(done) { - mock.$defineRoutes({ - "POST /item": function(request) { - return {status: 200, responseText: JSON.stringify({a: request.url, b: request.body.toString()})} - } - }) - request({method: "POST", url: "/item", body: new URLSearchParams({x: "y", z: "w"})}).then(function(data) { - o(data).deepEquals({a: "/item", b: "x=y&z=w"}) - }).then(done) - }); - o("ignores unresolved parameter via GET", function(done) { - mock.$defineRoutes({ - "GET /item/:x": function(request) { - return {status: 200, responseText: JSON.stringify({a: request.url})} - } - }) - request({method: "GET", url: "/item/:x"}).then(function(data) { - o(data).deepEquals({a: "/item/:x"}) - }).then(done) - }) - o("ignores unresolved parameter via POST", function(done) { - mock.$defineRoutes({ - "GET /item/:x": function(request) { - return {status: 200, responseText: JSON.stringify({a: request.url})} - } - }) - request({method: "GET", url: "/item/:x"}).then(function(data) { - o(data).deepEquals({a: "/item/:x"}) - }).then(done) - }) - o("type parameter works for Array responses", function(done) { - var Entity = function(args) { - return {_id: args.id} - } - - mock.$defineRoutes({ - "GET /item": function() { - return {status: 200, responseText: JSON.stringify([{id: 1}, {id: 2}, {id: 3}])} - } - }) - request({method: "GET", url: "/item", type: Entity}).then(function(data) { - o(data).deepEquals([{_id: 1}, {_id: 2}, {_id: 3}]) - }).then(done) - }) - o("type parameter works for Object responses", function(done) { - var Entity = function(args) { - return {_id: args.id} - } - - mock.$defineRoutes({ - "GET /item": function() { - return {status: 200, responseText: JSON.stringify({id: 1})} - } - }) - request({method: "GET", url: "/item", type: Entity}).then(function(data) { - o(data).deepEquals({_id: 1}) - }).then(done) - }) - o("serialize parameter works in GET", function(done) { - var serialize = function(data) { - return "id=" + data.id - } - - mock.$defineRoutes({ - "GET /item": function(request) { - return {status: 200, responseText: JSON.stringify({body: request.query})} - } - }) - request({method: "GET", url: "/item", serialize: serialize, params: {id: 1}}).then(function(data) { - o(data.body).equals("?id=1") - }).then(done) - }) - o("serialize parameter works in POST", function(done) { - var serialize = function(data) { - return "id=" + data.id - } - - mock.$defineRoutes({ - "POST /item": function(request) { - return {status: 200, responseText: JSON.stringify({body: request.body})} - } - }) - request({method: "POST", url: "/item", serialize: serialize, body: {id: 1}}).then(function(data) { - o(data.body).equals("id=1") - }).then(done) - }) - o("deserialize parameter works in GET", function(done) { - var deserialize = function(data) { - return data - } - - mock.$defineRoutes({ - "GET /item": function() { - return {status: 200, responseText: JSON.stringify({test: 123})} - } - }) - request({method: "GET", url: "/item", deserialize: deserialize}).then(function(data) { - o(data).deepEquals({test: 123}) - }).then(done) - }) - o("deserialize parameter works in POST", function(done) { - var deserialize = function(data) { - return data - } - - mock.$defineRoutes({ - "POST /item": function() { - return {status: 200, responseText: JSON.stringify({test: 123})} - } - }) - request({method: "POST", url: "/item", deserialize: deserialize}).then(function(data) { - o(data).deepEquals({test: 123}) - }).then(done) - }) - o("extract parameter works in GET", function(done) { - var extract = function() { - return {test: 123} - } - - mock.$defineRoutes({ - "GET /item": function() { - return {status: 200, responseText: ""} - } - }) - request({method: "GET", url: "/item", extract: extract}).then(function(data) { - o(data).deepEquals({test: 123}) - }).then(done) - }) - o("extract parameter works in POST", function(done) { - var extract = function() { - return {test: 123} - } - - mock.$defineRoutes({ - "POST /item": function() { - return {status: 200, responseText: ""} - } - }) - request({method: "POST", url: "/item", extract: extract}).then(function(data) { - o(data).deepEquals({test: 123}) - }).then(done) - }) - o("ignores deserialize if extract is defined", function(done) { - var extract = function(data) { - return data.status - } - var deserialize = o.spy() - - mock.$defineRoutes({ - "GET /item": function() { - return {status: 200, responseText: ""} - } - }) - request({method: "GET", url: "/item", extract: extract, deserialize: deserialize}).then(function(data) { - o(data).equals(200) - }).then(function() { - o(deserialize.callCount).equals(0) - }).then(done) - }) - o("config parameter works", function(done) { - mock.$defineRoutes({ - "POST /item": function() { - return {status: 200, responseText: ""} - } - }) - request({method: "POST", url: "/item", config: config}).then(done) - - function config(xhr) { - o(typeof xhr.setRequestHeader).equals("function") - o(typeof xhr.open).equals("function") - o(typeof xhr.send).equals("function") - } - }) - o("requests don't block each other", function(done) { - mock.$defineRoutes({ - "GET /item": function() { - return {status: 200, responseText: "[]"} - } - }) - request("/item").then(function() { - return request("/item") - }) - request("/item").then(function() { - return request("/item") - }) - setTimeout(function() { - o(complete.callCount).equals(4) - done() - }, 20) - }) - o("requests trigger finally once with a chained then", function(done) { - mock.$defineRoutes({ - "GET /item": function() { - return {status: 200, responseText: "[]"} - } - }) - var promise = request("/item") - promise.then(function() {}).then(function() {}) - promise.then(function() {}).then(function() {}) - setTimeout(function() { - o(complete.callCount).equals(1) - done() - }, 20) - }) - o("requests does not trigger finally when background: true", function(done) { - mock.$defineRoutes({ - "GET /item": function() { - return {status: 200, responseText: "[]"} - } - }) - request("/item", {background: true}).then(function() {}) - - setTimeout(function() { - o(complete.callCount).equals(0) - done() - }, 20) - }) - o("headers are set when header arg passed", function(done) { - mock.$defineRoutes({ - "POST /item": function() { - return {status: 200, responseText: ""} - } - }) - request({method: "POST", url: "/item", config: config, headers: {"Custom-Header": "Value"}}).then(done) - - function config(xhr) { - o(xhr.getRequestHeader("Custom-Header")).equals("Value") - } - }) - o("headers are with higher precedence than default headers", function(done) { - mock.$defineRoutes({ - "POST /item": function() { - return {status: 200, responseText: ""} - } - }) - request({method: "POST", url: "/item", config: config, headers: {"Content-Type": "Value"}}).then(done) - - function config(xhr) { - o(xhr.getRequestHeader("Content-Type")).equals("Value") - } - }) - o("doesn't fail on abort", function(done) { - mock.$defineRoutes({ - "GET /item": function() { - return {status: 200, responseText: JSON.stringify({a: 1})} - } - }) - - var failed = false - var resolved = false - function handleAbort(xhr) { - var onreadystatechange = xhr.onreadystatechange - xhr.onreadystatechange = function() { - onreadystatechange.call(xhr, {target: xhr}) - setTimeout(function() { // allow promises to (not) resolve first - o(failed).equals(false) - o(resolved).equals(false) - done() - }, 0) - } - xhr.abort() - } - request({method: "GET", url: "/item", config: handleAbort}).catch(function() { - failed = true - }) - .then(function() { - resolved = true - }) - }) - o("doesn't fail on replaced abort", function(done) { - mock.$defineRoutes({ - "GET /item": function() { - return {status: 200, responseText: JSON.stringify({a: 1})} - } - }) - - var failed = false - var resolved = false - var abortSpy = o.spy() - var replacement - function handleAbort(xhr) { - var onreadystatechange = xhr.onreadystatechange - xhr.onreadystatechange = function() { - onreadystatechange.call(xhr, {target: xhr}) - setTimeout(function() { // allow promises to (not) resolve first - o(failed).equals(false) - o(resolved).equals(false) - done() - }, 0) - } - return replacement = { - send: xhr.send.bind(xhr), - abort: abortSpy, - } - } - request({method: "GET", url: "/item", config: handleAbort}).then(function() { - resolved = true - }, function() { - failed = true - }) - replacement.abort() - o(abortSpy.callCount).equals(1) - }) - o("doesn't fail on file:// status 0", function(done) { - mock.$defineRoutes({ - "GET /item": function() { - return {status: 0, responseText: JSON.stringify({a: 1})} - } - }) - var failed = false - request({method: "GET", url: "file:///item"}).catch(function() { - failed = true - }).then(function(data) { - o(failed).equals(false) - o(data).deepEquals({a: 1}) - }).then(function() { - done() - }) - }) - o("set timeout to xhr instance", function() { - mock.$defineRoutes({ - "GET /item": function() { - return {status: 200, responseText: ""} - } - }) - return request({ - method: "GET", url: "/item", - timeout: 42, - config: function(xhr) { - o(xhr.timeout).equals(42) - } - }) - }) - o("set responseType to request instance", function() { - mock.$defineRoutes({ - "GET /item": function() { - return {status: 200, responseText: ""} - } - }) - return request({ - method: "GET", url: "/item", - responseType: "blob", - config: function(xhr) { - o(xhr.responseType).equals("blob") - } - }) - }) - o("params unmodified after interpolate", function() { - mock.$defineRoutes({ - "PUT /items/1": function() { - return {status: 200, responseText: "[]"} - } - }) - var params = {x: 1, y: 2} - var p = request({method: "PUT", url: "/items/:x", params: params}) - - o(params).deepEquals({x: 1, y: 2}) - - return p - }) - o("can return replacement from config", function() { - mock.$defineRoutes({ - "GET /a": function() { - return {status: 200, responseText: "[]"} - } - }) - var result - return request({ - url: "/a", - config: function(xhr) { - return result = { - send: o.spy(xhr.send.bind(xhr)), - } - }, - }) - .then(function () { - o(result.send.callCount).equals(1) - }) - }) - o("can abort from replacement", function() { - mock.$defineRoutes({ - "GET /a": function() { - return {status: 200, responseText: "[]"} - } - }) - var result - - request({ - url: "/a", - config: function(xhr) { - return result = { - send: o.spy(xhr.send.bind(xhr)), - abort: o.spy(), - } - }, - }) - - result.abort() - }) - }) - o.spec("failure", function() { - o("rejects on server error", function(done) { - mock.$defineRoutes({ - "GET /item": function() { - return {status: 500, responseText: JSON.stringify({error: "error"})} - } - }) - request({method: "GET", url: "/item"}).catch(function(e) { - o(e instanceof Error).equals(true) - o(e.message).equals("[object Object]") - o(e.response).deepEquals({error: "error"}) - o(e.code).equals(500) - }).then(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"})} - } - }) - request({method: "GET", url: "/item"}).catch(function(e) { - o(e instanceof Error).equals(true) - 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) { - mock.$defineRoutes({ - "GET /item": function() { - return {status: 500, responseText: "error"} - } - }) - request({method: "GET", url: "/item"}).catch(function(e) { - o(e.message).equals("null") - o(e.response).equals(null) - }).then(done) - }) - o("triggers all branched catches upon rejection", function(done) { - mock.$defineRoutes({ - "GET /item": function() { - return {status: 500, responseText: "error"} - } - }) - var promise = request({method: "GET", url: "/item"}) - var then = o.spy() - var catch1 = o.spy() - var catch2 = o.spy() - var catch3 = o.spy() - - promise.catch(catch1) - promise.then(then, catch2) - promise.then(then).catch(catch3) - - callAsync(function() { - callAsync(function() { - callAsync(function() { - o(catch1.callCount).equals(1) - o(then.callCount).equals(0) - o(catch2.callCount).equals(1) - o(catch3.callCount).equals(1) - done() - }) - }) - }) - }) - o("rejects on cors-like error", function(done) { - mock.$defineRoutes({ - "GET /item": function() { - return {status: 0} - } - }) - request({method: "GET", url: "/item"}).catch(function(e) { - o(e instanceof Error).equals(true) - }).then(done) - }) - o("rejects on request timeout", function(done) { - var timeout = 50 - var timeToGetItem = timeout + 1 - - mock.$defineRoutes({ - "GET /item": function() { - return new Promise(function(resolve) { - setTimeout(function() { - resolve({status: 200}) - }, timeToGetItem) - }) - } - }) - - request({ - method: "GET", url: "/item", - timeout: timeout - }).catch(function(e) { - o(e instanceof Error).equals(true) - o(e.message).equals("Request timed out") - o(e.code).equals(0) - }).then(function() { - done() - }) - }) - o("does not reject when time to request resource does not exceed timeout", function(done) { - var timeout = 50 - var timeToGetItem = timeout - 1 - var isRequestRejected = false - - mock.$defineRoutes({ - "GET /item": function() { - return new Promise(function(resolve) { - setTimeout(function() { - resolve({status: 200}) - }, timeToGetItem) - }) - } - }) - - request({ - method: "GET", url: "/item", - timeout: timeout - }).catch(function(e) { - isRequestRejected = true - o(e.message).notEquals("Request timed out") - }).then(function() { - o(isRequestRejected).equals(false) - 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"})} - } - }) - request({ - 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})} - } - }) - request({ - 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() - }) - }) - }) - o.spec("json header", function() { - function checkUnset(method) { - o("doesn't set header on " + method + " without body", function(done) { - var routes = {} - routes[method + " /item"] = function() { - return {status: 200, responseText: JSON.stringify({a: 1})} - } - mock.$defineRoutes(routes) - request({ - method: method, url: "/item", - config: function(xhr) { - var header = xhr.getRequestHeader("Content-Type") - o(header).equals(undefined) - header = xhr.getRequestHeader("Accept") - o(header).equals("application/json, text/*") - } - }).then(function(result) { - o(result).deepEquals({a: 1}) - done() - }).catch(function(e) { - done(e) - }) - }) - } - - function checkSet(method, body) { - o("sets header on " + method + " with body", function(done) { - var routes = {} - routes[method + " /item"] = function(response) { - return { - status: 200, - responseText: JSON.stringify({body: JSON.parse(response.body)}), - } - } - mock.$defineRoutes(routes) - request({ - method: method, url: "/item", body: body, - config: function(xhr) { - var header = xhr.getRequestHeader("Content-Type") - o(header).equals("application/json; charset=utf-8") - header = xhr.getRequestHeader("Accept") - o(header).equals("application/json, text/*") - } - }).then(function(result) { - o(result).deepEquals({body: body}) - done() - }).catch(function(e) { - done(e) - }) - }) - } - - checkUnset("GET") - checkUnset("HEAD") - checkUnset("OPTIONS") - checkUnset("POST") - checkUnset("PUT") - checkUnset("DELETE") - checkUnset("PATCH") - - checkSet("GET", {foo: "bar"}) - checkSet("HEAD", {foo: "bar"}) - checkSet("OPTIONS", {foo: "bar"}) - checkSet("POST", {foo: "bar"}) - checkSet("PUT", {foo: "bar"}) - checkSet("DELETE", {foo: "bar"}) - checkSet("PATCH", {foo: "bar"}) - }) - - // See: https://github.com/MithrilJS/mithril.js/issues/2426 - // - // TL;DR: lots of subtlety. Make sure you read the ES spec closely before - // updating this code or the corresponding finalizer code in - // `request/request` responsible for scheduling autoredraws, or you might - // inadvertently break things. - // - // The precise behavior here is that it schedules a redraw immediately after - // the second tick *after* the promise resolves, but `await` in engines that - // have implemented the change in https://github.com/tc39/ecma262/pull/1250 - // will only take one tick to get the value. Engines that haven't - // implemented that spec change would wait until the tick after the redraw - // was scheduled before it can see the new value. But this only applies when - // the engine needs to coerce the value, and this is where things get a bit - // hairy. As per spec, V8 checks the `.constructor` property of promises and - // if that `=== Promise`, it does *not* coerce it using `.then`, but instead - // just resolves it directly. This, of course, can screw with our autoredraw - // behavior, and we have to work around that. At the time of writing, no - // other browser checks for this additional constraint, and just blindly - // invokes `.then` instead, and so we end up working as anticipated. But for - // obvious reasons, it's a bad idea to rely on a spec violation for things - // to work unless the spec itself is clearly broken (in this case, it's - // not). And so we need to test for this very unusual edge case. - // - // The direct `eval` is just so I can convert early errors to runtime - // errors without having to explicitly wire up all the bindings set up in - // `o.beforeEach`. I evaluate it immediately inside a `try`/`catch` instead - // of inside the test code so any relevant syntax error can be detected - // ahead of time and the test skipped entirely. It might trigger mental - // alarms because `eval` is normally asking for problems, but this is a - // rare case where it's genuinely safe and rational. - try { - // eslint-disable-next-line no-eval - var runAsyncTest = eval( - "async () => {\n" + - " var p = request('/item')\n" + - " o(complete.callCount).equals(0)\n" + - // Note: this step does *not* invoke `.then` on the promise returned - // from `p.then(resolve, reject)`. - " await p\n" + - // The spec prior to https://github.com/tc39/ecma262/pull/1250 used - // to take 3 ticks instead of 1, so `complete` would have been - // called already and we would've been done. After it, it now takes - // 1 tick and so `complete` wouldn't have yet been called - it takes - // 2 ticks to get called. And so we have to wait for one more ticks - // for `complete` to get called. - " await null\n" + - " o(complete.callCount).equals(1)\n" + - "}" - ) - - o("invokes the redraw in native async/await", function () { - // Use the native promise for correct semantics. This test will fail - // if you use the polyfill, as it's based on `setImmediate` (falling - // back to `setTimeout`), and promise microtasks are run at higher - // priority than either of those. - request = Request(mock, complete).request - mock.$defineRoutes({ - "GET /item": function() { - return {status: 200, responseText: "[]"} - } - }) - return runAsyncTest() - }) - } catch (e) { - // ignore - this is just for browsers that natively support - // `async`/`await`, like most modern browsers. - // it's just a syntax error anyways. - } -}) diff --git a/route.js b/route.js deleted file mode 100644 index 6d9acd68b..000000000 --- a/route.js +++ /dev/null @@ -1,5 +0,0 @@ -"use strict" - -var mountRedraw = require("./mount-redraw") - -module.exports = require("./api/router")(typeof window !== "undefined" ? window : null, mountRedraw) diff --git a/scripts/.eslintrc.js b/scripts/.eslintrc.js deleted file mode 100644 index 9e7393319..000000000 --- a/scripts/.eslintrc.js +++ /dev/null @@ -1,16 +0,0 @@ -"use strict" - -module.exports = { - "extends": "../.eslintrc.js", - "env": { - "browser": null, - "node": true, - "es2022": true, - }, - "parserOptions": { - "ecmaVersion": 2022, - }, - "rules": { - "no-process-env": "off", - }, -}; diff --git a/scripts/_bundler-impl.js b/scripts/_bundler-impl.js deleted file mode 100644 index 49c458d55..000000000 --- a/scripts/_bundler-impl.js +++ /dev/null @@ -1,183 +0,0 @@ -"use strict" - -const fs = require("fs") -const path = require("path") -const execFileSync = require("child_process").execFileSync -const util = require("util") - -const readFile = util.promisify(fs.readFile) -const access = util.promisify(fs.access) - -function isFile(filepath) { - return access(filepath).then(() => true, () => false) -} -function escapeRegExp(string) { - return string.replace(/[|\\{}()[\]^$+*?.-]/g, "\\$&") -} -function escapeReplace(string) { - return string.replace(/\$/g, "\\$&") -} - -async function resolve(filepath, filename) { - if (filename[0] !== ".") { - // resolve as npm dependency - const packagePath = `./node_modules/${filename}/package.json` - let json, meta - - try { - json = await readFile(packagePath, "utf8") - } catch (e) { - meta = {} - } - - if (json) { - try { - meta = JSON.parse(json) - } - catch (e) { - throw new Error(`invalid JSON for ${packagePath}: ${json}`) - } - } - - const main = `./node_modules/${filename}/${meta.main || `${filename}.js`}` - return path.resolve(await isFile(main) ? main : `./node_modules/${filename}/index.js`) - } - else { - // resolve as local dependency - return path.resolve(path.dirname(filepath), filename + ".js") - } -} - -function matchAll(str, regexp) { - regexp.lastIndex = 0 - const result = [] - let exec - while ((exec = regexp.exec(str)) != null) result.push(exec) - return result -} - -let error -module.exports = async (input) => { - const modules = new Map() - const bindings = new Map() - const declaration = /^\s*(?:var|let|const|function)[\t ]+([\w_$]+)/gm - const include = /(?:((?:var|let|const|,|)[\t ]*)([\w_$\.\[\]"'`]+)(\s*=\s*))?require\(([^\)]+)\)(\s*[`\.\(\[])?/gm - let uuid = 0 - async function process(filepath, data) { - for (const [, binding] of matchAll(data, declaration)) bindings.set(binding, 0) - - const tasks = [] - - for (const [, def = "", variable = "", eq = "", dep, rest = ""] of matchAll(data, include)) { - tasks.push({filename: JSON.parse(dep), def, variable, eq, rest}) - } - - const imports = await Promise.all( - tasks.map((t) => resolve(filepath, t.filename)) - ) - - const results = [] - for (const [i, task] of tasks.entries()) { - const dependency = imports[i] - let pre = "", def = task.def - if (def[0] === ",") def = "\nvar ", pre = "\n" - const localUUID = uuid // global uuid can update from nested `process` call, ensure same id is used on declaration and consumption - const existingModule = modules.get(dependency) - modules.set(dependency, task.rest ? `_${localUUID}` : task.variable) - const code = await process( - dependency, - pre + ( - existingModule == null - ? await exportCode(task.filename, dependency, def, task.variable, task.eq, task.rest, localUUID) - : def + task.variable + task.eq + existingModule - ) - ) - uuid++ - results.push(code + task.rest) - } - - let i = 0 - return data.replace(include, () => results[i++]) - } - - async function exportCode(filename, filepath, def, variable, eq, rest, uuid) { - let code = await readFile(filepath, "utf-8") - // if there's a syntax error, report w/ proper stack trace - try { - new Function(code) - } - catch (e) { - try { - execFileSync("node", ["--check", filepath], { - stdio: "pipe", - }) - } - catch (e) { - if (e.message !== error) { - error = e.message - console.log(`\x1b[31m${e.message}\x1b[0m`) - } - } - } - - // disambiguate collisions - const targetPromises = [] - code.replace(include, (match, def, variable, eq, dep) => { - targetPromises.push(resolve(filepath, JSON.parse(dep))) - }) - - const ignoredTargets = await Promise.all(targetPromises) - const ignored = new Set() - - for (const target of ignoredTargets) { - const binding = modules.get(target) - if (binding != null) ignored.add(binding) - } - - if (new RegExp(`module\\.exports\\s*=\\s*${variable}\s*$`, "m").test(code)) ignored.add(variable) - for (const [binding, count] of bindings) { - if (!ignored.has(binding)) { - const before = code - code = code.replace( - new RegExp(`(\\b)${escapeRegExp(binding)}\\b`, "g"), - escapeReplace(binding) + count - ) - if (before !== code) bindings.set(binding, count + 1) - } - } - - // fix strings that got mangled by collision disambiguation - const string = /(["'])((?:\\\1|.)*?)(\1)/g - const candidates = Array.from(bindings, ([binding, count]) => escapeRegExp(binding) + (count - 1)).join("|") - const variables = new RegExp(candidates, "g") - code = code.replace(string, (match, open, data, close) => { - const fixed = data.replace(variables, (match) => match.replace(/\d+$/, "")) - return open + fixed + close - }) - - //fix props - const props = new RegExp(`((?:[^:]\\/\\/.*)?\\.\\s*)(${candidates})|([\\{,]\\s*)(${candidates})(\\s*:)`, "gm") - code = code.replace(props, (match, dot, a, pre, b, post) => { - // Don't do anything because dot was matched in a comment - if (dot && dot.indexOf("//") === 1) return match - if (dot) return dot + a.replace(/\d+$/, "") - return pre + b.replace(/\d+$/, "") + post - }) - - return code - .replace(/("|')use strict\1;?/gm, "") // remove extraneous "use strict" - .replace(/module\.exports\s*=\s*/gm, escapeReplace(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) - } - - const code = ";(function() {\n" + - (await process(path.resolve(input), await readFile(input, "utf-8"))) - .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 - "\n}());" - - //try {new Function(code); console.log(`build completed at ${new Date()}`)} catch (e) {} - error = null - return code -} diff --git a/scripts/build.js b/scripts/build.js new file mode 100644 index 000000000..0b41b5a08 --- /dev/null +++ b/scripts/build.js @@ -0,0 +1,79 @@ +import {fileURLToPath} from "node:url" +import fs from "node:fs/promises" +import {gzipSync} from "node:zlib" +import path from "node:path" + +import {rollup} from "rollup" + +import terser from "@rollup/plugin-terser" + +const dirname = path.dirname(fileURLToPath(import.meta.url)) + +/** @type {Partial>} */ +const terserMinify = { + iife: terser({ + compress: {passes: 3}, + format: {wrap_func_args: false}, + sourceMap: true, + }), + // See the comment in `src/core.js` + esm: terser({ + compress: {passes: 3}, + format: { + preserve_annotations: true, + wrap_func_args: false, + }, + sourceMap: true, + }), +} + +function format(n) { + return n.toString().replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,") +} + +/** @param {import("rollup").ModuleFormat} format */ +async function build(name, format) { + const bundle = await rollup({input: path.resolve(dirname, `../src/entry/${name}`)}) + + try { + await Promise.all([ + bundle.write({file: path.resolve(dirname, `../dist/${name}.js`), format, sourcemap: true}), + bundle.write({file: path.resolve(dirname, `../dist/${name}.min.js`), format, plugins: [terserMinify[format]], sourcemap: true}), + ]) + } finally { + await bundle.close() + } +} + +async function report(file) { + const [original, minified] = await Promise.all([ + fs.readFile(path.resolve(dirname, `../dist/${file}.js`)), + fs.readFile(path.resolve(dirname, `../dist/${file}.min.js`)), + ]) + const originalSize = original.length + const compressedSize = minified.length + const originalGzipSize = gzipSync(original).length + const compressedGzipSize = gzipSync(minified).length + + console.log(`${file}.js:`) + console.log(` Original: ${format(originalGzipSize)} bytes gzipped (${format(originalSize)} bytes uncompressed)`) + console.log(` Minified: ${format(compressedGzipSize)} bytes gzipped (${format(compressedSize)} bytes uncompressed)`) +} + +async function main() { + await fs.rm(path.resolve(dirname, "../dist"), {recursive: true, force: true}) + + await Promise.all([ + build("mithril.umd", "iife"), + build("mithril.esm", "esm"), + build("stream.umd", "iife"), + build("stream.esm", "esm"), + ]) + + await report("mithril.umd") + await report("mithril.esm") + await report("stream.umd") + await report("stream.esm") +} + +main() diff --git a/scripts/bundler-readme.md b/scripts/bundler-readme.md deleted file mode 100644 index 949d1ac9c..000000000 --- a/scripts/bundler-readme.md +++ /dev/null @@ -1,22 +0,0 @@ -# bundler.js - -Simplistic CommonJS module bundler - -Version: 0.1 -License: MIT - -## About - -This bundler attempts to aggressively bundle CommonJS modules by assuming the dependency tree is static, similar to what Rollup does for ES6 modules. - -Most browsers don't support ES6 `import/export` syntax, but we can achieve modularity by using CommonJS module syntax and transpiling it. - -Webpack is conservative and treats CommonJS modules as non-statically-analyzable since `require` and `module.exports` are legally allowed everywhere. Therefore, it must generate extra code to resolve dependencies at runtime (i.e. `__webpack_require()`). Rollup only works with ES6 modules. ES6 modules can be bundled more efficiently because they are statically analyzable, but some use cases are difficult to handle due to ES6's support for cyclic dependencies and hosting rules. This bundler assumes code is written in CommonJS style but follows a strict set of rules that emulate statically analyzable code and favors the usage of the factory pattern instead of relying on obscure corners of the JavaScript language (hoisting rules and binding semantics). - -### Caveats - -- Only supports modules that have the `require` and `module.exports` statement declared at the top-level scope before all other code, i.e. it does not support CommonJS modules that rely on dynamic importing/exporting. This means modules should only export a pure function or export a factory function if there are multiple statements and/or internal module state. The factory function pattern allows easier dependency injection in stateful modules, thus making modules testable. -- Changes the semantics of value/binding exporting between unbundled and bundled code, and therefore relying on those semantics is discouraged. -- Top level strictness is infectious (i.e. if entry file is in `"use strict"` mode, all modules inherit strict mode, and conversely, if the entry file is not in strict mode, all modules are pulled out of strict mode) -- Currently only supports assignments to `module.exports` (i.e. `module.exports.foo = bar` will not work) -- It is tiny and dependency-free because it uses regular expressions, and it only supports the narrow range of import/export declaration patterns outlined above. diff --git a/scripts/bundler.js b/scripts/bundler.js deleted file mode 100644 index 2c3f63333..000000000 --- a/scripts/bundler.js +++ /dev/null @@ -1,70 +0,0 @@ -"use strict" - -const fs = require("fs") -const zlib = require("zlib") -const chokidar = require("chokidar") -const Terser = require("terser") -const util = require("util") - -const readFile = util.promisify(fs.readFile) -const writeFile = util.promisify(fs.writeFile) -const gzip = util.promisify(zlib.gzip) - -const bundle = require("./_bundler-impl") - -const aliases = {o: "output", m: "minify", w: "watch", s: "save"} -const params = Object.create(null) -let command -for (let arg of process.argv.slice(2)) { - if (arg[0] === '"') arg = JSON.parse(arg) - if (arg[0] === "-") { - if (command != null) add(true) - command = arg.replace(/\-+/g, "") - } - else if (command != null) add(arg) - else params.input = arg -} -if (command != null) add(true) - -function add(value) { - params[aliases[command] || command] = value - command = null -} - -function format(n) { - return n.toString().replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,") -} - -async function build() { - const original = await bundle(params.input) - if (!params.minify) { - await writeFile(params.output, original, "utf-8") - return - } - console.log("minifying...") - const minified = Terser.minify(original) - if (minified.error) throw new Error(minified.error) - await writeFile(params.output, minified.code, "utf-8") - const originalSize = Buffer.byteLength(original, "utf-8") - const compressedSize = Buffer.byteLength(minified.code, "utf-8") - const originalGzipSize = (await gzip(original)).byteLength - const compressedGzipSize = (await gzip(minified.code)).byteLength - - console.log("Original size: " + format(originalGzipSize) + " bytes gzipped (" + format(originalSize) + " bytes uncompressed)") - console.log("Compiled size: " + format(compressedGzipSize) + " bytes gzipped (" + format(compressedSize) + " bytes uncompressed)") - - if (params.save) { - const readme = await readFile("./README.md", "utf8") - const kb = compressedGzipSize / 1000 - - await writeFile("./README.md", - readme.replace( - /()(.+?)()/, - "$1" + (kb % 1 ? kb.toFixed(2) : kb) + " KB$3" - ) - ) - } -} - -build() -if (params.watch) chokidar.watch(".", {ignored: params.output}).on("all", build) diff --git a/scripts/minify-stream.js b/scripts/minify-stream.js deleted file mode 100644 index 1ca13281c..000000000 --- a/scripts/minify-stream.js +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env node -/* eslint-disable no-process-exit */ -"use strict" - -process.on("unhandledRejection", (e) => { - process.exitCode = 1 - - if (!e.stdout || !e.stderr) throw e - - console.error(e.stack) - - if (e.stdout?.length) { - console.error(e.stdout.toString("utf-8")) - } - - if (e.stderr?.length) { - console.error(e.stderr.toString("utf-8")) - } - - // eslint-disable-next-line no-process-exit - process.exit() -}) - -const {promises: fs} = require("fs") -const path = require("path") -const zlib = require("zlib") -const Terser = require("terser") - -function format(n) { - return n.toString().replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,") -} - -module.exports = minify -async function minify() { - const input = path.resolve(__dirname, "../stream/stream.js") - const output = path.resolve(__dirname, "../stream/stream.min.js") - const original = await fs.readFile(input, "utf-8") - const minified = Terser.minify(original) - if (minified.error) throw new Error(minified.error) - await fs.writeFile(output, minified.code, "utf-8") - const originalSize = Buffer.byteLength(original, "utf-8") - const compressedSize = Buffer.byteLength(minified.code, "utf-8") - const originalGzipSize = zlib.gzipSync(original).byteLength - const compressedGzipSize = zlib.gzipSync(minified.code).byteLength - - console.log("Original size: " + format(originalGzipSize) + " bytes gzipped (" + format(originalSize) + " bytes uncompressed)") - console.log("Compiled size: " + format(compressedGzipSize) + " bytes gzipped (" + format(compressedSize) + " bytes uncompressed)") -} - -minify() diff --git a/scripts/server.js b/scripts/server.js new file mode 100644 index 000000000..4358f13a8 --- /dev/null +++ b/scripts/server.js @@ -0,0 +1,77 @@ +import * as fs from "node:fs" +import * as http from "node:http" +import * as path from "node:path" +import {fileURLToPath} from "node:url" + +const root = path.dirname(path.dirname(fileURLToPath(import.meta.url))) + +const port = process.argv[2] || "8080" + +if (!(/^[1-9][0-9]*$/).test(port) || Number(port) > 65535) { + console.error("Port must be a non-zero integer at most 65535 if provided") + // eslint-disable-next-line no-process-exit + process.exit(1) +} + +const url = "http://localhost:8080/" +const headers = { + "cache-control": "no-cache, no-store, must-revalidate", + "access-control-allow-origin": url, + "access-control-allow-headers": "origin, x-requested-with, content-type, accept, range", + "cross-origin-opener-policy": "same-origin", + "cross-origin-embedder-policy": "require-corp", +} + +function isDisconnectError(e) { + return (/^EPIPE$|^ECONN(?:RESET|ABORT|REFUSED)$/).test(e) +} + +const server = http.createServer((req, res) => { + const receivedDate = new Date() + + let parsedUrl + + try { + parsedUrl = new URL(req.url, url) + } catch { + res.writeHead(400, headers).end() + + console.log(`[${receivedDate.toISOString()}] ${res.statusCode} - ${req.method} ${req.url} "${req.headers["user-agent"] || ""}"`) + return + } + + const file = path.resolve(root, "." + path.posix.resolve("/", parsedUrl.pathname)) + + let contentType + + if (file.endsWith(".js")) { + contentType = "application/javascript;charset=utf-8" + } else if (file.endsWith(".html")) { + contentType = "text/html;charset=utf-8" + } + + fs.readFile(file, (err, buf) => { + if (!err) { + res.writeHead(200, contentType ? {...headers, "content-type": contentType} : headers).end(buf) + } else if (err.code === "ENOENT") { + res.writeHead(404, headers).end() + } else { + res.writeHead(500, headers).end() + } + + console.log(`[${receivedDate.toISOString()}] ${res.statusCode} - ${req.method} ${req.url} "${req.headers["user-agent"] || ""}"`) + }) +}) + +server.on("error", (e) => { + if (!isDisconnectError(e)) { + console.error(e) + process.exitCode = 1 + } +}) + +server.on("listening", () => { + console.log(`Listening at ${url}`) +}) + +server.listen(Number(port)) diff --git a/scripts/tests/test-bundler.js b/scripts/tests/test-bundler.js deleted file mode 100644 index b9208aa06..000000000 --- a/scripts/tests/test-bundler.js +++ /dev/null @@ -1,296 +0,0 @@ -"use strict" - -const fs = require("fs") -const util = require("util") -const path = require("path") -const access = util.promisify(fs.access) -const writeFile = util.promisify(fs.writeFile) -const unlink = util.promisify(fs.unlink) - -const o = require("ospec") -const bundle = require("../_bundler-impl") - -o.spec("bundler", async () => { - let filesCreated - const root = path.resolve(__dirname, "../..") - const p = (file) => path.join(root, file) - - async function write(filepath, data) { - try { - await access(p(filepath)) - } catch (e) { - return writeFile(p(filepath), data, "utf8") - } - throw new Error(`Don't call \`write('${filepath}')\`. Cannot overwrite file.`) - } - - function setup(files) { - filesCreated = Object.keys(files) - return Promise.all(filesCreated.map((f) => write(f, files[f]))) - } - - o.afterEach(() => Promise.all( - filesCreated.map((filepath) => unlink(p(filepath))) - )) - - o("relative imports works", async () => { - await setup({ - "a.js": 'var b = require("./b")', - "b.js": "module.exports = 1", - }) - - o(await bundle(p("a.js"))).equals(";(function() {\nvar b = 1\n}());") - }) - o("relative imports works with semicolons", async () => { - await setup({ - "a.js": 'var b = require("./b");', - "b.js": "module.exports = 1;", - }) - - o(await bundle(p("a.js"))).equals(";(function() {\nvar b = 1;\n}());") - }) - o("relative imports works with let", async () => { - await setup({ - "a.js": 'let b = require("./b")', - "b.js": "module.exports = 1", - }) - - o(await bundle(p("a.js"))).equals(";(function() {\nlet b = 1\n}());") - }) - o("relative imports works with const", async () => { - await setup({ - "a.js": 'const b = require("./b")', - "b.js": "module.exports = 1", - }) - - o(await bundle(p("a.js"))).equals(";(function() {\nconst b = 1\n}());") - }) - o("relative imports works with assignment", async () => { - await setup({ - "a.js": 'var a = {}\na.b = require("./b")', - "b.js": "module.exports = 1", - }) - - o(await bundle(p("a.js"))).equals(";(function() {\nvar a = {}\na.b = 1\n}());") - }) - o("relative imports works with reassignment", async () => { - await setup({ - "a.js": 'var b = {}\nb = require("./b")', - "b.js": "module.exports = 1", - }) - - o(await bundle(p("a.js"))).equals(";(function() {\nvar b = {}\nb = 1\n}());") - }) - o("relative imports removes extra use strict", async () => { - await setup({ - "a.js": '"use strict"\nvar b = require("./b")', - "b.js": '"use strict"\nmodule.exports = 1', - }) - - o(await bundle(p("a.js"))).equals(';(function() {\n"use strict"\nvar b = 1\n}());') - }) - o("relative imports removes extra use strict using single quotes", async () => { - await setup({ - "a.js": "'use strict'\nvar b = require(\"./b\")", - "b.js": "'use strict'\nmodule.exports = 1", - }) - - o(await bundle(p("a.js"))).equals(";(function() {\n'use strict'\nvar b = 1\n}());") - }) - o("relative imports removes extra use strict using mixed quotes", async () => { - await setup({ - "a.js": '"use strict"\nvar b = require("./b")', - "b.js": "'use strict'\nmodule.exports = 1", - }) - - o(await bundle(p("a.js"))).equals(';(function() {\n"use strict"\nvar b = 1\n}());') - }) - o("works w/ window", async () => { - await setup({ - "a.js": 'window.a = 1\nvar b = require("./b")', - "b.js": "module.exports = function() {return a}", - }) - - o(await bundle(p("a.js"))).equals(";(function() {\nwindow.a = 1\nvar b = function() {return a}\n}());") - }) - o("works without assignment", async () => { - await setup({ - "a.js": 'require("./b")', - "b.js": "1 + 1", - }) - - o(await bundle(p("a.js"))).equals(";(function() {\n1 + 1\n}());") - }) - o("works if used fluently", async () => { - await setup({ - "a.js": 'var b = require("./b").toString()', - "b.js": "module.exports = []", - }) - - o(await bundle(p("a.js"))).equals(";(function() {\nvar _0 = []\nvar b = _0.toString()\n}());") - }) - o("works if used fluently w/ multiline", async () => { - await setup({ - "a.js": 'var b = require("./b")\n\t.toString()', - "b.js": "module.exports = []", - }) - - o(await bundle(p("a.js"))).equals(";(function() {\nvar _0 = []\nvar b = _0\n\t.toString()\n}());") - }) - o("works if used w/ curry", async () => { - await setup({ - "a.js": 'var b = require("./b")()', - "b.js": "module.exports = function() {}", - }) - - o(await bundle(p("a.js"))).equals(";(function() {\nvar _0 = function() {}\nvar b = _0()\n}());") - }) - o("works if used w/ curry w/ multiline", async () => { - await setup({ - "a.js": 'var b = require("./b")\n()', - "b.js": "module.exports = function() {}", - }) - - o(await bundle(p("a.js"))).equals(";(function() {\nvar _0 = function() {}\nvar b = _0\n()\n}());") - }) - o("works if used fluently in one place and not in another", async () => { - await setup({ - "a.js": 'var b = require("./b").toString()\nvar c = require("./c")', - "b.js": "module.exports = []", - "c.js": 'var b = require("./b")\nmodule.exports = function() {return b}', - }) - - o(await bundle(p("a.js"))).equals(";(function() {\nvar _0 = []\nvar b = _0.toString()\nvar b0 = _0\nvar c = function() {return b0}\n}());") - }) - o("works if used in sequence", async () => { - await setup({ - "a.js": 'var b = require("./b"), c = require("./c")', - "b.js": "module.exports = 1", - "c.js": "var x\nmodule.exports = 2", - }) - - o(await bundle(p("a.js"))).equals(";(function() {\nvar b = 1\nvar x\nvar c = 2\n}());") - }) - o("works if assigned to property", async () => { - await setup({ - "a.js": 'var x = {}\nx.b = require("./b")\nx.c = require("./c")', - "b.js": "var bb = 1\nmodule.exports = bb", - "c.js": "var cc = 2\nmodule.exports = cc", - }) - - o(await bundle(p("a.js"))).equals(";(function() {\nvar x = {}\nvar bb = 1\nx.b = bb\nvar cc = 2\nx.c = cc\n}());") - }) - o("works if assigned to property using bracket notation", async () => { - await setup({ - "a.js": 'var x = {}\nx["b"] = require("./b")\nx["c"] = require("./c")', - "b.js": "var bb = 1\nmodule.exports = bb", - "c.js": "var cc = 2\nmodule.exports = cc", - }) - - o(await bundle(p("a.js"))).equals(';(function() {\nvar x = {}\nvar bb = 1\nx["b"] = bb\nvar cc = 2\nx["c"] = cc\n}());') - }) - o("works if collision", async () => { - await setup({ - "a.js": 'var b = require("./b")', - "b.js": "var b = 1\nmodule.exports = 2", - }) - - o(await bundle(p("a.js"))).equals(";(function() {\nvar b0 = 1\nvar b = 2\n}());") - }) - o("works if multiple aliases", async () => { - await setup({ - "a.js": 'var b = require("./b")\n', - "b.js": 'var b = require("./c")\nb.x = 1\nmodule.exports = b', - "c.js": "var b = {}\nmodule.exports = b", - }) - - o(await bundle(p("a.js"))).equals(";(function() {\nvar b = {}\nb.x = 1\n}());") - }) - o("works if multiple collision", async () => { - await setup({ - "a.js": 'var b = require("./b")\nvar c = require("./c")\nvar d = require("./d")', - "b.js": "var a = 1\nmodule.exports = a", - "c.js": "var a = 2\nmodule.exports = a", - "d.js": "var a = 3\nmodule.exports = a", - }) - - o(await bundle(p("a.js"))).equals(";(function() {\nvar a = 1\nvar b = a\nvar a0 = 2\nvar c = a0\nvar a1 = 3\nvar d = a1\n}());") - }) - o("works if included multiple times", async () => { - await setup({ - "a.js": "module.exports = 123", - "b.js": 'var a = require("./a").toString()\nmodule.exports = a', - "c.js": 'var a = require("./a").toString()\nvar b = require("./b")', - }) - - o(await bundle(p("c.js"))).equals(";(function() {\nvar _0 = 123\nvar a = _0.toString()\nvar a0 = _0.toString()\nvar b = a0\n}());") - }) - o("works if included multiple times reverse", async () => { - await setup({ - "a.js": "module.exports = 123", - "b.js": 'var a = require("./a").toString()\nmodule.exports = a', - "c.js": 'var b = require("./b")\nvar a = require("./a").toString()', - }) - - o(await bundle(p("c.js"))).equals(";(function() {\nvar _0 = 123\nvar a0 = _0.toString()\nvar b = a0\nvar a = _0.toString()\n}());") - }) - o("reuses binding if possible", async () => { - await setup({ - "a.js": 'var b = require("./b")\nvar c = require("./c")', - "b.js": 'var d = require("./d")\nmodule.exports = function() {return d + 1}', - "c.js": 'var d = require("./d")\nmodule.exports = function() {return d + 2}', - "d.js": "module.exports = 1", - }) - - o(await bundle(p("a.js"))).equals(";(function() {\nvar d = 1\nvar b = function() {return d + 1}\nvar c = function() {return d + 2}\n}());") - }) - o("disambiguates conflicts if imported collides with itself", async () => { - await setup({ - "a.js": 'var b = require("./b")', - "b.js": "var b = 1\nmodule.exports = function() {return b}", - }) - - o(await bundle(p("a.js"))).equals(";(function() {\nvar b0 = 1\nvar b = function() {return b0}\n}());") - }) - o("disambiguates conflicts if imported collides with something else", async () => { - await setup({ - "a.js": 'var a = 1\nvar b = require("./b")', - "b.js": "var a = 2\nmodule.exports = function() {return a}", - }) - - o(await bundle(p("a.js"))).equals(";(function() {\nvar a = 1\nvar a0 = 2\nvar b = function() {return a0}\n}());") - }) - o("disambiguates conflicts if imported collides with function declaration", async () => { - await setup({ - "a.js": 'function a() {}\nvar b = require("./b")', - "b.js": "var a = 2\nmodule.exports = function() {return a}", - }) - - o(await bundle(p("a.js"))).equals(";(function() {\nfunction a() {}\nvar a0 = 2\nvar b = function() {return a0}\n}());") - }) - o("disambiguates conflicts if imported collides with another module's private", async () => { - await setup({ - "a.js": 'var b = require("./b")\nvar c = require("./c")', - "b.js": "var a = 1\nmodule.exports = function() {return a}", - "c.js": "var a = 2\nmodule.exports = function() {return a}", - }) - - o(await bundle(p("a.js"))).equals(";(function() {\nvar a = 1\nvar b = function() {return a}\nvar a0 = 2\nvar c = function() {return a0}\n}());") - }) - o("does not mess up strings", async () => { - await setup({ - "a.js": 'var b = require("./b")', - "b.js": 'var b = "b b b \\" b"\nmodule.exports = function() {return b}', - }) - - o(await bundle(p("a.js"))).equals(';(function() {\nvar b0 = "b b b \\\" b"\nvar b = function() {return b0}\n}());') - }) - o("does not mess up properties", async () => { - await setup({ - "a.js": 'var b = require("./b")', - "b.js": "var b = {b: 1}\nmodule.exports = function() {return b.b}", - }) - - o(await bundle(p("a.js"))).equals(";(function() {\nvar b0 = {b: 1}\nvar b = function() {return b0.b}\n}());") - }) -}) diff --git a/src/core.js b/src/core.js new file mode 100644 index 000000000..44e53c205 --- /dev/null +++ b/src/core.js @@ -0,0 +1,1197 @@ +/* eslint-disable no-bitwise */ +import {checkCallback, hasOwn, invokeRedrawable, noop} from "./util.js" + +export {m as default} + +/* +Caution: be sure to check the minified output. I've noticed an issue with Terser trying to inline +single-use functions as IIFEs, and this predictably causes perf issues since engines don't seem to +reliably lower this in either their bytecode generation *or* their optimized code. + +Rather than painfully trying to reduce that to an MVC and filing a bug against it, I'm just +inlining and commenting everything. It also gives me a better idea of the true cost of various +functions. + +In `m`, I do use a no-inline hints (the `__NOINLINE__` in an inline block comment there) to +prevent Terser from inlining a cold function in a very hot code path, to try to squeeze a little +more performance out of the framework. Likewise, to try to preserve this through build scripts, +Terser annotations are preserved in the ESM production bundle (but not the UMD bundle). + +Also, be aware: I use some bit operations here. Nothing super fancy like find-first-set, just +mainly ANDs, ORs, and a one-off XOR for inequality. +*/ + +/* +State note: + +If remove on throw is `true` and an error occurs: +- All visited vnodes' new versions are removed. +- All unvisited vnodes' old versions are removed. + +If remove on throw is `false` and an error occurs: +- Attribute modification errors are logged. +- Views that throw retain the previous version and log their error. +- Errors other than the above cause the tree to be torn down as if remove on throw was `true`. +*/ + +/* +This same structure is used for several nodes. Here's an explainer for each type. + +Retain: +- `m`: `-1` +- All other properties are unused +- On ingest, the vnode itself is converted into the type of the element it's retaining. This + includes changing its type. + +Fragments: +- `m` bits 0-3: `0` +- `t`: unused +- `s`: unused +- `a`: unused +- `c`: virtual DOM children +- `d`: unused + +Keyed: +- `m` bits 0-3: `1` +- `t`: unused +- `s`: unused +- `a`: key to child map, also holds children +- `c`: unused +- `d`: unused + +Text: +- `m` bits 0-3: `2` +- `t`: unused +- `s`: unused +- `a`: text string +- `c`: unused +- `d`: unused + +Components: +- `m` bits 0-3: `3` +- `t`: component reference +- `s`: view function, may be same as component reference +- `a`: most recently received attributes +- `c`: instance vnode +- `d`: unused + +DOM elements: +- `m` bits 0-3: `4` +- `t`: tag name string +- `s`: event listener dictionary, if any events were ever registered +- `a`: most recently received attributes +- `c`: virtual DOM children +- `d`: element reference + +Layout: +- `m` bits 0-3: `5` +- `t`: unused +- `s`: uncaught +- `a`: callback to schedule +- `c`: unused +- `d`: parent DOM reference, for easier queueing + +Remove: +- `m` bits 0-3: `6` +- `t`: unused +- `s`: unused +- `a`: callback to schedule +- `c`: unused +- `d`: parent DOM reference, for easier queueing + +Set context: +- `m` bits 0-3: `7` +- `t`: unused +- `s`: unused +- `a`: unused +- `c`: virtual DOM children +- `d`: unused + +Use dependencies: +- `m` bits 0-3: `8` +- `t`: unused +- `s`: unused +- `a`: Dependency array +- `c`: virtual DOM children +- `d`: unused + +Inline: +- `m` bits 0-3: `8` +- `t`: unused +- `s`: unused +- `a`: view function +- `c`: instance vnode +- `d`: unused + +The `m` field is also used for various assertions, that aren't described here. +*/ + +var TYPE_MASK = 15 +var TYPE_RETAIN = -1 +var TYPE_FRAGMENT = 0 +var TYPE_KEYED = 1 +var TYPE_TEXT = 2 +var TYPE_ELEMENT = 3 +var TYPE_COMPONENT = 4 +var TYPE_LAYOUT = 5 +var TYPE_REMOVE = 6 +var TYPE_SET_CONTEXT = 7 +var TYPE_USE = 8 +var TYPE_INLINE = 9 +// var TYPE_RETAIN = 15 + +var FLAG_USED = 1 << 4 +var FLAG_IS_REMOVE = 1 << 5 +var FLAG_HTML_ELEMENT = 1 << 6 +var FLAG_CUSTOM_ELEMENT = 1 << 7 +var FLAG_INPUT_ELEMENT = 1 << 8 +var FLAG_SELECT_ELEMENT = 1 << 9 +var FLAG_OPTION_ELEMENT = 1 << 10 +var FLAG_TEXTAREA_ELEMENT = 1 << 11 +var FLAG_IS_FILE_INPUT = 1 << 12 +// Implicitly used as part of checking for `m.retain()`. +// var FLAG_IS_RETAIN = 1 << 31 + +var Vnode = (mask, tag, attrs, children) => ({ + m: mask, + t: tag, + a: attrs, + c: children, + s: null, + d: null, +}) + +var selectorParser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[(.+?)(?:\s*=\s*("|'|)((?:\\["'\]]|.)*?)\5)?\])/g +var selectorUnescape = /\\(["'\\])/g +var selectorCache = /*@__PURE__*/ new Map() + +var compileSelector = (selector) => { + var match, tag = "div", classes = [], attrs = {}, className, hasAttrs = false + + while (match = selectorParser.exec(selector)) { + var type = match[1], value = match[2] + if (type === "" && value !== "") { + tag = value + } else { + hasAttrs = true + if (type === "#") { + attrs.id = value + } else if (type === ".") { + classes.push(value) + } else if (match[3][0] === "[") { + var attrValue = match[6] + if (attrValue) attrValue = attrValue.replace(selectorUnescape, "$1") + if (match[4] === "class" || match[4] === "className") classes.push(attrValue) + else attrs[match[4]] = attrValue == null || attrValue + } + } + } + + if (classes.length > 0) { + className = classes.join(" ") + } + + var state = {t: tag, a: hasAttrs ? attrs : null, c: className} + selectorCache.set(selector, state) + return state +} + +/* +Edit this with caution and profile every change you make. This comprises about 4% of the total +runtime overhead in benchmarks, and any reduction in performance here will immediately be felt. + +Also, it's specially designed to only allocate the bare minimum it needs to build vnodes, as part +of this optimization process. It doesn't allocate arguments except as needed to build children, it +doesn't allocate attributes except to replace them for modifications, among other things. +*/ +var m = function (selector, attrs) { + var type = TYPE_ELEMENT + var start = 1 + var children + + if (typeof selector !== "string") { + if (typeof selector !== "function") { + throw new Error("The selector must be either a string or a component."); + } + type = selector === m.Fragment ? TYPE_FRAGMENT : TYPE_COMPONENT + } + + + if (attrs == null || typeof attrs === "object" && typeof attrs.m !== "number" && !Array.isArray(attrs)) { + start = 2 + if (arguments.length < 3 && attrs && Array.isArray(attrs.children)) { + children = attrs.children.slice() + } + } else { + attrs = null + } + + if (children == null) { + if (arguments.length === start + 1 && Array.isArray(arguments[start])) { + children = arguments[start].slice() + } else { + children = [] + while (start < arguments.length) children.push(arguments[start++]) + } + } + + // It may seem expensive to inline elements handling, but it's less expensive than you'd think. + // DOM nodes are about as commonly constructed as vnodes, but fragments are only constructed + // from JSX code (and even then, they aren't common). + + if (type === TYPE_ELEMENT) { + attrs = attrs || {} + var hasClassName = hasOwn.call(attrs, "className") + var dynamicClass = hasClassName ? attrs.className : attrs.class + var state = selectorCache.get(selector) + var original = attrs + + if (state == null) { + state = /*@__NOINLINE__*/compileSelector(selector) + } + + if (state.a != null) { + attrs = {...state.a, ...attrs} + } + + if (dynamicClass != null || state.c != null) { + if (attrs !== original) attrs = {...attrs} + attrs.class = dynamicClass != null + ? state.c != null ? `${state.c} ${dynamicClass}` : dynamicClass + : state.c + if (hasClassName) attrs.className = null + } + } + + if (type === TYPE_COMPONENT) { + attrs = {children, ...attrs} + children = null + } else { + for (var i = 0; i < children.length; i++) children[i] = m.normalize(children[i]) + } + + return Vnode(type, selector, attrs, children) +} + +m.TYPE_MASK = TYPE_MASK +m.TYPE_RETAIN = TYPE_RETAIN +m.TYPE_FRAGMENT = TYPE_FRAGMENT +m.TYPE_KEYED = TYPE_KEYED +m.TYPE_TEXT = TYPE_TEXT +m.TYPE_ELEMENT = TYPE_ELEMENT +m.TYPE_COMPONENT = TYPE_COMPONENT +m.TYPE_LAYOUT = TYPE_LAYOUT +m.TYPE_REMOVE = TYPE_REMOVE +m.TYPE_SET_CONTEXT = TYPE_SET_CONTEXT +m.TYPE_USE = TYPE_USE +m.TYPE_INLINE = TYPE_INLINE + +// Simple and sweet. Also useful for idioms like `onfoo: m.capture` to completely drop events while +// otherwise ignoring them. +m.capture = (ev) => { + if (typeof ev === "function") { + return (...args) => { + m.capture(...args) + return ev(...args) + } + } + ev.preventDefault() + ev.stopPropagation() + return "skip-redraw" +} + +m.retain = () => Vnode(TYPE_RETAIN, null, null, null) +m.inline = (view) => Vnode(TYPE_INLINE, null, checkCallback(view, false, "view"), null) +m.layout = (callback) => Vnode(TYPE_LAYOUT, null, checkCallback(callback), null) +m.remove = (callback) => Vnode(TYPE_REMOVE, null, checkCallback(callback), null) + +m.Fragment = (attrs) => attrs.children + +m.keyed = (values, view) => { + view = checkCallback(view, true, "view") + var map = new Map() + for (var value of values) { + if (typeof view === "function") value = view(value) + if (value != null && typeof value !== "boolean") { + if (!Array.isArray(value) || value.length < 1) { + throw new TypeError("Returned value must be a `[key, value]` array") + } + if (map.has(value[0])) { + // Coerce to string so symbols don't throw + throw new TypeError(`Duplicate key detected: ${String(value[0])}`) + } + map.set(value[0], m.normalize(value[1])) + } + } + return Vnode(TYPE_KEYED, null, map, null) +} + +m.set = (entries, ...children) => resolveSpecialFragment(TYPE_SET_CONTEXT, entries, ...children) +m.use = (deps, ...children) => resolveSpecialFragment(TYPE_USE, [...deps], ...children) + +m.normalize = (node) => { + if (node == null || typeof node === "boolean") return null + if (typeof node !== "object") return Vnode(TYPE_TEXT, null, String(node), null) + if (Array.isArray(node)) return Vnode(TYPE_FRAGMENT, null, null, node.map(m.normalize)) + return node +} + +var resolveSpecialFragment = (type, attrs, ...children) => { + var resolved = children.length === 1 && Array.isArray(children[0]) ? [...children[0]] : [...children] + for (var i = 0; i < resolved.length; i++) resolved[i] = m.normalize(resolved[i]) + return Vnode(type, null, attrs, resolved) +} + +var xlinkNs = "http://www.w3.org/1999/xlink" +var htmlNs = "http://www.w3.org/1999/xhtml" +var nameSpace = { + svg: "http://www.w3.org/2000/svg", + math: "http://www.w3.org/1998/Math/MathML" +} + +var currentHooks +var currentRedraw +var currentParent +var currentRefNode +var currentNamespace +var currentDocument +var currentContext +var currentRemoveOnThrow + +var insertAfterCurrentRefNode = (child) => { + if (currentRefNode) { + currentRefNode.after(currentRefNode = child) + } else { + currentParent.prepend(currentRefNode = child) + } +} + +//update +var moveToPosition = (vnode) => { + var type + while ((1 << TYPE_COMPONENT | 1 << TYPE_INLINE) & 1 << (type = vnode.m & TYPE_MASK)) { + if (!(vnode = vnode.c)) return + } + if ((1 << TYPE_FRAGMENT | 1 << TYPE_USE | 1 << TYPE_SET_CONTEXT) & 1 << type) { + vnode.c.forEach(moveToPosition) + } else if ((1 << TYPE_TEXT | 1 << TYPE_ELEMENT) & 1 << type) { + insertAfterCurrentRefNode(vnode.d) + } else if (type === TYPE_KEYED) { + vnode.a.forEach(moveToPosition) + } +} + +var updateFragment = (old, vnode) => { + // Patch the common prefix, remove the extra in the old, and create the extra in the new. + // + // Can't just take the max of both, because out-of-bounds accesses both disrupts + // optimizations and is just generally slower. + // + // Note: if either `vnode` or `old` is `null`, the common length and its own length are + // both zero, so it can't actually throw. + var newLength = vnode != null ? vnode.c.length : 0 + var oldLength = old != null ? old.c.length : 0 + var commonLength = oldLength < newLength ? oldLength : newLength + try { + for (var i = 0; i < commonLength; i++) updateNode(old.c[i], vnode.c[i]) + for (var i = commonLength; i < newLength; i++) updateNode(null, vnode.c[i]) + } catch (e) { + commonLength = i + for (var i = 0; i < commonLength; i++) updateNode(vnode.c[i], null) + for (var i = commonLength; i < oldLength; i++) updateNode(old.c[i], null) + throw e + } + for (var i = commonLength; i < oldLength; i++) updateNode(old.c[i], null) +} + +var updateUse = (old, vnode) => { + if ( + old != null && old.length !== 0 && + vnode != null && vnode.length !== 0 && + ( + vnode.a.length !== old.a.length || + vnode.a.some((b, i) => !Object.is(b, old.a[i])) + ) + ) { + updateFragment(old, null) + old = null + } + updateFragment(old, vnode) +} + +var updateKeyed = (old, vnode) => { + // I take a pretty straightforward approach here to keep it simple: + // 1. Build a map from old map to old vnode. + // 2. Walk the new vnodes, adding what's missing and patching what's in the old. + // 3. Remove from the old map the keys in the new vnodes, leaving only the keys that + // were removed this run. + // 4. Remove the remaining nodes in the old map that aren't in the new map. Since the + // new keys were already deleted, this is just a simple map iteration. + + // Note: if either `vnode` or `old` is `null`, they won't get here. The default mask is + // zero, and that causes keyed state to differ and thus a forced linear diff per above. + + var added = 0 + // It's a value that 1. isn't user-providable and 2. isn't likely to go away in future changes. + // Works well enough as a sentinel. + var error = selectorCache + try { + // Iterate the map. I get keys for free that way, and insertion order is guaranteed to be + // preserved in any spec-conformant engine. + vnode.a.forEach((n, k) => { + var p = old != null ? old.a.get(k) : null + if (p == null) { + updateNode(null, n) + } else { + var prev = currentRefNode + moveToPosition(p) + currentRefNode = prev + updateNode(p, n) + // Delete from the state set, but only after it's been successfully moved. This + // avoids needing to specially remove `p` on failure. + old.a.delete(k) + } + added++ + }) + added = -1 + } catch (e) { + error = e + } + if (old != null) removeKeyed(old) + // Either `added === 0` from the `catch` block or `added === -1` from completing the loop. + if (error !== selectorCache) { + for (var n of vnode.a.values()) { + if (--added) break + updateNode(n, null) + } + throw error + } +} + +var updateNode = (old, vnode) => { + // This is important. Declarative state bindings that rely on dependency tracking, like + // https://github.com/tc39/proposal-signals and related, memoize their results, but that's the + // absolute extent of what they necessarily reuse. They don't pool anything. That means all I + // need to do to support components based on them is just add this neat single line of code + // here. + // + // Code based on streams (see this repo here) will also potentially need this depending on how + // they do their combinators. + if (old === vnode) return + + var type + if (old == null) { + if (vnode == null) return + if (vnode.m < 0) return + if (vnode.m & FLAG_USED) { + throw new TypeError("Vnodes must not be reused") + } + type = vnode.m & TYPE_MASK + vnode.m |= FLAG_USED + } else { + type = old.m & TYPE_MASK + + if (vnode == null) { + try { + if (type !== (TYPE_RETAIN & TYPE_MASK)) removeNodeDispatch[type](old) + } catch (e) { + console.error(e) + } + return + } + + if (vnode.m < 0) { + // If it's a retain node, transmute it into the node it's retaining. Makes it much easier + // to implement and work with. + // + // Note: this key list *must* be complete. + vnode.m = old.m + vnode.t = old.t + vnode.s = old.s + vnode.a = old.a + vnode.c = old.c + vnode.d = old.d + return + } + + if (vnode.m & FLAG_USED) { + throw new TypeError("Vnodes must not be reused") + } + + if (type === (vnode.m & TYPE_MASK) && vnode.t === old.t) { + vnode.m = old.m + } else { + updateNode(old, null) + old = null + } + type = vnode.m & TYPE_MASK + } + + try { + updateNodeDispatch[type](old, vnode) + } catch (e) { + updateNode(old, null) + throw e + } +} + +var updateLayout = (_, vnode) => { + vnode.d = currentParent + currentHooks.push(vnode) +} + +var updateRemove = (_, vnode) => { + vnode.d = currentParent +} + +var emptyObject = {} + +var updateSet = (old, vnode) => { + var descs = Object.getOwnPropertyDescriptors(vnode.a) + for (var key of Reflect.ownKeys(descs)) { + // Drop the descriptor entirely if it's not enumerable. Setting it to an empty object + // avoids changing its shape, which is useful. + if (!descs[key].enumerable) descs[key] = emptyObject + // Drop the setter if one is present, to keep it read-only. + else if ("set" in descs[key]) descs[key].set = undefined + } + var prevContext = currentContext + currentContext = Object.freeze(Object.create(prevContext, descs)) + updateFragment(old, vnode) + currentContext = prevContext +} + +var updateText = (old, vnode) => { + if (old == null) { + insertAfterCurrentRefNode(vnode.d = currentDocument.createTextNode(vnode.a)) + } else { + if (`${old.a}` !== `${vnode.a}`) old.d.nodeValue = vnode.a + vnode.d = currentRefNode = old.d + } +} + +var handleAttributeError = (old, e, force) => { + if (currentRemoveOnThrow || force) { + if (old) removeElement(old) + throw e + } + console.error(e) +} + +var updateElement = (old, vnode) => { + var prevParent = currentParent + var prevRefNode = currentRefNode + var prevNamespace = currentNamespace + var mask = vnode.m + var attrs = vnode.a + var element, oldAttrs + + if (old == null) { + var entry = selectorCache.get(vnode.t) + var tag = entry ? entry.t : vnode.t + var customTag = tag.includes("-") + var is = !customTag && attrs && attrs.is + var ns = attrs && attrs.xmlns || nameSpace[tag] || prevNamespace + var opts = is ? {is} : null + + element = ( + ns + ? currentDocument.createElementNS(ns, tag, opts) + : currentDocument.createElement(tag, opts) + ) + + if (ns == null) { + // Doing it this way since it doesn't seem Terser is smart enough to optimize the `if` with + // every branch doing `a |= value` for differing `value`s to a ternary. It *is* smart + // enough to inline the constants, and the following pass optimizes the rest to just + // integers. + // + // Doing a simple constant-returning ternary also makes it easier for engines to emit the + // right code. + /* eslint-disable indent */ + vnode.m = mask |= ( + is || customTag + ? FLAG_HTML_ELEMENT | FLAG_CUSTOM_ELEMENT + : (tag = tag.toUpperCase(), ( + tag === "INPUT" ? FLAG_HTML_ELEMENT | FLAG_INPUT_ELEMENT + : tag === "SELECT" ? FLAG_HTML_ELEMENT | FLAG_SELECT_ELEMENT + : tag === "OPTION" ? FLAG_HTML_ELEMENT | FLAG_OPTION_ELEMENT + : tag === "TEXTAREA" ? FLAG_HTML_ELEMENT | FLAG_TEXTAREA_ELEMENT + : FLAG_HTML_ELEMENT + )) + ) + /* eslint-enable indent */ + + if (is) element.setAttribute("is", is) + } + + currentParent = element + currentNamespace = ns + } else { + vnode.s = old.s + oldAttrs = old.a + currentNamespace = (currentParent = element = vnode.d = old.d).namespaceURI + if (currentNamespace === htmlNs) currentNamespace = null + } + + currentRefNode = null + + try { + if (oldAttrs != null && oldAttrs === attrs) { + throw new Error("Attributes object cannot be reused.") + } + + if (attrs != null) { + // The DOM does things to inputs based on the value, so it needs set first. + // See: https://github.com/MithrilJS/mithril.js/issues/2622 + if (mask & FLAG_INPUT_ELEMENT && attrs.type != null) { + if (attrs.type === "file") mask |= FLAG_IS_FILE_INPUT + element.type = attrs.type + } + + for (var key in attrs) { + setAttr(vnode, element, mask, key, oldAttrs, attrs) + } + } + + for (var key in oldAttrs) { + mask |= FLAG_IS_REMOVE + setAttr(vnode, element, mask, key, oldAttrs, attrs) + } + } catch (e) { + return handleAttributeError(old, e, true) + } + + updateFragment(old, vnode) + + if (mask & FLAG_SELECT_ELEMENT && old == null) { + try { + // This does exactly what I want, so I'm reusing it to save some code + var normalized = getStyleKey(attrs, "value") + if ("value" in attrs) { + if (normalized === null) { + if (element.selectedIndex >= 0) { + element.value = null + } + } else { + if (element.selectedIndex < 0 || element.value !== normalized) { + element.value = normalized + } + } + } + } catch (e) { + handleAttributeError(old, e, false) + } + + try { + // This does exactly what I want, so I'm reusing it to save some code + var normalized = getPropKey(attrs, "selectedIndex") + if (normalized !== null) { + element.selectedIndex = normalized + } + } catch (e) { + handleAttributeError(old, e, false) + } + } + + currentParent = prevParent + currentRefNode = prevRefNode + currentNamespace = prevNamespace + + // Do this as late as possible to reduce how much work browsers have to do to reduce style + // recalcs during initial (sub)tree construction. Also will defer `adoptNode` callbacks in + // custom elements until the last possible point (which will help accelerate some of them). + if (old == null) { + insertAfterCurrentRefNode(vnode.d = element) + } + + currentRefNode = element +} + +var updateComponent = (old, vnode) => { + try { + var attrs = vnode.a + var tree, oldInstance, oldAttrs + rendered: { + if (old != null) { + tree = old.s + oldInstance = old.c + oldAttrs = old.a + } else if (typeof (tree = (vnode.s = vnode.t).call(currentContext, attrs, oldAttrs)) !== "function") { + break rendered + } + tree = (vnode.s = tree).call(currentContext, attrs, oldAttrs) + } + updateNode(oldInstance, vnode.c = m.normalize(tree)) + } catch (e) { + if (currentRemoveOnThrow) throw e + console.error(e) + } +} + +var updateInline = (old, vnode) => { + try { + updateNode(old != null ? old.c : null, vnode.c = m.normalize(vnode.a.call(currentContext, currentContext))) + } catch (e) { + if (currentRemoveOnThrow) throw e + console.error(e) + } +} + +var removeFragment = (old) => updateFragment(old, null) + +var removeKeyed = (old) => old.a.forEach((p) => updateNode(p, null)) + +var removeNode = (old) => { + try { + if (!old.d) return + old.d.remove() + old.d = null + } catch (e) { + console.error(e) + } +} + +var removeElement = (old) => { + removeNode(old) + updateFragment(old, null) +} + +var removeInstance = (old) => updateNode(old.c, null) + +// Replaces an otherwise necessary `switch`. +var updateNodeDispatch = [ + updateFragment, + updateKeyed, + updateText, + updateElement, + updateComponent, + updateLayout, + updateRemove, + updateSet, + updateUse, + updateInline, +] + +var removeNodeDispatch = [ + removeFragment, + removeKeyed, + removeNode, + removeElement, + removeInstance, + noop, + (old) => currentHooks.push(old), + removeFragment, + removeFragment, + removeInstance, +] + +//attrs + +/* eslint-disable no-unused-vars */ +var ASCII_HYPHEN = 0x2D +var ASCII_COLON = 0x3A +var ASCII_LOWER_A = 0x61 +var ASCII_LOWER_B = 0x62 +var ASCII_LOWER_C = 0x63 +var ASCII_LOWER_D = 0x64 +var ASCII_LOWER_E = 0x65 +var ASCII_LOWER_F = 0x66 +var ASCII_LOWER_G = 0x67 +var ASCII_LOWER_H = 0x68 +var ASCII_LOWER_I = 0x69 +var ASCII_LOWER_J = 0x6A +var ASCII_LOWER_K = 0x6B +var ASCII_LOWER_L = 0x6C +var ASCII_LOWER_M = 0x6D +var ASCII_LOWER_N = 0x6E +var ASCII_LOWER_O = 0x6F +var ASCII_LOWER_P = 0x70 +var ASCII_LOWER_Q = 0x71 +var ASCII_LOWER_R = 0x72 +var ASCII_LOWER_S = 0x73 +var ASCII_LOWER_T = 0x74 +var ASCII_LOWER_U = 0x75 +var ASCII_LOWER_V = 0x76 +var ASCII_LOWER_W = 0x77 +var ASCII_LOWER_X = 0x78 +var ASCII_LOWER_Y = 0x79 +var ASCII_LOWER_Z = 0x7A +/* eslint-enable no-unused-vars */ + +var getPropKey = (host, key) => { + if (host != null && hasOwn.call(host, key)) { + var value = host[key] + if (value !== false && value != null) return value + } + return null +} + +var getStyleKey = (host, key) => { + if (host != null && hasOwn.call(host, key)) { + var value = host[key] + if (value !== false && value != null) return `${value}` + } + return null +} + +var setStyle = (style, old, value, add) => { + for (var propName in value) { + var preferSetter = propName.charCodeAt(0) === ASCII_HYPHEN + var propValue = getStyleKey(value, propName) + if (propValue !== null) { + var oldValue = getStyleKey(old, propName) + if (add) { + if (propValue !== oldValue) { + if (preferSetter) { + style[propName] = propValue + } else { + style.setProperty(propName, propValue) + } + } + } else { + if (oldValue === null) { + if (preferSetter) { + style[propName] = "" + } else { + style.removeProperty(propName) + } + } + } + } + } +} + +/* +Edit this with extreme caution, and profile any change you make. + +Not only is this itself a hot spot (it comprises about 3-5% of runtime overhead), but the way it's +compiled can even sometimes have knock-on performance impacts elsewhere. Per some Turbolizer +experiments, this will generate around 10-15 KiB of assembly in its final optimized form. + +Some of the optimizations it does: + +- For pairs of attributes, I pack them into two integers so I can compare them in + parallel. +- I reuse the same character loads for `xlink:*` and `on*` to check for other nodes. I do not reuse + the last load, as the first 2 characters is usually enough just on its own to know if a special + attribute name is matchable. +- For small attribute names (4 characters or less), the code handles them in full, with no full + string comparison. +- I fuse all the conditions, `hasOwn` and existence checks, and all the add/remove logic into just + this, to reduce startup overhead and keep outer loop code size down. +- I use a lot of labels to reuse as much code as possible, and thus more ICs, to make optimization + easier and better-informed. +- Bit flags are used extensively here to merge as many comparisons as possible. This function is + actually the real reason why I'm using bit flags for stuff like `` in the + first place - it moves the check to just the create flow where it's only done once. +*/ +var setAttr = (vnode, element, mask, key, old, attrs) => { + try { + var newValue = getPropKey(attrs, key) + var oldValue = getPropKey(old, key) + + if (mask & FLAG_IS_REMOVE && newValue !== null) return + + forceSetAttribute: { + forceTryProperty: { + skipValueDiff: { + if (key.length > 1) { + var pair1 = key.charCodeAt(0) | key.charCodeAt(1) << 16 + + if (key.length === 2 && pair1 === (ASCII_LOWER_I | ASCII_LOWER_S << 16)) { + return + } else if (pair1 === (ASCII_LOWER_O | ASCII_LOWER_N << 16)) { + if (newValue === oldValue) return + // Update the event + if (typeof newValue === "function") { + if (typeof oldValue !== "function") { + if (vnode.s == null) vnode.s = new EventDict() + element.addEventListener(key.slice(2), vnode.s) + } + // Save this, so the current redraw is correctly tracked. + vnode.s._ = currentRedraw + vnode.s.set(key, newValue) + } else if (typeof oldValue === "function") { + element.removeEventListener(key.slice(2), vnode.s) + vnode.s.delete(key) + } + return + } else if (key.length > 3) { + var pair2 = key.charCodeAt(2) | key.charCodeAt(3) << 16 + if ( + key.length > 6 && + pair1 === (ASCII_LOWER_X | ASCII_LOWER_L << 16) && + pair2 === (ASCII_LOWER_I | ASCII_LOWER_N << 16) && + (key.charCodeAt(4) | key.charCodeAt(5) << 16) === (ASCII_LOWER_K | ASCII_COLON << 16) + ) { + key = key.slice(6) + if (newValue !== null) { + element.setAttributeNS(xlinkNs, key, newValue) + } else { + element.removeAttributeNS(xlinkNs, key) + } + return + } else if (key.length === 4) { + if ( + pair1 === (ASCII_LOWER_T | ASCII_LOWER_Y << 16) && + pair2 === (ASCII_LOWER_P | ASCII_LOWER_E << 16) + ) { + if (!(mask & FLAG_INPUT_ELEMENT)) break skipValueDiff + if (newValue === null) break forceSetAttribute + break forceTryProperty + } else if ( + // Try to avoid a few browser bugs on normal elements. + pair1 === (ASCII_LOWER_H | ASCII_LOWER_R << 16) && pair2 === (ASCII_LOWER_E | ASCII_LOWER_F << 16) || + pair1 === (ASCII_LOWER_L | ASCII_LOWER_I << 16) && pair2 === (ASCII_LOWER_S | ASCII_LOWER_T << 16) || + pair1 === (ASCII_LOWER_F | ASCII_LOWER_O << 16) && pair2 === (ASCII_LOWER_R | ASCII_LOWER_M << 16) + ) { + // If it's a custom element, just keep it. Otherwise, force the attribute + // to be set. + if (!(mask & FLAG_CUSTOM_ELEMENT)) { + break forceSetAttribute + } + } + } else if (key.length > 4) { + switch (key) { + case "children": + return + + case "class": + case "className": + case "title": + if (newValue === null) break forceSetAttribute + break forceTryProperty + + case "value": + if ( + // Filter out non-HTML keys and custom elements + (mask & (FLAG_HTML_ELEMENT | FLAG_CUSTOM_ELEMENT)) !== FLAG_HTML_ELEMENT || + !(key in element) + ) { + break + } + + if (newValue === null) { + if (mask & (FLAG_OPTION_ELEMENT | FLAG_SELECT_ELEMENT)) { + break forceSetAttribute + } else { + break forceTryProperty + } + } + + if (!(mask & (FLAG_INPUT_ELEMENT | FLAG_TEXTAREA_ELEMENT | FLAG_SELECT_ELEMENT | FLAG_OPTION_ELEMENT))) { + break + } + + // It's always stringified, so it's okay to always coerce + if (element.value === (newValue = `${newValue}`)) { + // Setting `` to the same value causes an + // error to be generated if it's non-empty + if (mask & FLAG_IS_FILE_INPUT) return + // Setting `` to the same value by typing on focused + // element moves cursor to end in Chrome + if (mask & (FLAG_INPUT_ELEMENT | FLAG_TEXTAREA_ELEMENT)) { + if (element === currentDocument.activeElement) return + } else { + if (oldValue != null && oldValue !== false) return + } + } + + if (mask & FLAG_IS_FILE_INPUT) { + //setting input[type=file][value] to different value is an error if it's non-empty + // Not ideal, but it at least works around the most common source of uncaught exceptions for now. + if (newValue !== "") { + console.error("File input `value` attributes must either mirror the current value or be set to the empty string (to reset).") + return + } + } + + break forceTryProperty + + case "style": + if (oldValue === newValue) { + // Styles are equivalent, do nothing. + } else if (newValue === null) { + // New style is missing, just clear it. + element.style = "" + } else if (typeof newValue !== "object") { + // New style is a string, let engine deal with patching. + element.style = newValue + } else if (oldValue === null || typeof oldValue !== "object") { + // `old` is missing or a string, `style` is an object. + element.style = "" + // Add new style properties + setStyle(element.style, null, newValue, true) + } else { + // Both old & new are (different) objects, or `old` is missing. + // Update style properties that have changed, or add new style properties + setStyle(element.style, oldValue, newValue, true) + // Remove style properties that no longer exist + setStyle(element.style, newValue, oldValue, false) + } + return + + case "selected": + var active = currentDocument.activeElement + if ( + element === active || + mask & FLAG_OPTION_ELEMENT && element.parentNode === active + ) { + break + } + // falls through + + case "checked": + case "selectedIndex": + break skipValueDiff + + // Try to avoid a few browser bugs on normal elements. + case "width": + case "height": + // If it's a custom element, just keep it. Otherwise, force the attribute + // to be set. + if (!(mask & FLAG_CUSTOM_ELEMENT)) { + break forceSetAttribute + } + } + } + } + } + + if (newValue !== null && typeof newValue !== "object" && oldValue === newValue) return + } + + // Filter out namespaced keys + if (!(mask & FLAG_HTML_ELEMENT)) { + break forceSetAttribute + } + } + + // Filter out namespaced keys + // Defer the property check until *after* we check everything. + if (key in element) { + element[key] = newValue + return + } + } + + if (newValue === null) { + if (oldValue !== null) element.removeAttribute(key) + } else { + element.setAttribute(key, newValue === true ? "" : newValue) + } + } catch (e) { + handleAttributeError(old, e, false) + } +} + +// Here's an explanation of how this works: +// 1. The event names are always (by design) prefixed by `on`. +// 2. The EventListener interface accepts either a function or an object with a `handleEvent` method. +// 3. The object inherits from `Map`, to avoid hitting global setters. +// 4. The event name is remapped to the handler before calling it. +// 5. In function-based event handlers, `ev.currentTarget === this`. We replicate that below. +// 6. In function-based event handlers, `return false` prevents the default action and stops event +// propagation. Instead of that, we hijack the return value, so we can have it auto-redraw if +// the user returns `"skip-redraw"` or a promise that resolves to it. +class EventDict extends Map { + async handleEvent(ev) { + invokeRedrawable(this._, this.get(`on${ev.type}`), ev.currentTarget, ev) + } +} + +var currentlyRendering = [] + +m.render = (dom, vnode, {redraw, removeOnThrow} = {}) => { + if (!dom) { + throw new TypeError("DOM element being rendered to does not exist.") + } + + checkCallback(redraw, true, "redraw") + + for (var root of currentlyRendering) { + if (dom.contains(root)) { + throw new TypeError("Node is currently being rendered to and thus is locked.") + } + } + + var active = dom.ownerDocument.activeElement + var namespace = dom.namespaceURI + + var prevHooks = currentHooks + var prevRedraw = currentRedraw + var prevParent = currentParent + var prevRefNode = currentRefNode + var prevNamespace = currentNamespace + var prevDocument = currentDocument + var prevContext = currentContext + var prevRemoveOnThrow = currentRemoveOnThrow + var hooks = currentHooks = [] + + try { + currentlyRendering.push(currentParent = dom) + currentRedraw = typeof redraw === "function" ? redraw : null + currentRefNode = null + currentNamespace = namespace === htmlNs ? null : namespace + currentDocument = dom.ownerDocument + currentContext = {redraw} + // eslint-disable-next-line no-implicit-coercion + currentRemoveOnThrow = !!removeOnThrow + + // First time rendering into a node clears it out + if (dom.vnodes == null) dom.textContent = "" + updateNode(dom.vnodes, vnode = m.normalize(vnode)) + dom.vnodes = vnode + // `document.activeElement` can return null: https://html.spec.whatwg.org/multipage/interaction.html#dom-document-activeelement + if (active != null && currentDocument.activeElement !== active && typeof active.focus === "function") { + active.focus() + } + for (var {a, d} of hooks) { + try { + a(d) + } catch (e) { + console.error(e) + } + } + } finally { + currentRedraw = prevRedraw + currentHooks = prevHooks + currentParent = prevParent + currentRefNode = prevRefNode + currentNamespace = prevNamespace + currentDocument = prevDocument + currentContext = prevContext + currentRemoveOnThrow = prevRemoveOnThrow + currentlyRendering.pop() + } +} + +m.mount = (root, view) => { + if (!root) throw new TypeError("Root must be an element") + + if (typeof view !== "function") { + throw new TypeError("View must be a function") + } + + var window = root.ownerDocument.defaultView + var id = 0 + var unschedule = () => { + if (id) { + window.cancelAnimationFrame(id) + id = 0 + } + } + var redraw = () => { if (!id) id = window.requestAnimationFrame(redraw.sync) } + // Cheating with context access for a minor bundle size win. + var Mount = (_, old) => [m.remove(unschedule), view.call(currentContext, !old)] + redraw.sync = () => { + unschedule() + m.render(root, m(Mount), {redraw}) + } + + m.render(root, null) + redraw.sync() + + return redraw +} diff --git a/src/entry/mithril.esm.js b/src/entry/mithril.esm.js new file mode 100644 index 000000000..b51937f03 --- /dev/null +++ b/src/entry/mithril.esm.js @@ -0,0 +1,24 @@ +import m from "../core.js" + +import {debouncer, throttler} from "../std/rate-limit.js" +import {link, route} from "../std/router.js" +import {match, p, query} from "../std/path-query.js" +import {tracked, trackedList} from "../std/tracked.js" +import fetch from "../std/fetch.js" +import init from "../std/init.js" +import lazy from "../std/lazy.js" + +m.route = route +m.link = link +m.p = p +m.query = query +m.match = match +m.fetch = fetch +m.lazy = lazy +m.init = init +m.tracked = tracked +m.trackedList = trackedList +m.throttler = throttler +m.debouncer = debouncer + +export default m diff --git a/src/entry/mithril.umd.js b/src/entry/mithril.umd.js new file mode 100644 index 000000000..50c773908 --- /dev/null +++ b/src/entry/mithril.umd.js @@ -0,0 +1,5 @@ +/* global module: false, window: false */ +import m from "./mithril.esm.js" + +if (typeof module !== "undefined") module.exports = m +else window.m = m diff --git a/src/entry/stream.esm.js b/src/entry/stream.esm.js new file mode 100644 index 000000000..f8c89b267 --- /dev/null +++ b/src/entry/stream.esm.js @@ -0,0 +1,152 @@ +var STATE_PENDING = 1 +var STATE_ACTIVE = 2 +var STATE_CHANGING = 3 +var STATE_ENDED = 4 + +var streamSet = (stream, value) => { + if (value !== SKIP) { + stream._v = value + if (stream._s !== STATE_ENDED) { + streamChanging(stream) + stream._s = STATE_ACTIVE + // Cloning the list to ensure it's still iterated in intended + // order + var streams = stream._d.slice() + var fns = stream._f.slice() + for (var i = 0; i < streams.length; i++) { + if (streams[i]._s !== STATE_ENDED) { + streamSet(streams[i], fns[i](stream._v)) + } + } + } + } + + return stream._v +} + +var streamChanging = (stream) => { + if (stream._s !== STATE_ENDED) stream._s = STATE_CHANGING + for (var s of stream._d) streamChanging(s) +} + +var streamMap = (stream, fn, ignoreInitial) => { + var target = ignoreInitial ? Stream() : Stream(fn(stream._v)) + target._p.push(stream) + stream._d.push(target) + stream._f.push(fn) + return target +} + +var Stream = (...args) => { + var stream = (...args) => streamSet(stream, args.length ? args[0] : SKIP) + + Object.setPrototypeOf(stream, Stream.prototype) + + stream._s = args.length && args[0] !== SKIP ? STATE_ACTIVE : STATE_PENDING + stream._v = args.length ? args[0] : undefined + stream._d = [] + stream._f = [] + stream._p = [] + stream._e = null + + return stream +} + +Stream["fantasy-land/of"] = Stream + +Stream.prototype = Object.create(Function.prototype, Object.getOwnPropertyDescriptors({ + constructor: Stream, + map(fn) { return streamMap(this, fn, this._s !== STATE_ACTIVE) }, + "fantasy-land/ap"(x) { return combine(() => (0, x._v)(this._v), [x, this]) }, + toJSON() { + var value = this._v + return (value != null && typeof value.toJSON === "function" ? value.toJSON() : value) + }, + get end() { + if (!this._e) { + this._e = Stream() + streamMap(this._e, (value) => { + if (value === true) { + for (var p of this._p) { + var childIndex = p._d.indexOf(this) + if (childIndex >= 0) { + p._d.splice(childIndex, 1) + p._f.splice(childIndex, 1) + } + } + this._s = STATE_ENDED + this._p.length = this._d.length = this._f.length = 0 + } + return value + }, true) + } + return this._e + }, +})) + +Stream.prototype["fantasy-land/map"] = Stream.prototype.map + +var SKIP = Stream.SKIP = {} + +var combine = Stream.combine = (fn, streams) => { + if (streams.some((s) => s.constructor !== Stream)) { + throw new Error("Ensure that each item passed to stream.combine/stream.merge/lift is a stream.") + } + var ready = streams.every((s) => s._s === STATE_ACTIVE) + var stream = ready ? Stream(fn(streams)) : Stream() + + var changed = [] + + var mappers = streams.map((s) => streamMap(s, (value) => { + changed.push(s) + if (ready || streams.every((s) => s._s !== STATE_PENDING)) { + ready = true + streamSet(stream, fn(changed)) + changed = [] + } + return value + }, true)) + + var endStream = stream.end.map((value) => { + if (value === true) { + for (var mapper of mappers) mapper.end(true) + endStream.end(true) + } + return undefined + }) + + return stream +} + +Stream.merge = (streams) => combine(() => streams.map((s) => s._v), streams) + +Stream.scan = (fn, acc, origin) => { + var stream = streamMap(origin, (v) => { + var next = fn(acc, v) + if (next !== SKIP) acc = next + return next + }, origin._s !== STATE_ACTIVE) + streamSet(stream, acc) + return stream +} + +Stream.scanMerge = (tuples, seed) => { + var streams = tuples.map((tuple) => tuple[0]) + + var stream = combine((changed) => { + for (var i = 0; i < streams.length; i++) { + if (changed.includes(streams[i])) { + seed = tuples[i][1](seed, streams[i]._v) + } + } + return seed + }, streams) + + streamSet(stream, seed) + + return stream +} + +Stream.lift = (fn, ...streams) => combine(() => fn(...streams.map((s) => s._v)), streams) + +export {Stream as default} diff --git a/src/entry/stream.umd.js b/src/entry/stream.umd.js new file mode 100644 index 000000000..1b88b9de7 --- /dev/null +++ b/src/entry/stream.umd.js @@ -0,0 +1,6 @@ +/* global window: false, module: false */ +import Stream from "./stream.esm.js" + +if (typeof module !== "undefined") module["exports"] = Stream +else if (typeof window.m === "function" && !("stream" in window.m)) window.m.stream = Stream +else window.m = {stream : Stream} diff --git a/src/std/fetch.js b/src/std/fetch.js new file mode 100644 index 000000000..a9ac8f0f1 --- /dev/null +++ b/src/std/fetch.js @@ -0,0 +1,71 @@ +/* global fetch */ + +import {checkCallback} from "../util.js" + +var mfetch = async (url, opts = {}) => { + checkCallback(opts.onprogress, true, "opts.onprogress") + checkCallback(opts.extract, true, "opts.extract") + + try { + var response = await fetch(url, opts) + + if (opts.onprogress && response.body) { + var reader = response.body.getReader() + var rawLength = response.headers.get("content-length") || "" + // This is explicit coercion, but ESLint is frequently too dumb to detect it correctly. + // Another example: https://github.com/eslint/eslint/issues/14623 + // eslint-disable-next-line no-implicit-coercion + var total = (/^\d+$/).test(rawLength) ? +rawLength : -1 + var current = 0 + + response = new Response(new ReadableStream({ + type: "bytes", + start: (ctrl) => reader || ctrl.close(), + cancel: (reason) => reader.cancel(reason), + async pull(ctrl) { + var result = await reader.read() + if (result.done) { + ctrl.close() + } else { + current += result.value.length + ctrl.enqueue(result.value) + opts.onprogress(current, total) + } + }, + }), response) + } + + if (response.ok) { + if (opts.extract) { + return await opts.extract(response) + } + + switch (opts.responseType || "json") { + case "json": return await response.json() + case "formdata": return await response.formData() + case "arraybuffer": return await response.arrayBuffer() + case "blob": return await response.blob() + case "text": return await response.text() + case "document": + // eslint-disable-next-line no-undef + return new DOMParser() + .parseFromString(await response.text(), response.headers.get("content-type") || "text/html") + default: + throw new TypeError(`Unknown response type: ${opts.responseType}`) + } + } + + var message = (await response.text()) || response.statusText + } catch (e) { + var cause = e + var message = e.message + } + + var e = new Error(message) + e.status = response ? response.status : 0 + e.response = response + e.cause = cause + throw e +} + +export {mfetch as default} diff --git a/src/std/init.js b/src/std/init.js new file mode 100644 index 000000000..61d288960 --- /dev/null +++ b/src/std/init.js @@ -0,0 +1,14 @@ +import m from "../core.js" + +import {checkCallback, invokeRedrawable} from "../util.js" + +function Init({f}, old) { + if (old) return m.retain() + var ctrl = new AbortController() + queueMicrotask(() => invokeRedrawable(this.redraw, f, undefined, ctrl.signal)) + return m.remove(() => ctrl.abort()) +} + +var init = (f) => m(Init, {f: checkCallback(f)}) + +export {init as default} diff --git a/src/std/lazy.js b/src/std/lazy.js new file mode 100644 index 000000000..d1e93e552 --- /dev/null +++ b/src/std/lazy.js @@ -0,0 +1,40 @@ +import m from "../core.js" + +import {checkCallback, noop} from "../util.js" + +var lazy = (opts) => { + checkCallback(opts.fetch, false, "opts.fetch") + checkCallback(opts.pending, true, "opts.pending") + checkCallback(opts.error, true, "opts.error") + + // Capture the error here so stack traces make more sense + var error = new ReferenceError("Component not found") + var redraws = new Set() + var Comp = function () { + redraws.add(checkCallback(this.redraw, false, "context.redraw")) + return opts.pending && opts.pending() + } + var init = async () => { + init = noop + try { + Comp = await opts.fetch() + if (typeof Comp !== "function") { + Comp = Comp.default + if (typeof Comp !== "function") throw error + } + } catch (e) { + console.error(e) + Comp = () => opts.error && opts.error(e) + } + var r = redraws + redraws = null + for (var f of r) f() + } + + return (attrs) => { + init() + return m(Comp, attrs) + } +} + +export {lazy as default} diff --git a/src/std/path-query.js b/src/std/path-query.js new file mode 100644 index 000000000..4bb05806a --- /dev/null +++ b/src/std/path-query.js @@ -0,0 +1,308 @@ +/* +Caution: `m.p` and the failure path of `m.match` are both perf-sensitive. More so than you might +think. And unfortunately, string indexing is incredibly slow. + +Suppose we're in a large CRUD app with 20 resources and 10 pages for each resource, for a total of +200 routes. And further, suppose we're on a complicated management page (like a domain management +page) with a grid of 50 rows and 8 routed icon links each. Each link has its URL constructed via +`m.p(...)`, for a total of 400 calls. (This is high, but still realistic. At the time of writing, +Namesilo's UI for selecting domains and performing batch operations on them is designed as a table +with about that many icon links and up to 100 domains per page.) + +To meet 60 FPS, we generally have to have the whole page rendered in under 10ms for the browser to +not skip frames. To give the user some buffer for view inefficiency, let's aim for 2ms of overhead +for all the `m.match` and `m.p` calls. From some local benchmarking, the failure path of `m.match` +requires about 1us/op, so 200 routes would come out to about 0.2ms. (The success path is well under +0.1ms, so its overhead is negligible.) That leaves us about 1.8ms for 400 calls to `m.p(...)`. Do +the math, and that comes out to a whopping 4.5 us/call for us to meet our deadline. + +I've tried the following for `m.p`, and most of them ended up being too slow. Times are for calls +with two string interpolation parameters (the slow path), measured on an older laptop. The laptop +experiences a roughly 30-60% perf boost when charging over when running from battery. The lower end +is while charging, the higher end is while on battery. + +- A direct port of v2's `m.buildPathname`: 15-25 us + - This provides headroom for up to about 70 calls per frame. +- Replace its inner `template.replace` with a `re.exec(template)` loop: 12-18 microseconds + - This provides headroom for up to about 100 calls per frame. +- Switch from using match strings to computing positions from `exec.index`: 6.5-12 microseconds + - This provides headroom for up to about 150 calls per frame. +- Switch from using match strings to computing positions from `exec.index`: 6.5-12 microseconds + - This provides headroom for up to about 150 calls per frame. +- Iterate string directly: 2-3.5 microseconds + - This provides headroom for up to about 500 calls per frame. + +I've tried optimizing it further, but I'm running into the limits of string performance at this +point. And the computing positions from `exec.index` is about the fastest I could get any +regexp-based solution to go. + +Also, I tried at first restricting parameters to JS identifiers (like `m.match` parameters are, as +I use named groups to generate the properties), but that, just on the regexp side, cut performance +in more than half. The `exec.match` form, the ideal one for regexp-based solutions, slowed down +from 12 microseconds to about 35-40 microseconds. And that would reduce headroom down to only about +45-50 calls per frame. This rate is simply too slow to even be viable for some smaller apps. +*/ + +// Allowed terminators for `m.match`: +// - `.` for `:file.:ext` +// - `-` for `:lang-:locale` +// - `/` for `/:some/:path/` +// - end for `/:some/:path` +// Escape with `\\` +// Use `*rest` for rest + +import {hasOwn} from "../util.js" + +var toString = {}.toString + +var invalidMatchTemplate = /\/\/|[:*][^$_\p{IDS}]|[:*].[$\p{IDC}]*[:*]|\*.*?[^$\p{IDC}]|:([$_\p{IDS}][$\p{IDC}]*)[^$\p{IDC}].*?[:*]\1(?![$\p{IDC}])/u +// I escape literal text so people can use things like `:file.:ext` or `:lang-:locale` in routes. +// This is all merged into one pass so I don't also accidentally escape `-` and make it harder to +// detect it to ban it from template parameters. +var matcherCompile = /([:*])([$_\p{IDS}][$\p{IDC}]*)|\\\\|\\?([$^*+.()|[\]{}])|\\(.)/ug + +var serializeQueryValue = (pq, result, prefix, value) => { + var proto + + if (value != null && value !== false) { + if (Array.isArray(value)) { + for (var i of value) { + result = serializeQueryValue(pq, result, `${prefix}[]`, i) + } + } else if ( + typeof value === "object" && + ((proto = Object.getPrototypeOf(value)) == null || proto === Object.prototype || toString.call(value) === "[object Object]") + ) { + for (var k in value) { + if (hasOwn.call(value, k)) { + result = serializeQueryValue(pq, result, `${prefix}[${k}]`, value[k]) + } + } + } else { + var sep = pq.s + pq.s = "&" + result += sep + encodeURIComponent(prefix) + (value === true ? "" : `=${ + typeof value === "number" || typeof value === "bigint" + ? value + : encodeURIComponent(value) + }`) + } + } + + return result +} + +var serializeQueryParams = (sep, value, exclude, params) => { + var pq = {s: sep} + for (var key in params) { + if (hasOwn.call(params, key) && !exclude.includes(key)) { + value = serializeQueryValue(pq, value, key, params[key]) + } + } + return value +} + +var query = (params) => serializeQueryParams("", "", [], params) + +var QUERY = 0 +var ESCAPE = 1 +var CHAR = 2 +// Structure: +// Bit 0: is raw +// Bit 1: is next +// Bit 2: always set +var VAR_START = 4 +// var RAW_VAR_START = 5 +var VAR_NEXT = 6 +// var RAW_VAR_NEXT = 7 +var STATE_IS_RAW = 1 +var STATE_IS_NEXT = 2 + + +// Returns `path` from `template` + `params` +/** + * @param {string} template + * @param {undefined | null | Record} params + */ +var p = (template, params) => { + // This carefully only iterates the template once. + var prev = 0 + var start = 0 + var state = CHAR + // An array is fine. It's almost never large enough for the overhead of hashing to pay off. + var inTemplate = [] + // Used for later. + var hash = "" + var queryIndex = -1 + var hashIndex = -1 + var result = "" + var sep = "?" + + var NOT_VAR_NEXT = VAR_NEXT - 1 + + // Using `for ... of` so the engine can do bounds check elimination more easily. + for (var i = 0;; i++) { + var ch = template.charAt(i) + + if ( + state > NOT_VAR_NEXT && + (ch === "" || ch === "#" || ch === "?" || ch === "\\" || ch === "/" || ch === "." || ch === "-") + ) { + var segment = template.slice(start + 1, i) + + // If no such parameter exists, don't interpolate it. + if (params != null && params[segment] != null) { + inTemplate.push(segment) + segment = `${params[segment]}` + + // Escape normal parameters, but not variadic ones. + // eslint-disable-next-line no-bitwise + if (state & STATE_IS_RAW) { + var newHashIndex = segment.indexOf("#") + var newQueryIndex = (newHashIndex < 0 ? segment : segment.slice(0, newHashIndex)).indexOf("?") + if (newQueryIndex >= 0) { + sep = "&" + queryIndex = result.length + (prev - start) + newQueryIndex + } + if (newHashIndex >= 0) { + hashIndex = result.length + (prev - start) + newHashIndex + } + } else { + segment = encodeURIComponent(segment) + } + + // Drop the preceding `:`/`*`/`\` character from the appended segment + if (prev !== start) { + result += template.slice(prev, start) + } + + result += segment + + // Start from the next end + prev = i + } + } + + if (ch === "#") { + if (hashIndex < 0) hashIndex = i + } else if (ch !== "") { + if (state === QUERY) { + // do nothing + } else if (ch === "?") { + // The query start cannot be escaped. It's a proper URL delimiter. + if (queryIndex < 0) { + queryIndex = i + sep = "&" + } else { + // Inject an `&` in place of a `?`. Note that `sep === "&"` + if (prev !== i) result += template.slice(prev, i) + result += "&" + prev = i + 1 + } + state = QUERY + } else if (state === ESCAPE) { + // Drop the preceding `\` character from the appended segment + if (prev !== start) { + result += template.slice(prev, start) + } + + state = CHAR + start = prev = i + } else if (ch === "\\") { + start = i + state = ESCAPE + } else if (ch === ":" || ch === "*") { + if (state > CHAR) { + throw new SyntaxError("Template parameter names must be separated by either a '/', '-', or '.'.") + } + // eslint-disable-next-line no-bitwise + state = VAR_START | (ch === "*") + start = i + } else if (ch === "/" || ch === "." || ch === "-") { + state = CHAR + } else if (state > CHAR) { + // eslint-disable-next-line no-bitwise + state |= STATE_IS_NEXT + } + + continue + } + + if (prev === 0 && params == null) { + return template + } + + if (prev < template.length) { + result += template.slice(prev) + } + + if (hashIndex >= 0) { + hash = result.slice(hashIndex) + result = result.slice(0, hashIndex) + } + + return serializeQueryParams(sep, result, inTemplate, params) + hash + } +} + +/** @typedef {RegExp & {r: number, p: URLSearchParams}} Matcher */ + +/** @type {Map} */ +var cache = new Map() + +/** @param {string} pattern @returns {Matcher} */ +var compile = (pattern) => { + if (invalidMatchTemplate.test(pattern)) { + throw new SyntaxError("Invalid pattern") + } + + var queryIndex = pattern.indexOf("?") + var hashIndex = pattern.indexOf("#") + var index = queryIndex < hashIndex ? queryIndex : hashIndex + var rest + var re = new RegExp(`^${pattern.slice(0, index < 0 ? undefined : index).replace( + matcherCompile, + (_, p, name, esc1, esc2) => { + if (p === "*") { + rest = name + return `(?<${name}>.*)` + } else if (p === ":") { + return `(?<${name}>[^/]+)` + } else { + return esc2 || `\\${esc1 || "\\"}` + } + } + )}$`, "u") + cache.set(pattern, re) + re.r = rest + re.p = new URLSearchParams(index < 0 ? "" : pattern.slice(index, hashIndex < 0 ? undefined : hashIndex)) + return re +} + +/** @param {{path: string, params: URLSearchParams}} route */ +var match = ({path, params}, pattern) => { + var re = cache.get(pattern) + if (!re) { + re = /*@__NOINLINE__*/compile(pattern) + } + + var exec = re.exec(path) + var restIndex = re.r + if (!exec) return + + for (var [k, v] of re.p) { + if (params.get(k) !== v) return + } + + // Taking advantage of guaranteed insertion order and group iteration order here to reduce the + // condition to a simple numeric comparison. + for (var k in exec.groups) { + if (restIndex--) { + exec.groups[k] = decodeURIComponent(exec.groups[k]) + } + } + + return {...exec.groups} +} + +export {p, query, match} diff --git a/src/std/rate-limit.js b/src/std/rate-limit.js new file mode 100644 index 000000000..89bf832bf --- /dev/null +++ b/src/std/rate-limit.js @@ -0,0 +1,188 @@ +/* global performance, setTimeout, clearTimeout */ + +import {noop} from "../util.js" + +var validateDelay = (delay) => { + if (!Number.isFinite(delay) || delay <= 0) { + throw new RangeError("Timer delay must be finite and positive") + } +} + +var rateLimiterImpl = (delay = 500, isThrottler) => { + validateDelay(delay) + + var closed = false + var start = 0 + var timer = 0 + var resolveNext = noop + + var callback = () => { + timer = undefined + resolveNext(false) + resolveNext = noop + } + + var rateLimiter = async (ignoreLeading) => { + if (closed) { + return true + } + + resolveNext(true) + resolveNext = noop + + if (timer) { + if (isThrottler) { + return new Promise((resolve) => resolveNext = resolve) + } + + clearTimeout(timer) + ignoreLeading = true + } + + start = performance.now() + timer = setTimeout(callback, delay) + + if (!ignoreLeading) { + return + } + + return new Promise((resolve) => resolveNext = resolve) + } + + rateLimiter.update = (newDelay) => { + validateDelay(newDelay) + delay = newDelay + + if (closed) return + if (timer) { + clearTimeout(timer) + timer = setTimeout(callback, (start - performance.now()) + delay) + } + } + + rateLimiter.dispose = () => { + if (closed) return + closed = true + clearTimeout(timer) + resolveNext(true) + resolveNext = noop + } + + return rateLimiter +} + +/** + * A general-purpose bi-edge throttler, with a dynamically configurable limit. It's much better + * than your typical `throttle(f, ms)` because it lets you easily separate the trigger and reaction + * using a single shared, encapsulated state object. That same separation is also used to make the + * rate limit dynamically reconfigurable on hit. + * + * Create as `throttled = m.throttler(ms)` and do `if (await throttled()) return` to rate-limit + * the code that follows. The result is one of three values, to allow you to identify edges: + * + * - Leading edge: `undefined` + * - Trailing edge: `false`, returned only if a second call was made + * - No edge: `true` + * + * Call `throttled.update(ms)` to update the interval. This not only impacts future delays, but also any current one. + * + * To dispose, like on component removal, call `throttled.dispose()`. + * + * If you don't sepecify a delay, it defaults to 500ms on creation, which works well enough for + * most needs. There is no default for `throttled.update(...)` - you must specify one explicitly. + * + * Example usage: + * + * ```js + * const throttled = m.throttler() + * let results, error + * return function () { + * return [ + * m.remove(throttled.dispose), + * m("input[type=search]", { + * oninput: async (ev) => { + * // Skip redraw if rate limited - it's pointless + * if (await throttled()) return false + * error = results = null + * this.redraw() + * try { + * const response = await fetch(m.p("/search", {q: ev.target.value})) + * if (response.ok) { + * results = await response.json() + * } else { + * error = await response.text() + * } + * } catch (e) { + * error = e.message + * } + * }, + * }), + * results.map((result) => m(SearchResult, {result})), + * !error || m(ErrorDisplay, {error})), + * ] + * } + * ``` + * + * Important note: due to the way this is implemented in basically all runtimes, the throttler's + * clock might not tick during sleep, so if you do `await throttled()` and immediately sleep in a + * low-power state for 5 minutes, you might have to wait another 10 minutes after resuming to a + * high-power state. + */ +var throttler = (delay) => rateLimiterImpl(delay, 1) + +/** + * A general-purpose bi-edge debouncer, with a dynamically configurable limit. It's much better + * than your typical `debounce(f, ms)` because it lets you easily separate the trigger and reaction + * using a single shared, encapsulated state object. That same separation is also used to make the + * rate limit dynamically reconfigurable on hit. + * + * Create as `debounced = m.debouncer(ms)` and do `if (await debounced()) return` to rate-limit + * the code that follows. The result is one of three values, to allow you to identify edges: + * + * - Leading edge: `undefined` + * - Trailing edge: `false`, returned only if a second call was made + * - No edge: `true` + * + * Call `debounced.update(ms)` to update the interval. This not only impacts future delays, but also any current one. + * + * To dispose, like on component removal, call `debounced.dispose()`. + * + * If you don't sepecify a delay, it defaults to 500ms on creation, which works well enough for + * most needs. There is no default for `debounced.update(...)` - you must specify one explicitly. + * + * Example usage: + * + * ```js + * const debounced = m.debouncer() + * let results, error + * return (attrs) => [ + * m.remove(debounced.dispose), + * m("input[type=text].value", { + * async oninput(ev) { + * // Skip redraw if rate limited - it's pointless + * if ((await debounced()) !== false) return false + * try { + * const response = await fetch(m.p("/save/:id", {id: attrs.id}), { + * body: JSON.stringify({value: ev.target.value}), + * }) + * if (!response.ok) { + * error = await response.text() + * } + * } catch (e) { + * error = e.message + * } + * }, + * }), + * results.map((result) => m(SearchResult, {result})), + * !error || m(ErrorDisplay, {error})), + * ] + * ``` + * + * Important note: due to the way this is implemented in basically all runtimes, the debouncer's + * clock might not tick during sleep, so if you do `await debounced()` and immediately sleep in a + * low-power state for 5 minutes, you might have to wait another 10 minutes after resuming to a + * high-power state. + */ +var debouncer = (delay) => rateLimiterImpl(delay, 0) + +export {throttler, debouncer} diff --git a/src/std/router.js b/src/std/router.js new file mode 100644 index 000000000..39015aaac --- /dev/null +++ b/src/std/router.js @@ -0,0 +1,133 @@ +/* global window: false */ +import m from "../core.js" + +import {checkCallback} from "../util.js" + +var Route = function ({p: prefix}) { + var href = this.href + var mustReplace, redraw, currentParsedHref + var currentRoute + + var updateRouteWithHref = () => { + var url = new URL(href) + var urlPath = url.pathname + url.search + url.hash + var decodedPrefix = prefix + var index = urlPath.indexOf(decodedPrefix) + if (index < 0) index = urlPath.indexOf(decodedPrefix = encodeURI(decodedPrefix)) + if (index >= 0) urlPath = urlPath.slice(index + decodedPrefix.length) + if (urlPath[0] !== "/") urlPath = `/${urlPath}` + + var parsedUrl = new URL(urlPath, href) + var path = decodeURI(parsedUrl.pathname) + mustReplace = false + currentRoute = { + prefix, + path, + params: parsedUrl.searchParams, + current: path + parsedUrl.search + parsedUrl.hash, + set, + match, + } + return currentParsedHref = parsedUrl.href + } + + var updateRoute = () => { + if (href === window.location.href) return + href = window.location.href + if (currentParsedHref !== updateRouteWithHref()) redraw() + } + + var set = (path, {replace, state} = {}) => { + if (mustReplace) replace = true + mustReplace = true + queueMicrotask(updateRoute) + redraw() + if (typeof window === "object") { + window.history[replace ? "replaceState" : "pushState"](state, "", prefix + path) + } + } + + var match = (path) => m.match(currentRoute, path) + + if (!href) { + if (typeof window !== "object") { + throw new TypeError("Outside the DOM, `href` must be set") + } + href = window.location.href + window.addEventListener("popstate", updateRoute) + } else if (typeof href !== "string") { + throw new TypeError("The initial route href must be a string if given") + } + + updateRouteWithHref() + + return function ({v: view}) { + redraw = checkCallback(this.redraw, false, "context.redraw") + + return [ + m.remove(() => window.removeEventListener("popstate", updateRoute)), + m.set({route: currentRoute}, m.inline(view)), + ] + } +} + +export var route = (prefix, view) => { + if (typeof prefix !== "string") { + throw new TypeError("The route prefix must be a string") + } + + return m(Route, {v: checkCallback(view, false, "view"), p: prefix}) +} + +// Let's provide a *right* way to manage a route link, rather than letting people screw up +// accessibility on accident. +// +// Note: this does *not* support disabling. Instead, consider more accessible alternatives like not +// showing the link in the first place. If you absolutely have to disable the link, disable it by +// removing this component (like via `m("div", {disabled}, !disabled && m(Link))`). There's +// friction here for a reason. +var Link = () => { + var href, opts, setRoute + var listener = (ev) => { + // Adapted from React Router's implementation: + // https://github.com/ReactTraining/react-router/blob/520a0acd48ae1b066eb0b07d6d4d1790a1d02482/packages/react-router-dom/modules/Link.js + // + // Try to be flexible and intuitive in how we handle links. + // Fun fact: links aren't as obvious to get right as you + // would expect. There's a lot more valid ways to click a + // link than this, and one might want to not simply click a + // link, but right click or command-click it to copy the + // link target, etc. Nope, this isn't just for blind people. + if ( + // Skip if `onclick` prevented default + !ev.defaultPrevented && + // Ignore everything but left clicks + (ev.button === 0 || ev.which === 0 || ev.which === 1) && + // Let the browser handle `target=_blank`, etc. + (!ev.currentTarget.target || ev.currentTarget.target === "_self") && + // No modifier keys + !ev.ctrlKey && !ev.metaKey && !ev.shiftKey && !ev.altKey + ) { + setRoute(href, opts) + // Capture the event, and don't double-call `redraw`. + return m.capture(ev) + } + } + + return function (attrs, old) { + setRoute = this.route.set + href = attrs.h + opts = attrs.o + return [ + m.layout((dom) => { + dom.href = this.route.prefix + href + if (!old) dom.addEventListener("click", listener) + }), + m.remove((dom) => { + dom.removeEventListener("click", listener) + }), + ] + } +} + +export var link = (href, opts) => m(Link, {h: `${href}`, o: opts}) diff --git a/src/std/tracked.js b/src/std/tracked.js new file mode 100644 index 000000000..2b4e7e2ef --- /dev/null +++ b/src/std/tracked.js @@ -0,0 +1,187 @@ +/* +Here's the intent. +- Usage in model: + - List + - Get + - Track + - Delete + - Replace (equivalent to delete + track) +- Usage in view: + - Iterate live handles + - Release aborted live handles that no longer needed + +Models can do basic CRUD operations on the collection. +- They can list what's currently there. +- They can get a current value. +- They can set the current value. +- They can delete the current value. +- They can replace the current value, deleting a value that's already there. + +In the view, they use handles to abstract over the concept of a key. Duplicates are theoretically +possible, so they should use the handle itself as the key for `m.keyed(...)`. It might look +something like this: + +```js +return m.keyed(t.live(), (handle) => ( + [handle.key, m(Entry, { + name: handle.key, + value: handle.value, + removed: handle.signal.aborted, + onremovaltransitionended: () => handle.release(), + })] +)) +``` + +There used to be an in-renderer way to manage this transparently, but there's a couple big reasons +why that was removed in favor of this: + +1. It's very complicated to get right. Like, the majority of the removal code was related to it. In + fact, this module is considerably smaller than the code that'd have to go into the renderer to + support it, as this isn't nearly as perf-sensitive as that. +2. When you need to remove something asynchronously, there's multiple ways you may want to manage + transitions. You might want to stagger them. You might want to do them all at once. You might + want to clear some state and not other state. You might want to preserve some elements of a + sibling's state. Embedding it in the renderer would force an opinion on you, and in order to + work around it, you'd have to do something like this anyways. + +As for the difference between `m.trackedList()` and `m.tracked()`, the first is for tracking lists +(and is explained above), and `m.tracked()` is for single values (but uses `m.trackedList()` +internally to avoid a ton of code duplication). +*/ + +import m from "../core.js" + +import {checkCallback, noop} from "../util.js" + +/** + * @template K, V + * @typedef TrackedHandle + * + * @property {K} key + * @property {V} value + * @property {AbortSignal} signal + * @property {() => void} release + * @property {() => void} remove + */ + +/** + * @template K, V + * @typedef Tracked + * + * @property {() => Array>} live + * @property {() => Array<[K, V]>} list + * @property {(key: K) => boolean} has + * @property {(key: K) => undefined | V} get + * @property {(key: K, value: V) => void} set + * @property {(key: K, value: V) => void} replace + * @property {(key: K) => boolean} delete + */ + +var trackedState = (redraw) => { + checkCallback(redraw, false, "redraw") + /** @type {Map>} */ + var state = new Map() + var removed = new WeakSet() + /** @type {Set>} */ var live = new Set() + + /** @param {null | AbortController & TrackedHandle} prev */ + var abort = (prev) => { + try { + if (prev) { + if (removed.has(prev)) { + live.delete(prev) + } else { + prev.abort() + } + } + } catch (e) { + console.error(e) + } + } + + /** @param {K} k */ + var remove = (k, r) => { + var prev = state.get(k) + var result = state.delete(k) + abort(prev) + if (r) redraw() + return result + } + + /** + * @param {K} k + * @param {V} v + * @param {number} bits + * Bit 1 forcibly releases the old handle, and bit 2 causes an update notification to be sent + * (something that's unwanted during initialization). + */ + var setHandle = (k, v, bits) => { + var prev = state.get(k) + // Note: it extending `AbortController` is an implementation detail. It exposing a `signal` + // property is *not*. + var handle = /** @type {AbortController & TrackedHandle} */ (new AbortController()) + handle.key = k + handle.value = v + handle.release = (ev) => { + if (ev) m.capture(ev) + if (!handle) return + if (state.get(handle.key) === handle) { + removed.add(handle) + handle = null + } else if (live.delete(handle)) { + redraw() + } + } + handle.remove = (ev) => { + if (ev) m.capture(ev) + remove(handle.key, 0) + } + state.set(k, handle) + live.add(handle) + // eslint-disable-next-line no-bitwise + if (bits & 1) live.delete(prev) + abort(prev) + // eslint-disable-next-line no-bitwise + if (bits & 2) redraw() + } + + return {s: state, l: live, h: setHandle, r: remove} +} + +/** + * @template K, V + * @param {Iterable<[K, V]>} [initial] + * @param {() => void} redraw + * @returns {TrackedList} + */ +var trackedList = (redraw, initial) => { + var {s: state, l: live, h: setHandle, r: remove} = trackedState(redraw) + + for (var [k, v] of initial || []) setHandle(k, v, 1) + + return { + live: () => [...live], + list: () => Array.from(state.values(), (h) => [h.key, h.value]), + has: (k) => state.has(k), + get: (k) => (k = state.get(k)) && k.value, + set: (k, v) => setHandle(k, v, 3), + replace: (k, v) => setHandle(k, v, 2), + delete: (k) => remove(k, 1), + forget: (k) => (k = state.get(k)) && k.release(), + } +} + +var tracked = (redraw) => { + var {l: live, h: setHandle, r: remove} = trackedState(redraw) + var initial = noop + var id = -1 + return (state) => { + if (!Object.is(initial, initial = state)) { + remove(id++, 0) + setHandle(id, state, 1) + } + return [...live] + } +} + +export {tracked, trackedList} diff --git a/src/util.js b/src/util.js new file mode 100644 index 000000000..53dea3e9f --- /dev/null +++ b/src/util.js @@ -0,0 +1,20 @@ +export var hasOwn = {}.hasOwnProperty + +export var invokeRedrawable = async (redraw, fn, thisValue, ...args) => { + if (typeof fn === "function") { + thisValue = Reflect.apply(fn, thisValue, args) + if (thisValue === "skip-redraw") return + if (thisValue && typeof thisValue.then === "function" && (await thisValue) === "skip-redraw") return + redraw() + } +} + +export var checkCallback = (callback, allowNull, label = "callback") => { + if (allowNull && callback == null || typeof callback === "function") { + return callback + } + + throw new TypeError(`\`${label}\` must be a function${allowNull ? " if provided." : "."}`) +} + +export var noop = () => {} diff --git a/stream.js b/stream.js deleted file mode 100644 index 89e5baab0..000000000 --- a/stream.js +++ /dev/null @@ -1,3 +0,0 @@ -"use strict" - -module.exports = require("./stream/stream") diff --git a/stream/stream.js b/stream/stream.js deleted file mode 100644 index 98f41d96a..000000000 --- a/stream/stream.js +++ /dev/null @@ -1,183 +0,0 @@ -/* eslint-disable */ -;(function() { -"use strict" -/* eslint-enable */ -Stream.SKIP = {} -Stream.lift = lift -Stream.scan = scan -Stream.merge = merge -Stream.combine = combine -Stream.scanMerge = scanMerge -Stream["fantasy-land/of"] = Stream - -var warnedHalt = false -Object.defineProperty(Stream, "HALT", { - get: function() { - warnedHalt || console.log("HALT is deprecated and has been renamed to SKIP"); - warnedHalt = true - return Stream.SKIP - } -}) - -function Stream(value) { - var dependentStreams = [] - var dependentFns = [] - - function stream(v) { - if (arguments.length && v !== Stream.SKIP) { - value = v - if (open(stream)) { - stream._changing() - stream._state = "active" - // Cloning the list to ensure it's still iterated in intended - // order - dependentStreams.slice().forEach(function(s, i) { - if (open(s)) s(this[i](value)) - }, dependentFns.slice()) - } - } - - return value - } - - stream.constructor = Stream - stream._state = arguments.length && value !== Stream.SKIP ? "active" : "pending" - stream._parents = [] - - stream._changing = function() { - if (open(stream)) stream._state = "changing" - dependentStreams.forEach(function(s) { - s._changing() - }) - } - - stream._map = function(fn, ignoreInitial) { - var target = ignoreInitial ? Stream() : Stream(fn(value)) - target._parents.push(stream) - dependentStreams.push(target) - dependentFns.push(fn) - return target - } - - stream.map = function(fn) { - return stream._map(fn, stream._state !== "active") - } - - var end - function createEnd() { - end = Stream() - end.map(function(value) { - if (value === true) { - stream._parents.forEach(function (p) {p._unregisterChild(stream)}) - stream._state = "ended" - stream._parents.length = dependentStreams.length = dependentFns.length = 0 - } - return value - }) - return end - } - - stream.toJSON = function() { return value != null && typeof value.toJSON === "function" ? value.toJSON() : value } - - stream["fantasy-land/map"] = stream.map - stream["fantasy-land/ap"] = function(x) { return combine(function(s1, s2) { return s1()(s2()) }, [x, stream]) } - - stream._unregisterChild = function(child) { - var childIndex = dependentStreams.indexOf(child) - if (childIndex !== -1) { - dependentStreams.splice(childIndex, 1) - dependentFns.splice(childIndex, 1) - } - } - - Object.defineProperty(stream, "end", { - get: function() { return end || createEnd() } - }) - - return stream -} - -function combine(fn, streams) { - var ready = streams.every(function(s) { - if (s.constructor !== Stream) - throw new Error("Ensure that each item passed to stream.combine/stream.merge/lift is a stream.") - return s._state === "active" - }) - var stream = ready - ? Stream(fn.apply(null, streams.concat([streams]))) - : Stream() - - var changed = [] - - var mappers = streams.map(function(s) { - return s._map(function(value) { - changed.push(s) - if (ready || streams.every(function(s) { return s._state !== "pending" })) { - ready = true - stream(fn.apply(null, streams.concat([changed]))) - changed = [] - } - return value - }, true) - }) - - var endStream = stream.end.map(function(value) { - if (value === true) { - mappers.forEach(function(mapper) { mapper.end(true) }) - endStream.end(true) - } - return undefined - }) - - return stream -} - -function merge(streams) { - return combine(function() { return streams.map(function(s) { return s() }) }, streams) -} - -function scan(fn, acc, origin) { - var stream = origin.map(function(v) { - var next = fn(acc, v) - if (next !== Stream.SKIP) acc = next - return next - }) - stream(acc) - return stream -} - -function scanMerge(tuples, seed) { - var streams = tuples.map(function(tuple) { return tuple[0] }) - - var stream = combine(function() { - var changed = arguments[arguments.length - 1] - streams.forEach(function(stream, i) { - if (changed.indexOf(stream) > -1) - seed = tuples[i][1](seed, stream()) - }) - - return seed - }, streams) - - stream(seed) - - return stream -} - -function lift() { - var fn = arguments[0] - var streams = Array.prototype.slice.call(arguments, 1) - return merge(streams).map(function(streams) { - return fn.apply(undefined, streams) - }) -} - -function open(s) { - return s._state === "pending" || s._state === "active" || s._state === "changing" -} - -if (typeof module !== "undefined") module["exports"] = Stream -else if (typeof window.m === "function" && !("stream" in window.m)) window.m.stream = Stream -else window.m = {stream : Stream} - -}()); diff --git a/stream/stream.min.js b/stream/stream.min.js deleted file mode 100644 index 4260f4d37..000000000 --- a/stream/stream.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(){"use strict";t.SKIP={},t.lift=function(){var n=arguments[0],t=Array.prototype.slice.call(arguments,1);return r(t).map((function(t){return n.apply(void 0,t)}))},t.scan=function(n,e,r){var a=r.map((function(r){var a=n(e,r);return a!==t.SKIP&&(e=a),a}));return a(e),a},t.merge=r,t.combine=e,t.scanMerge=function(n,t){var r=n.map((function(n){return n[0]})),a=e((function(){var e=arguments[arguments.length-1];return r.forEach((function(r,a){e.indexOf(r)>-1&&(t=n[a][1](t,r()))})),t}),r);return a(t),a},t["fantasy-land/of"]=t;var n=!1;function t(n){var r,i=[],u=[];function c(e){return arguments.length&&e!==t.SKIP&&(n=e,a(c)&&(c._changing(),c._state="active",i.slice().forEach((function(t,e){a(t)&&t(this[e](n))}),u.slice()))),n}function o(){return(r=t()).map((function(n){return!0===n&&(c._parents.forEach((function(n){n._unregisterChild(c)})),c._state="ended",c._parents.length=i.length=u.length=0),n})),r}return c.constructor=t,c._state=arguments.length&&n!==t.SKIP?"active":"pending",c._parents=[],c._changing=function(){a(c)&&(c._state="changing"),i.forEach((function(n){n._changing()}))},c._map=function(e,r){var a=r?t():t(e(n));return a._parents.push(c),i.push(a),u.push(e),a},c.map=function(n){return c._map(n,"active"!==c._state)},c.toJSON=function(){return null!=n&&"function"==typeof n.toJSON?n.toJSON():n},c["fantasy-land/map"]=c.map,c["fantasy-land/ap"]=function(n){return e((function(n,t){return n()(t())}),[n,c])},c._unregisterChild=function(n){var t=i.indexOf(n);-1!==t&&(i.splice(t,1),u.splice(t,1))},Object.defineProperty(c,"end",{get:function(){return r||o()}}),c}function e(n,e){var r=e.every((function(n){if(n.constructor!==t)throw new Error("Ensure that each item passed to stream.combine/stream.merge/lift is a stream.");return"active"===n._state})),a=r?t(n.apply(null,e.concat([e]))):t(),i=[],u=e.map((function(t){return t._map((function(u){return i.push(t),(r||e.every((function(n){return"pending"!==n._state})))&&(r=!0,a(n.apply(null,e.concat([i]))),i=[]),u}),!0)})),c=a.end.map((function(n){!0===n&&(u.forEach((function(n){n.end(!0)})),c.end(!0))}));return a}function r(n){return e((function(){return n.map((function(n){return n()}))}),n)}function a(n){return"pending"===n._state||"active"===n._state||"changing"===n._state}Object.defineProperty(t,"HALT",{get:function(){return n||console.log("HALT is deprecated and has been renamed to SKIP"),n=!0,t.SKIP}}),"undefined"!=typeof module?module.exports=t:"function"!=typeof window.m||"stream"in window.m?window.m={stream:t}:window.m.stream=t}(); \ No newline at end of file diff --git a/test-utils/browserMock.js b/test-utils/browserMock.js index ead6e9e45..7cf3a39b8 100644 --- a/test-utils/browserMock.js +++ b/test-utils/browserMock.js @@ -1,18 +1,11 @@ -"use strict" +import domMock from "./domMock.js" +import pushStateMock from "./pushStateMock.js" -var pushStateMock = require("./pushStateMock") -var domMock = require("./domMock") -var xhrMock = require("./xhrMock") +export default function browserMock(env = {}) { + var $window = {} -module.exports = function(env) { - env = env || {} - var $window = env.window = {} - - var dom = domMock() - var xhr = xhrMock() - for (var key in dom) if (!$window[key]) $window[key] = dom[key] - for (var key in xhr) if (!$window[key]) $window[key] = xhr[key] - pushStateMock(env) + domMock($window, env) + pushStateMock($window, env) return $window -} \ No newline at end of file +} diff --git a/test-utils/callAsync.js b/test-utils/callAsync.js index 426964c99..368d79435 100644 --- a/test-utils/callAsync.js +++ b/test-utils/callAsync.js @@ -1,3 +1,30 @@ -"use strict" +/* global setTimeout, clearTimeout, setImmediate, clearImmediate */ +const callAsyncRaw = typeof setImmediate === "function" ? setImmediate : setTimeout +const cancelAsyncRaw = typeof clearImmediate === "function" ? clearImmediate : clearTimeout -module.exports = typeof setImmediate === "function" ? setImmediate : setTimeout +const timers = new Set() + +export function callAsync(f) { + const id = callAsyncRaw(() => { + timers.delete(id) + return f() + }) + timers.add(id) +} + +export function waitAsync() { + return new Promise((resolve) => { + const id = callAsyncRaw(() => { + resolve() + timers.delete(id) + }) + timers.add(id) + }) +} + +export function clearPending() { + for (const timer of timers) { + cancelAsyncRaw(timer) + } + timers.clear() +} diff --git a/test-utils/components.js b/test-utils/components.js deleted file mode 100644 index 5ba37044a..000000000 --- a/test-utils/components.js +++ /dev/null @@ -1,31 +0,0 @@ -"use strict" - -var m = require("../render/hyperscript") - -module.exports = [ - { - kind: "POJO", - create: function(methods) { - var res = {view: function() {return m("div")}} - Object.keys(methods || {}).forEach(function(m){res[m] = methods[m]}) - return res - } - }, { - kind: "constructible", - create: function(methods) { - function res(){} - res.prototype.view = function() {return m("div")} - Object.keys(methods || {}).forEach(function(m){res.prototype[m] = methods[m]}) - return res - } - }, { - kind: "closure", - create: function(methods) { - return function() { - var res = {view: function() {return m("div")}} - Object.keys(methods || {}).forEach(function(m){res[m] = methods[m]}) - return res - } - } - } -] diff --git a/test-utils/domMock.js b/test-utils/domMock.js index 5536b49fd..f94bf8f26 100644 --- a/test-utils/domMock.js +++ b/test-utils/domMock.js @@ -1,8 +1,7 @@ -"use strict" +/* global setTimeout, clearTimeout */ /* 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 - ... @@ -14,10 +13,10 @@ options: - spy:(f: Function) => Function */ -module.exports = function(options) { +export default function domMock($window, options) { options = options || {} - var spy = options.spy || function(f){return f} - var spymap = [] + var spy = options.spy || ((f) => f) + var spymap = new Map() // This way I'm not also implementing a partial `URL` polyfill. Based on the // regexp at https://urlregex.com/, but adapted to allow relative URLs and @@ -34,22 +33,24 @@ module.exports = function(options) { "^" + urlHash + "$" ) - var hasOwn = ({}.hasOwnProperty) + var hasOwn = {}.hasOwnProperty + var registerSpies = () => {} + var getSpies - function registerSpies(element, spies) { - if(options.spy) { - var i = spymap.indexOf(element) - if (i === -1) { - spymap.push(element, spies) + if (options.spy) { + registerSpies = (element, spies) => { + var prev = spymap.get(element) + if (prev) { + Object.assign(prev, spies) } else { - var existing = spymap[i + 1] - for (var k in spies) existing[k] = spies[k] + spymap.set(element, spies) } } - } - function getSpies(element) { - if (element == null || typeof element !== "object") throw new Error("Element expected") - if(options.spy) return spymap[spymap.indexOf(element) + 1] + + getSpies = (element) => { + if (element == null || typeof element !== "object") throw new Error("Element expected") + return spymap.get(element) + } } function isModernEvent(type) { @@ -62,13 +63,14 @@ module.exports = function(options) { 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 + const record = this._events.get(e.type) + if (record != null) { + for (var i = 0; i < record.handlers.length; i++) { + var useCapture = record.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})} + var handler = record.handlers[i] + if (typeof handler === "function") try {handler.call(this, e)} catch(e) {console.error(e)} + else try {handler.handleEvent(e)} catch(e) {console.error(e)} if (stopped) return } } @@ -77,101 +79,27 @@ module.exports = function(options) { // 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 - if (ancestor === child) throw new Error("Node cannot be inserted at the specified point in the hierarchy") - - if (child.nodeType == null) throw new Error("Argument is not a DOM element") - - var index = this.childNodes.indexOf(child) - if (index > -1) this.childNodes.splice(index, 1) - if (child.nodeType === 11) { - while (child.firstChild != null) appendChild.call(this, child.firstChild) - child.childNodes = [] - } - else { - this.childNodes.push(child) - if (child.parentNode != null && child.parentNode !== this) removeChild.call(child.parentNode, child) - child.parentNode = this - } - } - function removeChild(child) { - if (child == null || typeof child !== "object" || !("nodeType" in child)) { - throw new TypeError("Failed to execute removeChild, parameter is not of type 'Node'") - } - var index = this.childNodes.indexOf(child) - if (index > -1) { - this.childNodes.splice(index, 1) - child.parentNode = null - } - else throw new TypeError("Failed to execute 'removeChild', child not found in parent") - } - function insertBefore(child, reference) { - var ancestor = this - while (ancestor !== child && ancestor !== null) ancestor = ancestor.parentNode - if (ancestor === child) throw new Error("Node cannot be inserted at the specified point in the hierarchy") - - if (child.nodeType == null) throw new Error("Argument is not a DOM element") - - var refIndex = this.childNodes.indexOf(reference) - 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) 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) { - var subchild = child.firstChild - removeChild.call(child, subchild) - subchild.parentNode = this - } - child.childNodes = [] - } - else { - this.childNodes.splice(refIndex, 0, child) - if (child.parentNode != null && child.parentNode !== this) removeChild.call(child.parentNode, child) - child.parentNode = this + if (e.eventPhase > 1 && typeof this["on" + e.type] === "function" && !isModernEvent(e.type)) { + try { + this["on" + e.type](e) + } catch (e) { + console.error(e) } } } - function getAttribute(name) { - if (this.attributes[name] == null) return null - return this.attributes[name].value - } - function setAttribute(name, value) { - /*eslint-disable no-implicit-coercion*/ - // 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: hasOwn.call(this.attributes, name) ? this.attributes[name].namespaceURI : null, - get value() {return nodeValue}, - set value(value) { - /*eslint-disable no-implicit-coercion*/ - nodeValue = "" + value - /*eslint-enable no-implicit-coercion*/ - }, - get nodeValue() {return nodeValue}, - set nodeValue(value) { - this.value = value - } + + class Attr { + constructor(namespaceURI, value) { + this.namespaceURI = namespaceURI + // this is the correct kind of conversion, passing a Symbol throws in browsers too. + this._value = `${value}` } + get value() { return this._value } + set value(value) { this._value = `${value}` } + get nodeValue() { return this._value } + set nodeValue(value) { this._value = `${value}` } } - function setAttributeNS(ns, name, value) { - this.setAttribute(name, value) - this.attributes[name].namespaceURI = ns - } - function removeAttribute(name) { - delete this.attributes[name] - } - function hasAttribute(name) { - return name in this.attributes - } + var declListTokenizer = /;|"(?:\\.|[^"\n])*"|'(?:\\.|[^'\n])*'/g /** * This will split a semicolon-separated CSS declaration list into an array of @@ -204,487 +132,732 @@ 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) - }) - appendChild.call(stack[depth], element) - if (!selfClosed && voidElements.indexOf(startTag.toLowerCase()) < 0) stack[++depth] = element - } - else if (endTag) { - depth-- - } - else if (text) { - appendChild.call(stack[depth], $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>$/) - 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} - } function camelCase(string) { - return string.replace(/-\D/g, function(match) {return match[1].toUpperCase()}) + return string.replace(/-[a-z]/g, (match) => match[1].toUpperCase()) } - var activeElement - var delay = 16, last = 0 - var $window = { - DOMParser: DOMParser, - requestAnimationFrame: function(callback) { - var elapsed = Date.now() - last - return setTimeout(function() { - callback() - last = Date.now() - }, delay - elapsed) - }, - document: { - createElement: function(tag) { - var cssText = "" - var style = {} - Object.defineProperties(style, { - cssText: { - get: function() {return cssText}, - set: function (value) { - var buf = [] - if (typeof value === "string") { - for (var key in style) style[key] = "" - var rules = splitDeclList(value) - for (var i = 0; i < rules.length; i++) { - var rule = rules[i] - var colonIndex = rule.indexOf(":") - if (colonIndex > -1) { - var rawKey = rule.slice(0, colonIndex).trim() - var key = camelCase(rawKey) - var value = rule.slice(colonIndex + 1).trim() - if (key !== "cssText") { - style[key] = style[rawKey] = value - buf.push(rawKey + ": " + value + ";") - } - } - } - element.setAttribute("style", cssText = buf.join(" ")) - } - } - }, - getPropertyValue: {value: function(key){ - return style[key] - }}, - removeProperty: {value: function(key){ - style[key] = style[camelCase(key)] = "" - }}, - setProperty: {value: function(key, value){ - style[key] = style[camelCase(key)] = value - }} - }) - var events = {} - var element = { - nodeType: 1, - nodeName: tag.toUpperCase(), - namespaceURI: "http://www.w3.org/1999/xhtml", - appendChild: appendChild, - removeChild: removeChild, - insertBefore: insertBefore, - hasAttribute: hasAttribute, - getAttribute: getAttribute, - setAttribute: setAttribute, - setAttributeNS: setAttributeNS, - removeAttribute: removeAttribute, - parentNode: null, - childNodes: [], - attributes: {}, - ownerDocument: $window.document, - contains: function(child) { - while (child != null) { - if (child === this) return true - child = child.parentNode - } - return false - }, - get firstChild() { - return this.childNodes[0] || null - }, - get nextSibling() { - if (this.parentNode == null) return null - var index = this.parentNode.childNodes.indexOf(this) - if (index < 0) throw new TypeError("Parent's childNodes is out of sync") - return this.parentNode.childNodes[index + 1] || null - }, - // eslint-disable-next-line accessor-pairs - set textContent(value) { - this.childNodes = [] - if (value !== "") appendChild.call(this, $window.document.createTextNode(value)) - }, - // eslint-disable-next-line accessor-pairs - set innerHTML(value) { - var voidElements = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"] - while (this.firstChild) removeChild.call(this, this.firstChild) - var match = value.match(/^(.*)<\/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 - }, - set style(value){ - this.style.cssText = value - }, - get className() { - return this.attributes["class"] ? this.attributes["class"].value : "" - }, - set className(value) { - if (this.namespaceURI === "http://www.w3.org/2000/svg") throw new Error("Cannot set property className of SVGElement") - else this.setAttribute("class", value) - }, - focus: function() {activeElement = this}, - 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, 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) { - 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) { - var parents = [] - if (this.parentNode != null) { - var parent = this.parentNode - do { - parents.push(parent) - parent = parent.parentNode - } while (parent != null) - } - e.target = this - 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 - } - 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 - } - } - } 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 - } - } + + class CSSStyleDeclarationHandler { + constructor(element) { + this.element = element + this.style = new Map() + this.raws = new Set() + this.cssText = undefined + } + + preventExtensions() { + return false + } + + _setCSSText(value) { + var buf = [] + if (typeof value === "string") { + for (var key of this.style.keys()) this.style.set(key, "") + const rules = splitDeclList(value) + for (let i = 0; i < rules.length; i++) { + const rule = rules[i] + const colonIndex = rule.indexOf(":") + if (colonIndex > -1) { + const rawKey = rule.slice(0, colonIndex).trim() + const key = camelCase(rawKey) + const value = rule.slice(colonIndex + 1).trim() + if (key !== "cssText") { + this.style.set(rawKey, value) + this.style.set(key, value) + this.raws.add(rawKey) + buf.push(rawKey + ": " + value + ";") } + } + } + this.element.setAttribute("style", this.cssText = buf.join(" ")) + } + } + + _getCSSText() { + if (this.cssText != null) return this.cssText + const result = [] + for (const key of this.raws) { + result.push(`${key}: ${this.style.get(key)};`) + } + return this.cssText = result.join(" ") + } + + get(target, key) { + if (typeof key !== "string") return Reflect.get(target, key) + if (Reflect.has(target, key)) return Reflect.get(target, key) + const value = this.style.get(key) + if (value !== undefined) return value + switch (key) { + case "cssText": return this._getCSSText() + case "cssFloat": return this.style.get("float") + default: return "" + } + } + + set(target, key, value) { + if (typeof key !== "string") return Reflect.set(target, key) + if (Reflect.has(target, key)) return Reflect.set(target, key) + if (value == null) value = "" + switch (key) { + case "cssText": this._setCSSText(value); return true + case "cssFloat": key = "float"; break + } + if (value === "") { + this.style.delete(key) + this.style.delete(camelCase(key)) + this.raws.add(key) + } else { + this.style.set(key, value) + this.style.set(camelCase(key), value) + this.raws.add(key) + } + this.cssText = undefined + return true + } + } + + class CSSStyleDeclaration { + constructor(element) { + return new Proxy(this, new CSSStyleDeclarationHandler(element)) + } + + getPropertyValue(key) { + return this[key] + } + + removeProperty(key) { + this[key] = this[camelCase(key)] = "" + } + + setProperty(key, value) { + this[key] = this[camelCase(key)] = value + } + } + + class ChildNode { + constructor(nodeType, nodeName) { + this.nodeType = nodeType + this.nodeName = nodeName + this.parentNode = null + } - }, - onclick: null, - _events: events + remove() { + if (this == null || typeof this !== "object" || !("nodeType" in this)) { + throw new TypeError("Failed to execute 'remove', this is not of type 'ChildNode'") + } + var parent = this.parentNode + if (parent == null) return + var index = parent.childNodes.indexOf(this) + if (index < 0) { + throw new TypeError("BUG: child linked to parent, parent doesn't contain child") + } + parent.childNodes.splice(index, 1) + this.parentNode = null + } + + after(child) { + if (this == null || typeof this !== "object" || !("nodeType" in this)) { + throw new TypeError("Failed to execute 'remove', this is not of type 'ChildNode'") + } + if (child == null || typeof child !== "object" || !("nodeType" in child)) { + throw new TypeError("Failed to execute 'remove', parameter is not of type 'ChildNode'") + } + var parent = this.parentNode + if (parent == null) return + var index = parent.childNodes.indexOf(this) + if (index < 0) { + throw new TypeError("BUG: child linked to parent, parent doesn't contain child") + } + child.remove() + parent.childNodes.splice(index + 1, 0, child) + child.parentNode = parent + } + + get nextSibling() { + if (this.parentNode == null) return null + var index = this.parentNode.childNodes.indexOf(this) + if (index < 0) throw new TypeError("Parent's childNodes is out of sync") + return this.parentNode.childNodes[index + 1] || null + } + } + + class Text extends ChildNode { + constructor(value) { + super(3, "#text") + this._value = `${value}` + } + + get childNodes() { + return [] + } + + get firstChild() { + return null + } + + get nodeValue() { + return this._value + } + + set nodeValue(value) { + this._value = `${value}` + } + } + + class Element extends ChildNode { + constructor(nodeName, ns) { + if (ns == null) ns = "http://www.w3.org/1999/xhtml" + super(1, nodeName) + this._style = new CSSStyleDeclaration(this) + this.namespaceURI = ns + this.parentNode = null + this.childNodes = [] + this.attributes = {} + this.ownerDocument = $window.document + this.onclick = null + this._events = new Map() + } + + appendChild(child) { + var ancestor = this + while (ancestor !== child && ancestor !== null) ancestor = ancestor.parentNode + if (ancestor === child) throw new Error("Node cannot be inserted at the specified point in the hierarchy") + + if (child.nodeType == null) throw new Error("Argument is not a DOM element") + + var index = this.childNodes.indexOf(child) + if (index > -1) this.childNodes.splice(index, 1) + this.childNodes.push(child) + if (child.parentNode != null && child.parentNode !== this) child.parentNode.removeChild(child) + child.parentNode = this + } + + removeChild(child) { + if (child == null || typeof child !== "object" || !("nodeType" in child)) { + throw new TypeError("Failed to execute removeChild, parameter is not of type 'Node'") + } + var index = this.childNodes.indexOf(child) + if (index > -1) { + this.childNodes.splice(index, 1) + child.parentNode = null + } + else throw new TypeError("Failed to execute 'removeChild', child not found in parent") + } + + insertBefore(child, refNode) { + var ancestor = this + while (ancestor !== child && ancestor !== null) ancestor = ancestor.parentNode + if (ancestor === child) throw new Error("Node cannot be inserted at the specified point in the hierarchy") + + if (child.nodeType == null) throw new Error("Argument is not a DOM element") + + var refIndex = this.childNodes.indexOf(refNode) + var index = this.childNodes.indexOf(child) + if (refNode !== null && refIndex < 0) throw new TypeError("Invalid argument") + if (index > -1) { + this.childNodes.splice(index, 1) + child.parentNode = null + } + if (refNode === null) this.appendChild(child) + else { + if (index !== -1 && refIndex > index) refIndex-- + this.childNodes.splice(refIndex, 0, child) + if (child.parentNode != null && child.parentNode !== this) child.parentNode.removeChild(child) + child.parentNode = this + } + } + + prepend(child) { + this.insertBefore(child, this.firstChild) + } + + hasAttribute(name) { + return name in this.attributes + } + + getAttribute(name) { + if (this.attributes[name] == null) return null + return this.attributes[name].value + } + + setAttribute(name, value) { + value = `${value}` + if (hasOwn.call(this.attributes, name)) { + this.attributes[name].value = value + } else { + this.attributes[name] = new Attr(null, value) + } + } + + setAttributeNS(ns, name, value) { + if (hasOwn.call(this.attributes, name)) { + this.attributes[name].namespaceURI = ns + this.attributes[name].value = value + } else { + this.attributes[name] = new Attr(ns, value) + } + } + + removeAttribute(name) { + delete this.attributes[name] + } + + removeAttributeNS(ns, name) { + // Namespace is ignored for now + delete this.attributes[name] + } + + contains(child) { + while (child != null) { + if (child === this) return true + child = child.parentNode + } + return false + } + + get firstChild() { + return this.childNodes[0] || null + } + + get nextSibling() { + if (this.parentNode == null) return null + var index = this.parentNode.childNodes.indexOf(this) + if (index < 0) throw new TypeError("Parent's childNodes is out of sync") + return this.parentNode.childNodes[index + 1] || null + } + + // eslint-disable-next-line accessor-pairs + set textContent(value) { + this.childNodes = [] + if (value !== "") this.appendChild($window.document.createTextNode(value)) + } + + get style() { + return this._style + } + + set style(value) { + this.style.cssText = value + } + + get className() { + if (this.namespaceURI === "http://www.w3.org/2000/svg") throw new Error("Cannot get property className of SVGElement") + else return this.getAttribute("class") + } + + set className(value) { + if (this.namespaceURI === "http://www.w3.org/2000/svg") throw new Error("Cannot set property className of SVGElement") + else this.setAttribute("class", value) + } + + focus() { + activeElement = this + } + + blur() { + if (activeElement === this) activeElement = null + } + + addEventListener(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} + } + const record = this._events.get(type) + if (record == null) { + this._events.set(type, {handlers: [handler], options: [options]}) + } else { + for (var i = 0; i < record.handlers.length; i++) { + if (record.handlers[i] === handler && record.options[i].capture === options.capture) { + return + } } + record.handlers.push(handler) + record.options.push(options) + } + } - if (element.nodeName === "A") { - Object.defineProperty(element, "href", { - 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 if (this.namespaceURI === "http://www.w3.org/1999/xhtml") { - if (!this.hasAttribute("href")) return "" - // HACK: if it's valid already, there's nothing to implement. - var value = this.attributes.href.value - if (validURLRegex.test(encodeURI(value))) return value - } - return "[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, - }) + removeEventListener(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} + } + const record = this._events.get(type) + if (record != null) { + for (var i = 0; i < record.handlers.length; i++) { + if (record.handlers[i] === handler && record.options[i].capture === options.capture) { + record.handlers.splice(i, 1) + record.options.splice(i, 1) + break + } } + } + } - if (element.nodeName === "INPUT") { - var checked - Object.defineProperty(element, "checked", { - get: function() {return checked === undefined ? this.attributes["checked"] !== undefined : checked}, - set: function(value) {checked = Boolean(value)}, - enumerable: true, - }) - - var value = "" - var valueSetter = spy(function(v) { - /*eslint-disable no-implicit-coercion*/ - value = v === null ? "" : "" + v - /*eslint-enable no-implicit-coercion*/ - }) - Object.defineProperty(element, "value", { - get: function() { - return value - }, - set: valueSetter, - enumerable: true, - }) - Object.defineProperty(element, "valueAsDate", { - get: function() { - if (this.getAttribute("type") !== "date") return null - return new Date(value).getTime() - }, - set: function(v) { - if (this.getAttribute("type") !== "date") throw new Error("invalid state") - var time = new Date(v).getTime() - valueSetter(isNaN(time) ? "" : new Date(time).toUTCString()) - }, - enumerable: true, - }) - Object.defineProperty(element, "valueAsNumber", { - get: function() { - switch (this.getAttribute("type")) { - case "date": return new Date(value).getTime() - case "number": return new Date(value).getTime() - default: return NaN - } - }, - set: function(v) { - v = Number(v) - if (!isNaN(v) && !isFinite(v)) throw new TypeError("infinite value") - switch (this.getAttribute("type")) { - case "date": valueSetter(isNaN(v) ? "" : new Date(v).toUTCString()); break; - case "number": valueSetter(String(value)); break; - default: throw new Error("invalid state") - } - }, - enumerable: true, - }) - - // we currently emulate the non-ie behavior, but emulating ie may be more useful (throw when an invalid type is set) - var typeSetter = spy(function(v) { - this.setAttribute("type", v) - }) - Object.defineProperty(element, "type", { - get: function() { - if (!this.hasAttribute("type")) return "text" - var type = this.getAttribute("type") - return (/^(?:radio|button|checkbox|color|date|datetime|datetime-local|email|file|hidden|month|number|password|range|research|search|submit|tel|text|url|week|image)$/) - .test(type) - ? type - : "text" - }, - set: typeSetter, - enumerable: true, - }) - registerSpies(element, { - valueSetter: valueSetter, - typeSetter: typeSetter - }) + dispatchEvent(e) { + var parents = [] + if (this.parentNode != null) { + var parent = this.parentNode + do { + parents.push(parent) + parent = parent.parentNode + } while (parent != null) + } + e.target = this + 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, "cancelBubble", { + configurable: true, + get: function () { return stopped } + }) + 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 + } + } + } 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 + } } + } + } + } + class HTMLAnchorElement extends Element { + constructor() { + super("A", null) + } - if (element.nodeName === "TEXTAREA") { - var wasNeverSet = true - var value = "" - var valueSetter = spy(function(v) { - wasNeverSet = false - /*eslint-disable no-implicit-coercion*/ - value = v === null ? "" : "" + v - /*eslint-enable no-implicit-coercion*/ - }) - Object.defineProperty(element, "value", { - get: function() { - return wasNeverSet && this.firstChild ? this.firstChild.nodeValue : value - }, - set: valueSetter, - enumerable: true, - }) - registerSpies(element, { - valueSetter: valueSetter - }) - } + get href() { + if (this.namespaceURI === "http://www.w3.org/2000/svg") { + var val = this.hasAttribute("href") ? this.attributes.href.value : "" + return {baseVal: val, animVal: val} + } else if (this.namespaceURI === "http://www.w3.org/1999/xhtml") { + if (!this.hasAttribute("href")) return "" + // HACK: if it's valid already, there's nothing to implement. + var value = this.attributes.href.value + if (validURLRegex.test(encodeURI(value))) return value + } + return "[FIXME implement]" + } - /* eslint-disable radix */ - - if (element.nodeName === "CANVAS") { - Object.defineProperty(element, "width", { - get: function() {return this.attributes["width"] ? Math.floor(parseInt(this.attributes["width"].value) || 0) : 300}, - set: function(value) {this.setAttribute("width", Math.floor(Number(value) || 0).toString())}, - }) - Object.defineProperty(element, "height", { - get: function() {return this.attributes["height"] ? Math.floor(parseInt(this.attributes["height"].value) || 0) : 300}, - set: function(value) {this.setAttribute("height", Math.floor(Number(value) || 0).toString())}, - }) - } + set href(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) + } + } + + class HTMLInputElement extends Element { + constructor() { + super("INPUT", null) + this._checked = undefined + this._value = "" - /* eslint-enable radix */ + registerSpies(this, { + valueSetter: this._valueSetter = spy(this._setValue), + }) + } + + _setValue(v) { + this._value = v === null ? "" : `${v}` + } + + get checked() { + return this._checked === undefined ? this.hasAttribute("checked") : this._checked + } + + set checked(value) { + this._checked = Boolean(value) + } + + get value() { + return this._value + } + + set value(value) { + this._valueSetter(value) + } + + get valueAsDate() { + if (this.getAttribute("type") !== "date") return null + return new Date(this._value).getTime() + } + + set valueAsDate(v) { + if (this.getAttribute("type") !== "date") throw new Error("invalid state") + var time = new Date(v).getTime() + this._valueSetter(isNaN(time) ? "" : new Date(time).toUTCString()) + } + + get valueAsNumber() { + switch (this.getAttribute("type")) { + case "date": return new Date(this._value).getTime() + case "number": return new Date(this._value).getTime() + default: return NaN + } + } + + set valueAsNumber(v) { + v = Number(v) + if (!isNaN(v) && !isFinite(v)) throw new TypeError("infinite value") + switch (this.getAttribute("type")) { + case "date": this._valueSetter(isNaN(v) ? "" : new Date(v).toUTCString()); break; + case "number": this._valueSetter(`${v}`); break; + default: throw new Error("invalid state") + } + } + + get type() { + var type = this.getAttribute("type") + if (type != null && (/^(?:radio|button|checkbox|color|date|datetime|datetime-local|email|file|hidden|month|number|password|range|research|search|submit|tel|text|url|week|image)$/).test(type)) { + return type + } else { + return "text" + } + } + + set type(value) { + this.setAttribute("type", value) + } + } + + class HTMLTextAreaElement extends Element { + constructor() { + super("TEXTAREA", null) + this._value = undefined - function getOptions(element) { - var options = [] - for (var i = 0; i < element.childNodes.length; i++) { - if (element.childNodes[i].nodeName === "OPTION") options.push(element.childNodes[i]) - else if (element.childNodes[i].nodeName === "OPTGROUP") options = options.concat(getOptions(element.childNodes[i])) + registerSpies(this, { + valueSetter: this._valueSetter = spy(this._setValue), + }) + } + + _setValue(v) { + this._value = v === null ? "" : `${v}` + } + + get value() { + if (this._value === undefined && this.firstChild) { + return this.firstChild.nodeValue + } else { + return this._value + } + } + + set value(value) { + this._valueSetter(value) + } + } + + class HTMLCanvasElement extends Element { + constructor() { + super("CANVAS", null) + } + + get width() { + const value = this.getAttribute("width") + // eslint-disable-next-line radix + return value != null ? Math.floor(parseInt(value) || 0) : 300 + } + + set width(value) { + this.setAttribute("width", Math.floor(Number(value) || 0).toString()) + } + + get height() { + const value = this.getAttribute("height") + // eslint-disable-next-line radix + return value != null ? Math.floor(parseInt(value) || 0) : 300 + } + + set height(value) { + this.setAttribute("height", Math.floor(Number(value) || 0).toString()) + } + } + + function pushOptions(options, element) { + for (const child of element.childNodes) { + if (child.nodeName === "OPTION") { + options.push(child) + } else if (child.nodeName === "OPTGROUP") { + pushOptions(options, child) + } + } + } + + function getOptions(element) { + const options = [] + pushOptions(options, element) + return options + } + + function getOptionValue(element) { + const value = element.getAttribute("value") + if (value != null) return value + const child = element.firstChild + if (child != null) return child.nodeValue + return "" + } + + class HTMLSelectElement extends Element { + constructor() { + super("SELECT", null) + // this._selectedValue = undefined + this._selectedIndex = 0 + + registerSpies(this, { + valueSetter: this._valueSetter = spy(this._setValue) + }) + } + + _setValue(value) { + if (value === null) { + this._selectedIndex = -1 + } else { + var options = getOptions(this) + var stringValue = `${value}` + for (var i = 0; i < options.length; i++) { + if (getOptionValue(options[i]) === stringValue) { + // this._selectedValue = stringValue + this._selectedIndex = i + return } - return options - } - function getOptionValue(element) { - return element.attributes["value"] != null ? - element.attributes["value"].value : - element.firstChild != null ? element.firstChild.nodeValue : "" } - if (element.nodeName === "SELECT") { - // var selectedValue - var selectedIndex = 0 - Object.defineProperty(element, "selectedIndex", { - get: function() {return getOptions(this).length > 0 ? selectedIndex : -1}, - set: function(value) { - var options = getOptions(this) - if (value >= 0 && value < options.length) { - // selectedValue = getOptionValue(options[selectedIndex]) - selectedIndex = value - } - else { - // selectedValue = "" - selectedIndex = -1 - } - }, - enumerable: true, - }) - var valueSetter = spy(function(value) { - if (value === null) { - selectedIndex = -1 - } else { - var options = getOptions(this) - /*eslint-disable no-implicit-coercion*/ - var stringValue = "" + value - /*eslint-enable no-implicit-coercion*/ - for (var i = 0; i < options.length; i++) { - if (getOptionValue(options[i]) === stringValue) { - // selectedValue = stringValue - selectedIndex = i - return - } - } - // selectedValue = stringValue - selectedIndex = -1 - } - }) - Object.defineProperty(element, "value", { - get: function() { - if (this.selectedIndex > -1) return getOptionValue(getOptions(this)[this.selectedIndex]) - return "" - }, - set: valueSetter, - enumerable: true, - }) - registerSpies(element, { - valueSetter: valueSetter - }) + // this._selectedValue = stringValue + this._selectedIndex = -1 + } + } + + get selectedIndex() { + if (getOptions(this).length) { + return this._selectedIndex + } else { + return -1 + } + } + + set selectedIndex(value) { + var options = getOptions(this) + if (value >= 0 && value < options.length) { + // this._selectedValue = getOptionValue(options[selectedIndex]) + this._selectedIndex = value + } else { + // this._selectedValue = "" + this._selectedIndex = -1 + } + } + + get value() { + if (this.selectedIndex > -1) { + return getOptionValue(getOptions(this)[this.selectedIndex]) + } + return "" + } + + set value(value) { + this._valueSetter(value) + } + } + + class HTMLOptionElement extends Element { + constructor() { + super("OPTION", null) + registerSpies(this, { + valueSetter: this._valueSetter = spy(this._setValue) + }) + } + + _setValue(value) { + this.setAttribute("value", value) + } + + get value() { + return getOptionValue(this) + } + + set value(value) { + this._valueSetter(value) + } + + // TODO? handle `selected` without a parent (works in browsers) + get selected() { + var index = getOptions(this.parentNode).indexOf(this) + return index === this.parentNode.selectedIndex + } + + set selected(value) { + if (value) { + var index = getOptions(this.parentNode).indexOf(this) + if (index > -1) this.parentNode.selectedIndex = index + } else { + this.parentNode.selectedIndex = 0 + } + } + } + + var activeElement = null + var delay = 16, last = 0 + Object.assign($window, { + window: $window, + requestAnimationFrame(callback) { + var elapsed = performance.now() - last + return setTimeout(() => { + last = performance.now() + try { + callback() + } catch (e) { + console.error(e) } - if (element.nodeName === "OPTION") { - var valueSetter = spy(function(value) { - /*eslint-disable no-implicit-coercion*/ - this.setAttribute("value", "" + value) - /*eslint-enable no-implicit-coercion*/ - }) - Object.defineProperty(element, "value", { - get: function() {return getOptionValue(this)}, - set: valueSetter, - enumerable: true, - }) - registerSpies(element, { - valueSetter: valueSetter - }) - - Object.defineProperty(element, "selected", { - // TODO? handle `selected` without a parent (works in browsers) - get: function() { - var options = getOptions(this.parentNode) - var index = options.indexOf(this) - return index === this.parentNode.selectedIndex - }, - set: function(value) { - if (value) { - var options = getOptions(this.parentNode) - var index = options.indexOf(this) - if (index > -1) this.parentNode.selectedIndex = index - } - else this.parentNode.selectedIndex = 0 - }, - enumerable: true, - }) + }, delay - elapsed) + }, + cancelAnimationFrame: clearTimeout, + document: { + defaultView: $window, + createElement: function(tag) { + if (!tag) throw new Error("Tag must be provided") + tag = `${tag}`.toUpperCase() + + switch (tag) { + case "A": return new HTMLAnchorElement() + case "INPUT": return new HTMLInputElement() + case "TEXTAREA": return new HTMLTextAreaElement() + case "CANVAS": return new HTMLCanvasElement() + case "SELECT": return new HTMLSelectElement() + case "OPTION": return new HTMLOptionElement() + default: return new Element(tag, null) } - return element }, createElementNS: function(ns, tag, is) { var element = this.createElement(tag, is) @@ -693,43 +866,7 @@ module.exports = function(options) { return element }, createTextNode: function(text) { - /*eslint-disable no-implicit-coercion*/ - var nodeValue = "" + text - /*eslint-enable no-implicit-coercion*/ - return { - nodeType: 3, - nodeName: "#text", - parentNode: null, - get childNodes() { return [] }, - get firstChild() { return null }, - get nodeValue() {return nodeValue}, - set nodeValue(value) { - /*eslint-disable no-implicit-coercion*/ - nodeValue = "" + value - /*eslint-enable no-implicit-coercion*/ - }, - get nextSibling() { - if (this.parentNode == null) return null - var index = this.parentNode.childNodes.indexOf(this) - if (index < 0) throw new TypeError("Parent's childNodes is out of sync") - return this.parentNode.childNodes[index + 1] || null - }, - } - }, - createDocumentFragment: function() { - return { - ownerDocument: $window.document, - nodeType: 11, - nodeName: "#document-fragment", - appendChild: appendChild, - insertBefore: insertBefore, - removeChild: removeChild, - parentNode: null, - childNodes: [], - get firstChild() { - return this.childNodes[0] || null - }, - } + return new Text(text) }, createEvent: function() { return { @@ -737,17 +874,15 @@ module.exports = function(options) { initEvent: function(type) {this.type = type} } }, - get activeElement() {return activeElement}, + get activeElement() { + return activeElement + }, }, - } - $window.document.defaultView = $window - $window.document.documentElement = $window.document.createElement("html") - appendChild.call($window.document.documentElement, $window.document.createElement("head")) - $window.document.body = $window.document.createElement("body") - appendChild.call($window.document.documentElement, $window.document.body) - activeElement = $window.document.body + }) - if (options.spy) $window.__getSpies = getSpies + $window.document.documentElement = new Element("HTML", null) + $window.document.documentElement.appendChild($window.document.head = new Element("HEAD", null)) + $window.document.documentElement.appendChild($window.document.body = new Element("BODY", null)) - return $window + if (options.spy) $window.__getSpies = getSpies } diff --git a/test-utils/global.js b/test-utils/global.js new file mode 100644 index 000000000..4dbedafe1 --- /dev/null +++ b/test-utils/global.js @@ -0,0 +1,111 @@ +// Load order is important for the imports. + +/* global globalThis, window, global */ + +import o from "ospec" + +import m from "../src/entry/mithril.esm.js" + +import browserMock from "./browserMock.js" +import {clearPending} from "./callAsync.js" +import throttleMocker from "./throttleMock.js" + +const G = ( + typeof globalThis !== "undefined" + ? globalThis + : typeof window !== "undefined" ? window : global +) + +const originalWindow = G.window +const originalDocument = G.document +const originalConsoleError = console.error + +export function restoreDOMGlobals() { + G.window = originalWindow + G.document = originalDocument +} + +export function setupGlobals(env = {}) { + let registeredRoots + /** @type {ReturnType} */ let $window + /** @type {ReturnType} */ let rafMock + + function register(root) { + registeredRoots.add(root) + return root + } + + function initialize(env) { + $window = browserMock(env) + rafMock = throttleMocker() + registeredRoots = new Set([$window.document.body]) + + G.window = $window.window + G.document = $window.document + $window.requestAnimationFrame = rafMock.schedule + $window.cancelAnimationFrame = rafMock.clear + + if (env && env.expectNoConsoleError) { + console.error = (...args) => { + if (typeof process === "function") process.exitCode = 1 + var replacement = console.error + // Node's `console.trace` delegates to `console.error` as a property. Have it + // actually call what it intended to call. + try { + console.error = originalConsoleError + console.trace("Unexpected `console.error` call") + } finally { + console.error = replacement + } + originalConsoleError.apply(console, args) + } + } + } + + o.beforeEach(() => { + initialize({...env}) + return env.initialize && env.initialize(G) + }) + + o.afterEach(() => { + const errors = [] + const roots = registeredRoots + registeredRoots = null + for (const root of roots) { + try { + m.render(root, null) + } catch (e) { + errors.push(e) + } + } + var mock = rafMock + $window = null + rafMock = null + restoreDOMGlobals() + console.error = originalConsoleError + clearPending() + o(errors).deepEquals([]) + errors.length = 0 + o(mock.queueLength()).equals(0) + return env.cleanup && env.cleanup(G) + }) + + return { + initialize, + register, + + /** @returns {ReturnType} */ + get window() { + return $window + }, + + /** @returns {ReturnType} */ + get rafMock() { + return rafMock + }, + + get root() { + return $window.document.body + }, + } +} diff --git a/test-utils/injectBrowserMock.js b/test-utils/injectBrowserMock.js new file mode 100644 index 000000000..62d04f444 --- /dev/null +++ b/test-utils/injectBrowserMock.js @@ -0,0 +1,10 @@ +/* global global: false */ +import browserMock from "../test-utils/browserMock.js" + +const mock = browserMock() +if (typeof global !== "undefined") { + global.window = mock + global.document = mock.document +} + +export {mock as default} diff --git a/test-utils/parseURL.js b/test-utils/parseURL.js index e60cc531f..2d4d5bac2 100644 --- a/test-utils/parseURL.js +++ b/test-utils/parseURL.js @@ -1,6 +1,4 @@ -"use strict" - -module.exports = function parseURL(url, root) { +export default function parseURL(url, root) { var data = {} var protocolIndex = url.indexOf("://") var pathnameIndex = protocolIndex > -1 ? url.indexOf("/", protocolIndex + 3) : url.indexOf("/") diff --git a/test-utils/pushStateMock.js b/test-utils/pushStateMock.js index e65f79e79..9f603dcbb 100644 --- a/test-utils/pushStateMock.js +++ b/test-utils/pushStateMock.js @@ -1,23 +1,9 @@ -"use strict" +import {callAsync} from "../test-utils/callAsync.js" +import parseURL from "../test-utils/parseURL.js" -var parseURL = require("../test-utils/parseURL") -var callAsync = require("../test-utils/callAsync") - -function debouncedAsync(f) { - var ref - return function() { - if (ref != null) return - ref = callAsync(function(){ - ref = null - f() - }) - } -} - -module.exports = function(options) { +export default function pushStateMock($window, options) { if (options == null) options = {} - var $window = options.window || {} var protocol = options.protocol || "http:" var hostname = options.hostname || "localhost" var port = "" @@ -52,10 +38,15 @@ module.exports = function(options) { if (value === "") return "" return (value.charAt(0) !== prefix ? prefix : "") + value } - function _hashchange() { - if (typeof $window.onhashchange === "function") $window.onhashchange({type: "hashchange"}) + var hashchangePending = false + function hashchange() { + if (hashchangePending) return + callAsync(() => { + hashchangePending = false + if (typeof $window.onhashchange === "function") $window.onhashchange({type: "hashchange"}) + }) + hashchangePending = true } - var hashchange = debouncedAsync(_hashchange) function popstate() { if (typeof $window.onpopstate === "function") $window.onpopstate({type: "popstate", state: $window.history.state}) } @@ -183,8 +174,8 @@ module.exports = function(options) { return past.length === 0 ? null : past[past.length - 1].state }, } - $window.onpopstate = null, - $window.onhashchange = null, + $window.onpopstate = null + $window.onhashchange = null $window.onunload = null $window.addEventListener = function (name, handler) { @@ -194,6 +185,4 @@ module.exports = function(options) { $window.removeEventListener = function (name, handler) { $window["on" + name] = handler } - - return $window } diff --git a/test-utils/tests/test-browserMock.js b/test-utils/tests/test-browserMock.js deleted file mode 100644 index c08b573fe..000000000 --- a/test-utils/tests/test-browserMock.js +++ /dev/null @@ -1,40 +0,0 @@ -"use strict" - -var o = require("ospec") -var browserMock = require("../../test-utils/browserMock") -var callAsync = require("../../test-utils/callAsync") -o.spec("browserMock", function() { - - var $window - o.beforeEach(function() { - $window = browserMock() - }) - - o("Mocks DOM, pushState and XHR", function() { - o($window.location).notEquals(undefined) - o($window.document).notEquals(undefined) - o($window.XMLHttpRequest).notEquals(undefined) - }) - o("$window.onhashchange can be reached from the pushStateMock functions", function(done) { - $window.onhashchange = o.spy() - $window.location.hash = "#a" - - callAsync(function(){ - o($window.onhashchange.callCount).equals(1) - done() - }) - }) - o("$window.onpopstate can be reached from the pushStateMock functions", function() { - $window.onpopstate = o.spy() - $window.history.pushState(null, null, "#a") - $window.history.back() - - o($window.onpopstate.callCount).equals(1) - }) - o("$window.onunload can be reached from the pushStateMock functions", function() { - $window.onunload = o.spy() - $window.location.href = "/a" - - o($window.onunload.callCount).equals(1) - }) -}) diff --git a/test-utils/tests/test-callAsync.js b/test-utils/tests/test-callAsync.js deleted file mode 100644 index 579269e25..000000000 --- a/test-utils/tests/test-callAsync.js +++ /dev/null @@ -1,25 +0,0 @@ -"use strict" - -var o = require("ospec") -var callAsync = require("../../test-utils/callAsync") - -o.spec("callAsync", function() { - o("works", function(done) { - var count = 0 - callAsync(function() { - o(count).equals(1) - done() - }) - count++ - }) - o("gets called before setTimeout", function(done) { - var timeout - callAsync(function() { - clearTimeout(timeout) - done() - }) - timeout = setTimeout(function() { - throw new Error("callAsync was called too slow") - }, 5) - }) -}) diff --git a/test-utils/tests/test-components.js b/test-utils/tests/test-components.js deleted file mode 100644 index 941fe7cf2..000000000 --- a/test-utils/tests/test-components.js +++ /dev/null @@ -1,55 +0,0 @@ -"use strict" - -var o = require("ospec") -var components = require("../../test-utils/components") -var m = require("../../render/hyperscript") - -o.spec("test-utils/components", function() { - var test = o.spy(function(component) { - return function() { - o("works", function() { - o(typeof component.kind).equals("string") - - var methods = {oninit: function(){}, view: function(){}} - - var cmp1, cmp2 - - if (component.kind === "POJO") { - cmp1 = component.create() - cmp2 = component.create(methods) - } else if (component.kind === "constructible") { - cmp1 = new (component.create()) - cmp2 = new (component.create(methods)) - } else if (component.kind === "closure") { - cmp1 = component.create()() - cmp2 = component.create(methods)() - } else { - throw new Error("unexpected component kind") - } - - o(cmp1 != null).equals(true) - o(typeof cmp1.view).equals("function") - - var vnode = cmp1.view() - - o(vnode != null).equals(true) - o(vnode).deepEquals(m("div")) - - if (component.kind !== "constructible") { - o(cmp2).deepEquals(methods) - } else { - // deepEquals doesn't search the prototype, do it manually - o(cmp2 != null).equals(true) - o(cmp2.view).equals(methods.view) - o(cmp2.oninit).equals(methods.oninit) - } - }) - } - }) - o.after(function(){ - o(test.callCount).equals(3) - }) - components.forEach(function(component) { - o.spec(component.kind, test(component)) - }) -}) diff --git a/test-utils/tests/test-pushStateMock.js b/test-utils/tests/test-pushStateMock.js deleted file mode 100644 index 989d1dc21..000000000 --- a/test-utils/tests/test-pushStateMock.js +++ /dev/null @@ -1,705 +0,0 @@ -"use strict" - -var o = require("ospec") -var pushStateMock = require("../../test-utils/pushStateMock") -var callAsync = require("../../test-utils/callAsync") -o.spec("pushStateMock", function() { - - var $window - o.beforeEach(function() { - $window = pushStateMock() - }) - - o.spec("initial state", function() { - o("has url on page load", function() { - o($window.location.href).equals("http://localhost/") - }) - }) - - o.spec("set href", function() { - o("changes url on location.href change", function() { - var old = $window.location.href - $window.location.href = "http://localhost/a" - - o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/a") - }) - o("changes url on relative location.href change", function() { - var old = $window.location.href - $window.location.href = "a" - - o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/a") - o($window.location.pathname).equals("/a") - }) - o("changes url on dotdot location.href change", function() { - $window.location.href = "a" - var old = $window.location.href - $window.location.href = ".." - - o(old).equals("http://localhost/a") - o($window.location.href).equals("http://localhost/") - o($window.location.pathname).equals("/") - }) - o("changes url on deep dotdot location.href change", function() { - $window.location.href = "a/b/c" - var old = $window.location.href - $window.location.href = ".." - - o(old).equals("http://localhost/a/b/c") - o($window.location.href).equals("http://localhost/a") - o($window.location.pathname).equals("/a") - }) - o("does not change url on dotdot location.href change from root", function() { - var old = $window.location.href - $window.location.href = ".." - - o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/") - o($window.location.pathname).equals("/") - }) - o("changes url on dot relative location.href change", function() { - var old = $window.location.href - $window.location.href = "a" - $window.location.href = "./b" - - o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/b") - o($window.location.pathname).equals("/b") - }) - o("does not change url on dot location.href change", function() { - var old = $window.location.href - $window.location.href = "a" - $window.location.href = "." - - o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/a") - o($window.location.pathname).equals("/a") - }) - o("changes url on hash-only location.href change", function() { - var old = $window.location.href - $window.location.href = "#a" - - o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/#a") - o($window.location.hash).equals("#a") - }) - o("changes url on search-only location.href change", function() { - var old = $window.location.href - $window.location.href = "?a" - - o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/?a") - o($window.location.search).equals("?a") - }) - o("changes hash on location.href change", function() { - var old = $window.location.href - $window.location.href = "http://localhost/a#b" - - o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/a#b") - o($window.location.hash).equals("#b") - }) - o("changes search on location.href change", function() { - var old = $window.location.href - $window.location.href = "http://localhost/a?b" - - o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/a?b") - o($window.location.search).equals("?b") - }) - o("changes search and hash on location.href change", function() { - var old = $window.location.href - $window.location.href = "http://localhost/a?b#c" - - o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/a?b#c") - o($window.location.search).equals("?b") - o($window.location.hash).equals("#c") - }) - o("handles search with search and hash", function() { - var old = $window.location.href - $window.location.href = "http://localhost/a?b?c#d" - - o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/a?b?c#d") - o($window.location.search).equals("?b?c") - o($window.location.hash).equals("#d") - }) - o("handles hash with search and hash", function() { - var old = $window.location.href - $window.location.href = "http://localhost/a#b?c#d" - - o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/a#b?c#d") - o($window.location.search).equals("") - o($window.location.hash).equals("#b?c#d") - }) - }) - o.spec("set search", function() { - o("changes url on location.search change", function() { - var old = $window.location.href - $window.location.search = "?b" - - o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/?b") - o($window.location.search).equals("?b") - }) - }) - o.spec("set hash", function() { - o("changes url on location.hash change", function() { - var old = $window.location.href - $window.location.hash = "#b" - - o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/#b") - o($window.location.hash).equals("#b") - }) - }) - o.spec("set pathname", function() { - o("changes url on location.pathname change", function() { - var old = $window.location.href - $window.location.pathname = "/a" - - o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/a") - o($window.location.pathname).equals("/a") - }) - }) - o.spec("set protocol", function() { - o("setting protocol throws", function(done) { - try { - $window.location.protocol = "https://" - } - catch (e) { - return done() - } - throw new Error("Expected an error") - }) - }) - o.spec("set port", function() { - o("setting origin changes href", function() { - var old = $window.location.href - $window.location.port = "81" - - o(old).equals("http://localhost/") - o($window.location.port).equals("81") - o($window.location.href).equals("http://localhost:81/") - }) - }) - o.spec("set hostname", function() { - o("setting hostname changes href", function() { - var old = $window.location.href - $window.location.hostname = "127.0.0.1" - - o(old).equals("http://localhost/") - o($window.location.hostname).equals("127.0.0.1") - o($window.location.href).equals("http://127.0.0.1/") - }) - }) - o.spec("set origin", function() { - o("setting origin is ignored", function() { - var old = $window.location.href - $window.location.origin = "http://127.0.0.1" - - o(old).equals("http://localhost/") - o($window.location.origin).equals("http://localhost") - }) - }) - o.spec("set host", function() { - o("setting host is ignored", function() { - var old = $window.location.href - $window.location.host = "http://127.0.0.1" - - o(old).equals("http://localhost/") - o($window.location.host).equals("localhost") - }) - }) - o.spec("pushState", function() { - o("changes url on pushstate", function() { - var old = $window.location.href - $window.history.pushState(null, null, "http://localhost/a") - - o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/a") - }) - o("changes search on pushstate", function() { - var old = $window.location.href - $window.history.pushState(null, null, "http://localhost/?a") - - o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/?a") - o($window.location.search).equals("?a") - }) - o("changes search on relative pushstate", function() { - var old = $window.location.href - $window.history.pushState(null, null, "?a") - - o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/?a") - o($window.location.search).equals("?a") - }) - o("changes hash on pushstate", function() { - var old = $window.location.href - $window.history.pushState(null, null, "http://localhost/#a") - - o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/#a") - o($window.location.hash).equals("#a") - }) - o("changes hash on relative pushstate", function() { - var old = $window.location.href - $window.history.pushState(null, null, "#a") - - o(old).equals("http://localhost/") - o($window.location.href).equals("http://localhost/#a") - o($window.location.hash).equals("#a") - }) - }) - o.spec("onpopstate", function() { - o("history.back() without history does not trigger onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.back() - - o($window.onpopstate.callCount).equals(0) - }) - o("history.back() after pushstate triggers onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.pushState(null, null, "http://localhost/a") - $window.history.back() - - o($window.onpopstate.callCount).equals(1) - o($window.onpopstate.args[0].type).equals("popstate") - }) - o("history.back() after relative pushstate triggers onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.pushState(null, null, "a") - $window.history.back() - - o($window.onpopstate.callCount).equals(1) - }) - o("history.back() after search pushstate triggers onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.pushState(null, null, "http://localhost/?a") - $window.history.back() - - o($window.onpopstate.callCount).equals(1) - }) - o("history.back() after relative search pushstate triggers onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.pushState(null, null, "?a") - $window.history.back() - - o($window.onpopstate.callCount).equals(1) - }) - o("history.back() after hash pushstate triggers onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.pushState(null, null, "http://localhost/#a") - $window.history.back() - - o($window.onpopstate.callCount).equals(1) - }) - o("history.back() after relative hash pushstate triggers onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.pushState(null, null, "#a") - $window.history.back() - - o($window.onpopstate.callCount).equals(1) - }) - o("history.back() after replacestate does not trigger onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.replaceState(null, null, "http://localhost/a") - $window.history.back() - - o($window.onpopstate.callCount).equals(0) - }) - o("history.back() after relative replacestate does not trigger onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.replaceState(null, null, "a") - $window.history.back() - - o($window.onpopstate.callCount).equals(0) - }) - o("history.back() after relative search replacestate does not trigger onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.replaceState(null, null, "?a") - $window.history.back() - - o($window.onpopstate.callCount).equals(0) - }) - o("history.back() after relative hash replacestate does not trigger onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.replaceState(null, null, "#a") - $window.history.back() - - o($window.onpopstate.callCount).equals(0) - }) - o("history.forward() after pushstate triggers onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.pushState(null, null, "http://localhost/a") - $window.history.back() - $window.history.forward() - - o($window.onpopstate.callCount).equals(2) - }) - o("history.forward() after relative pushstate triggers onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.pushState(null, null, "a") - $window.history.back() - $window.history.forward() - - o($window.onpopstate.callCount).equals(2) - }) - o("history.forward() after search pushstate triggers onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.pushState(null, null, "http://localhost/?a") - $window.history.back() - $window.history.forward() - - o($window.onpopstate.callCount).equals(2) - }) - o("history.forward() after relative search pushstate triggers onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.pushState(null, null, "?a") - $window.history.back() - $window.history.forward() - - o($window.onpopstate.callCount).equals(2) - }) - o("history.forward() after hash pushstate triggers onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.pushState(null, null, "http://localhost/#a") - $window.history.back() - $window.history.forward() - - o($window.onpopstate.callCount).equals(2) - }) - o("history.forward() after relative hash pushstate triggers onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.pushState(null, null, "#a") - $window.history.back() - $window.history.forward() - - o($window.onpopstate.callCount).equals(2) - }) - o("history.forward() without history does not trigger onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.forward() - - o($window.onpopstate.callCount).equals(0) - }) - o("history navigation without history does not trigger onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.back() - $window.history.forward() - - o($window.onpopstate.callCount).equals(0) - }) - o("reverse history navigation without history does not trigger onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.forward() - $window.history.back() - - o($window.onpopstate.callCount).equals(0) - }) - o("onpopstate has correct url during call", function(done) { - $window.location.href = "a" - $window.onpopstate = function() { - o($window.location.href).equals("http://localhost/a") - done() - } - $window.history.pushState(null, null, "b") - $window.history.back() - }) - o("replaceState does not break forward history", function() { - $window.onpopstate = o.spy() - - $window.history.pushState(null, null, "b") - $window.history.back() - - o($window.onpopstate.callCount).equals(1) - o($window.location.href).equals("http://localhost/") - - $window.history.replaceState(null, null, "a") - - o($window.location.href).equals("http://localhost/a") - - $window.history.forward() - - o($window.onpopstate.callCount).equals(2) - o($window.location.href).equals("http://localhost/b") - }) - o("pushstate retains state", function() { - $window.onpopstate = o.spy() - - $window.history.pushState({a: 1}, null, "#a") - $window.history.pushState({b: 2}, null, "#b") - - o($window.onpopstate.callCount).equals(0) - - $window.history.back() - - o($window.onpopstate.callCount).equals(1) - o($window.onpopstate.args[0].type).equals("popstate") - o($window.onpopstate.args[0].state).deepEquals({a: 1}) - - $window.history.back() - - o($window.onpopstate.callCount).equals(2) - o($window.onpopstate.args[0].type).equals("popstate") - o($window.onpopstate.args[0].state).equals(null) - - $window.history.forward() - - o($window.onpopstate.callCount).equals(3) - o($window.onpopstate.args[0].type).equals("popstate") - o($window.onpopstate.args[0].state).deepEquals({a: 1}) - - $window.history.forward() - - o($window.onpopstate.callCount).equals(4) - o($window.onpopstate.args[0].type).equals("popstate") - o($window.onpopstate.args[0].state).deepEquals({b: 2}) - }) - o("replacestate replaces state", function() { - $window.onpopstate = o.spy(pop) - - $window.history.replaceState({a: 1}, null, "a") - - o($window.history.state).deepEquals({a: 1}) - - $window.history.pushState(null, null, "a") - $window.history.back() - - function pop(e) { - o(e.state).deepEquals({a: 1}) - o($window.history.state).deepEquals({a: 1}) - } - }) - }) - o.spec("onhashchance", function() { - o("onhashchange triggers on location.href change", function(done) { - $window.onhashchange = o.spy() - $window.location.href = "http://localhost/#a" - - callAsync(function(){ - o($window.onhashchange.callCount).equals(1) - o($window.onhashchange.args[0].type).equals("hashchange") - done() - }) - }) - o("onhashchange triggers on relative location.href change", function(done) { - $window.onhashchange = o.spy() - $window.location.href = "#a" - - callAsync(function(){ - o($window.onhashchange.callCount).equals(1) - done() - }) - }) - o("onhashchange triggers on location.hash change", function(done) { - $window.onhashchange = o.spy() - $window.location.hash = "#a" - - callAsync(function(){ - o($window.onhashchange.callCount).equals(1) - done() - }) - }) - o("onhashchange does not trigger on page change", function(done) { - $window.onhashchange = o.spy() - $window.location.href = "http://localhost/a" - - callAsync(function(){ - o($window.onhashchange.callCount).equals(0) - done() - }) - }) - o("onhashchange does not trigger on page change with different hash", function(done) { - $window.location.href = "http://localhost/#a" - callAsync(function(){ - $window.onhashchange = o.spy() - $window.location.href = "http://localhost/a#b" - - callAsync(function(){ - o($window.onhashchange.callCount).equals(0) - done() - }) - }) - }) - o("onhashchange does not trigger on page change with same hash", function(done) { - $window.location.href = "http://localhost/#b" - callAsync(function(){ - $window.onhashchange = o.spy() - $window.location.href = "http://localhost/a#b" - - callAsync(function(){ - o($window.onhashchange.callCount).equals(0) - done() - }) - }) - }) - o("onhashchange triggers on history.back()", function(done) { - $window.location.href = "#a" - callAsync(function(){ - $window.onhashchange = o.spy() - $window.history.back() - - callAsync(function(){ - o($window.onhashchange.callCount).equals(1) - done() - }) - }) - }) - o("onhashchange triggers on history.forward()", function(done) { - $window.location.href = "#a" - callAsync(function(){ - $window.onhashchange = o.spy() - $window.history.back() - callAsync(function(){ - $window.history.forward() - - callAsync(function(){ - o($window.onhashchange.callCount).equals(2) - done() - }) - }) - }) - }) - o("onhashchange triggers once when the hash changes twice in a single tick", function(done) { - $window.location.href = "#a" - callAsync(function(){ - $window.onhashchange = o.spy() - $window.history.back() - $window.history.forward() - - callAsync(function(){ - o($window.onhashchange.callCount).equals(1) - done() - }) - }) - }) - o("onhashchange does not trigger on history.back() that causes page change with different hash", function(done) { - $window.location.href = "#a" - $window.location.href = "a#b" - callAsync(function(){ - $window.onhashchange = o.spy() - $window.history.back() - - callAsync(function(){ - o($window.onhashchange.callCount).equals(0) - done() - }) - }) - }) - o("onhashchange does not trigger on history.back() that causes page change with same hash", function(done) { - $window.location.href = "#a" - $window.location.href = "a#a" - callAsync(function(){ - $window.onhashchange = o.spy() - $window.history.back() - - callAsync(function(){ - o($window.onhashchange.callCount).equals(0) - done() - }) - }) - }) - o("onhashchange does not trigger on history.forward() that causes page change with different hash", function(done) { - $window.location.href = "#a" - $window.location.href = "a#b" - callAsync(function(){ - $window.onhashchange = o.spy() - $window.history.back() - $window.history.forward() - - callAsync(function(){ - o($window.onhashchange.callCount).equals(0) - done() - }) - }) - }) - o("onhashchange does not trigger on history.forward() that causes page change with same hash", function(done) { - $window.location.href = "#a" - $window.location.href = "a#b" - callAsync(function(){ - $window.onhashchange = o.spy() - $window.history.back() - $window.history.forward() - - callAsync(function(){ - o($window.onhashchange.callCount).equals(0) - done() - }) - }) - }) - }) - o.spec("onunload", function() { - o("onunload triggers on location.href change", function() { - $window.onunload = o.spy() - $window.location.href = "http://localhost/a" - - o($window.onunload.callCount).equals(1) - o($window.onunload.args[0].type).equals("unload") - }) - o("onunload triggers on relative location.href change", function() { - $window.onunload = o.spy() - $window.location.href = "a" - - o($window.onunload.callCount).equals(1) - }) - o("onunload triggers on search change via location.href", function() { - $window.onunload = o.spy() - $window.location.href = "http://localhost/?a" - - o($window.onunload.callCount).equals(1) - }) - o("onunload triggers on relative search change via location.href", function() { - $window.onunload = o.spy() - $window.location.href = "?a" - - o($window.onunload.callCount).equals(1) - }) - o("onunload does not trigger on hash change via location.href", function() { - $window.onunload = o.spy() - $window.location.href = "http://localhost/#a" - - o($window.onunload.callCount).equals(0) - }) - o("onunload does not trigger on relative hash change via location.href", function() { - $window.onunload = o.spy() - $window.location.href = "#a" - - o($window.onunload.callCount).equals(0) - }) - o("onunload does not trigger on hash-only history.back()", function() { - $window.location.href = "#a" - $window.onunload = o.spy() - $window.history.back() - - o($window.onunload.callCount).equals(0) - }) - o("onunload does not trigger on hash-only history.forward()", function() { - $window.location.href = "#a" - $window.history.back() - $window.onunload = o.spy() - $window.history.forward() - - o($window.onunload.callCount).equals(0) - }) - o("onunload has correct url during call via location.href change", function(done) { - $window.onunload = function() { - o($window.location.href).equals("http://localhost/") - done() - } - $window.location.href = "a" - }) - o("onunload has correct url during call via location.search change", function(done) { - $window.onunload = function() { - o($window.location.href).equals("http://localhost/") - done() - } - $window.location.search = "?a" - }) - }) -}) diff --git a/test-utils/tests/test-xhrMock.js b/test-utils/tests/test-xhrMock.js deleted file mode 100644 index dee088eb8..000000000 --- a/test-utils/tests/test-xhrMock.js +++ /dev/null @@ -1,100 +0,0 @@ -"use strict" - -var o = require("ospec") -var xhrMock = require("../../test-utils/xhrMock") - -o.spec("xhrMock", function() { - var $window - o.beforeEach(function() { - $window = xhrMock() - }) - - o.spec("xhr", function() { - o("works", function(done) { - $window.$defineRoutes({ - "GET /item": function(request) { - o(request.url).equals("/item") - return {status: 200, responseText: "test"} - } - }) - var xhr = new $window.XMLHttpRequest() - xhr.open("GET", "/item") - xhr.onreadystatechange = function() { - if (xhr.readyState === 4) { - o(xhr.status).equals(200) - o(xhr.responseText).equals("test") - done() - } - } - xhr.send() - }) - o("works w/ search", function(done) { - $window.$defineRoutes({ - "GET /item": function(request) { - o(request.query).equals("?a=b") - return {status: 200, responseText: "test"} - } - }) - var xhr = new $window.XMLHttpRequest() - xhr.open("GET", "/item?a=b") - xhr.onreadystatechange = function() { - if (xhr.readyState === 4) { - done() - } - } - xhr.send() - }) - o("works w/ body", function(done) { - $window.$defineRoutes({ - "POST /item": function(request) { - o(request.body).equals("a=b") - return {status: 200, responseText: "test"} - } - }) - var xhr = new $window.XMLHttpRequest() - xhr.open("POST", "/item") - xhr.onreadystatechange = function() { - if (xhr.readyState === 4) { - done() - } - } - xhr.send("a=b") - }) - o("passes event to onreadystatechange", function(done) { - $window.$defineRoutes({ - "GET /item": function(request) { - o(request.url).equals("/item") - return {status: 200, responseText: "test"} - } - }) - var xhr = new $window.XMLHttpRequest() - xhr.open("GET", "/item") - xhr.onreadystatechange = function(ev) { - o(ev.target).equals(xhr) - if (xhr.readyState === 4) { - done() - } - } - xhr.send() - }) - o("handles routing error", function(done) { - var xhr = new $window.XMLHttpRequest() - xhr.open("GET", "/nonexistent") - xhr.onreadystatechange = function() { - if (xhr.readyState === 4) { - o(xhr.status).equals(500) - done() - } - } - xhr.send("a=b") - }) - o("Setting a header twice merges the header", function() { - // Source: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader - var xhr = new $window.XMLHttpRequest() - xhr.open("POST", "/test") - xhr.setRequestHeader("Content-Type", "foo") - xhr.setRequestHeader("Content-Type", "bar") - o(xhr.getRequestHeader("Content-Type")).equals("foo, bar") - }) - }) -}) diff --git a/test-utils/throttleMock.js b/test-utils/throttleMock.js index 21eb53be6..247bd7485 100644 --- a/test-utils/throttleMock.js +++ b/test-utils/throttleMock.js @@ -1,18 +1,27 @@ -"use strict" - -module.exports = function() { - var queue = [] +export default function throttleMocker() { + let queue = new Map() + let id = 0 return { - schedule: function(fn) { - queue.push(fn) + schedule(fn) { + queue.set(++id, fn) + return id }, - fire: function() { - var tasks = queue - queue = [] - tasks.forEach(function(fn) {fn()}) + clear(id) { + queue.delete(id) }, - queueLength: function(){ - return queue.length + fire() { + const tasks = queue + queue = new Map() + for (const fn of tasks.values()) { + try { + fn() + } catch (e) { + console.error(e) + } + } + }, + queueLength() { + return queue.size } } } diff --git a/test-utils/xhrMock.js b/test-utils/xhrMock.js deleted file mode 100644 index bd42c5b70..000000000 --- a/test-utils/xhrMock.js +++ /dev/null @@ -1,142 +0,0 @@ -"use strict" - -var callAsync = require("../test-utils/callAsync") -var parseURL = require("../test-utils/parseURL") -var parseQueryString = require("../querystring/parse") - -module.exports = function() { - var routes = {} - // var callback = "callback" - var serverErrorHandler = function(url) { - return {status: 500, responseText: "server error, most likely the URL was not defined " + url} - } - - function FormData() {} - var $window = { - FormData: FormData, - URLSearchParams: URLSearchParams, - XMLHttpRequest: function XMLHttpRequest() { - var args = {} - var headers = {} - var aborted = false - this.setRequestHeader = function(header, value) { - /* - the behavior of setHeader is not your expected setX API. - If the header is already set, it'll merge with whatever you add - rather than overwrite - Source: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader - */ - if (headers[header]) { - headers[header] += ", " + value; - } else { - headers[header] = value - } - } - this.getRequestHeader = function(header) { - return headers[header] - } - this.open = function(method, url, async, user, password) { - var urlData = parseURL(url, {protocol: "http:", hostname: "localhost", port: "", pathname: "/"}) - args.rawUrl = url - args.method = method - args.pathname = urlData.pathname - args.search = urlData.search - args.async = async != null ? async : true - args.user = user - args.password = password - } - this.responseType = "" - this.response = null - this.timeout = 0 - Object.defineProperty(this, "responseText", {get: function() { - if (this.responseType === "" || this.responseType === "text") { - return this.response - } else { - throw new Error("Failed to read the 'responseText' property from 'XMLHttpRequest': The value is only accessible if the object's 'responseType' is '' or 'text' (was '" + this.responseType + "').") - } - }}) - this.send = function(body) { - var self = this - - var completeResponse = function (data) { - self._responseCompleted = true - if(!aborted) { - self.status = data.status - // Match spec - if (self.responseType === "json") { - try { self.response = JSON.parse(data.responseText) } - catch (e) { /* ignore */ } - } else { - self.response = data.responseText - } - } else { - self.status = 0 - } - self.readyState = 4 - if (args.async === true) { - callAsync(function() { - if (typeof self.onreadystatechange === "function") self.onreadystatechange({target: self}) - }) - } - } - - var data - if (!aborted) { - var handler = routes[args.method + " " + args.pathname] || serverErrorHandler.bind(null, args.pathname) - data = handler({rawUrl: args.rawUrl, url: args.pathname, query: args.search || {}, body: body || null}) - } - - if (typeof self.timeout === "number" && self.timeout > 0) { - setTimeout(function () { - if (self._responseCompleted) { - return - } - - self.status = 0; - if (typeof self.ontimeout === "function") self.ontimeout({target: self, type:"timeout"}) - }, self.timeout) - } - - if (data instanceof Promise) { - data.then(completeResponse) - } else { - completeResponse(data) - } - } - this.abort = function() { - aborted = true - } - }, - document: { - createElement: function(tag) { - return {nodeName: tag.toUpperCase(), parentNode: null} - }, - documentElement: { - appendChild: function(element) { - element.parentNode = this - if (element.nodeName === "SCRIPT") { - var urlData = parseURL(element.src, {protocol: "http:", hostname: "localhost", port: "", pathname: "/"}) - var handler = routes["GET " + urlData.pathname] || serverErrorHandler.bind(null, element.src) - var data = handler({url: urlData.pathname, query: urlData.search, body: null}) - parseQueryString(urlData.search) - callAsync(function() { - if (data.status === 200) { - new Function("$window", "with ($window) return " + data.responseText).call($window, $window) - } - else if (typeof element.onerror === "function") { - element.onerror({type: "error"}) - } - }) - } - }, - removeChild: function(element) { - element.parentNode = null - }, - }, - }, - $defineRoutes: function(rules) { - routes = rules - }, - } - return $window -} diff --git a/render/tests/test-attributes.js b/tests/core/attributes.js similarity index 50% rename from render/tests/test-attributes.js rename to tests/core/attributes.js index 1daed5e5f..5c1c6bf43 100644 --- a/render/tests/test-attributes.js +++ b/tests/core/attributes.js @@ -1,18 +1,12 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") -var m = require("../../render/hyperscript") -var trust = require("../../render/trust") +import {setupGlobals} from "../../test-utils/global.js" + +import m from "../../src/entry/mithril.esm.js" o.spec("attributes", function() { - var $window, root, render - o.beforeEach(function() { - $window = domMock() - root = $window.document.body - render = vdom($window) - }) + var G = setupGlobals() + o.spec("basics", function() { o("works (create/update/remove)", function() { @@ -20,45 +14,45 @@ o.spec("attributes", function() { var b = m("div", {id: "test"}) var c = m("div") - render(root, a); + m.render(G.root, a); - o(a.dom.hasAttribute("id")).equals(false) + o(a.d.hasAttribute("id")).equals(false) - render(root, b); + m.render(G.root, b); - o(b.dom.getAttribute("id")).equals("test") + o(b.d.getAttribute("id")).equals("test") - render(root, c); + m.render(G.root, c); - o(c.dom.hasAttribute("id")).equals(false) + o(c.d.hasAttribute("id")).equals(false) }) o("undefined attr is equivalent to a lack of attr", function() { var a = m("div", {id: undefined}) var b = m("div", {id: "test"}) var c = m("div", {id: undefined}) - render(root, a); + m.render(G.root, a); - o(a.dom.hasAttribute("id")).equals(false) + o(a.d.hasAttribute("id")).equals(false) - render(root, b); + m.render(G.root, b); - o(b.dom.hasAttribute("id")).equals(true) - o(b.dom.getAttribute("id")).equals("test") + o(b.d.hasAttribute("id")).equals(true) + o(b.d.getAttribute("id")).equals("test") // #1804 - render(root, c); + m.render(G.root, c); - o(c.dom.hasAttribute("id")).equals(false) + o(c.d.hasAttribute("id")).equals(false) }) }) o.spec("customElements", function(){ o("when vnode is customElement without property, custom setAttribute called", function(){ - var f = $window.document.createElement + var f = G.window.document.createElement var spies = [] - $window.document.createElement = function(tag, is){ + G.window.document.createElement = function(tag, is){ var el = f(tag, is) var spy = o.spy(el.setAttribute) el.setAttribute = spy @@ -67,7 +61,7 @@ o.spec("attributes", function() { return el } - render(root, [ + m.render(G.root, [ m("input", {value: "hello"}), m("input", {value: "hello"}), m("input", {value: "hello"}), @@ -80,17 +74,20 @@ o.spec("attributes", function() { o(spies[0].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[4].calls).deepEquals([ + {this: spies[4].elem, args: ["is", "something-special"]}, + {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 + o("when vnode is customElement with property, custom setAttribute only called for `is`", function(){ + var f = G.window.document.createElement var spies = [] var getters = [] var setters = [] - $window.document.createElement = function(tag, is){ + G.window.document.createElement = function(tag, is){ var el = f(tag, is) var spy = o.spy(el.setAttribute) el.setAttribute = spy @@ -111,7 +108,7 @@ o.spec("attributes", function() { return el } - render(root, [ + m.render(G.root, [ m("input", {value: "hello"}), m("input", {value: "hello"}), m("input", {value: "hello"}), @@ -124,7 +121,8 @@ o.spec("attributes", function() { 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[4].callCount).equals(1) + o(spies[4].args[0]).equals("is") o(spies[5].callCount).equals(0) o(getters[0].callCount).equals(0) o(getters[1].callCount).equals(0) @@ -134,130 +132,138 @@ o.spec("attributes", function() { o(setters[2].calls).deepEquals([{this: spies[5].elem, args: ["x"]}]) }) + o("`is` attribute is not removed when the attribute is removed from hyperscript", function(){ + var vnode = m("input") + + m.render(G.root, m("input", {is: "something-special"})) + m.render(G.root, vnode) + + o(G.root.firstChild).equals(vnode.d) + o(G.root.firstChild.attributes["is"].value).equals("something-special") + }) + }) o.spec("input readonly", function() { o("when input readonly is true, attribute is present", function() { var a = m("input", {readonly: true}) - render(root, a) + m.render(G.root, a) - o(a.dom.attributes["readonly"].value).equals("") + o(a.d.attributes["readonly"].value).equals("") }) o("when input readonly is false, attribute is not present", function() { var a = m("input", {readonly: false}) - render(root, a) + m.render(G.root, a) - o(a.dom.attributes["readonly"]).equals(undefined) + o(a.d.attributes["readonly"]).equals(undefined) }) }) o.spec("input checked", function() { o("when input checked is true, attribute is not present", function() { var a = m("input", {checked: true}) - render(root, a) + m.render(G.root, a) - o(a.dom.checked).equals(true) - o(a.dom.attributes["checked"]).equals(undefined) + o(a.d.checked).equals(true) + o(a.d.attributes["checked"]).equals(undefined) }) o("when input checked is false, attribute is not present", function() { var a = m("input", {checked: false}) - render(root, a) + m.render(G.root, a) - o(a.dom.checked).equals(false) - o(a.dom.attributes["checked"]).equals(undefined) + o(a.d.checked).equals(false) + o(a.d.attributes["checked"]).equals(undefined) }) o("after input checked is changed by 3rd party, it can still be changed by render", function() { var a = m("input", {checked: false}) var b = m("input", {checked: true}) - render(root, a) + m.render(G.root, a) - a.dom.checked = true //setting the javascript property makes the value no longer track the state of the attribute - a.dom.checked = false + a.d.checked = true //setting the javascript property makes the value no longer track the state of the attribute + a.d.checked = false - render(root, b) + m.render(G.root, b) - o(a.dom.checked).equals(true) - o(a.dom.attributes["checked"]).equals(undefined) + o(a.d.checked).equals(true) + o(a.d.attributes["checked"]).equals(undefined) }) }) o.spec("input.value", function() { o("can be set as text", function() { var a = m("input", {value: "test"}) - render(root, a); + m.render(G.root, a); - o(a.dom.value).equals("test") + o(a.d.value).equals("test") }) o("a lack of attribute removes `value`", function() { var a = m("input") var b = m("input", {value: "test"}) var c = m("input") - render(root, a) + m.render(G.root, a) - o(a.dom.value).equals("") + o(a.d.value).equals("") - render(root, b) + m.render(G.root, b) - o(a.dom.value).equals("test") + o(a.d.value).equals("test") // https://github.com/MithrilJS/mithril.js/issues/1804#issuecomment-304521235 - render(root, c) + m.render(G.root, c) - o(a.dom.value).equals("") + o(a.d.value).equals("") }) o("can be set as number", function() { var a = m("input", {value: 1}) - render(root, a); + m.render(G.root, a); - o(a.dom.value).equals("1") + o(a.d.value).equals("1") }) o("null becomes the empty string", function() { var a = m("input", {value: null}) var b = m("input", {value: "test"}) var c = m("input", {value: null}) - render(root, a); + m.render(G.root, a); - o(a.dom.value).equals("") - o(a.dom.getAttribute("value")).equals(null) + o(a.d.value).equals("") + o(a.d.getAttribute("value")).equals(null) - render(root, b); + m.render(G.root, b); - o(b.dom.value).equals("test") - o(b.dom.getAttribute("value")).equals(null) + o(b.d.value).equals("test") + o(b.d.getAttribute("value")).equals(null) - render(root, c); + m.render(G.root, c); - o(c.dom.value).equals("") - o(c.dom.getAttribute("value")).equals(null) + o(c.d.value).equals("") + o(c.d.getAttribute("value")).equals(null) }) o("'' and 0 are different values", function() { var a = m("input", {value: 0}) var b = m("input", {value: ""}) var c = m("input", {value: 0}) - render(root, a); + m.render(G.root, a); - o(a.dom.value).equals("0") + o(a.d.value).equals("0") - render(root, b); + m.render(G.root, b); - o(b.dom.value).equals("") + o(b.d.value).equals("") // #1595 redux - render(root, c); + m.render(G.root, c); - o(c.dom.value).equals("0") + o(c.d.value).equals("0") }) o("isn't set when equivalent to the previous value and focused", function() { - var $window = domMock({spy: o.spy}) - var root = $window.document.body - var render = vdom($window) + G.initialize({spy: o.spy}) var a =m("input") var b = m("input", {value: "1"}) @@ -265,58 +271,50 @@ o.spec("attributes", function() { var d = m("input", {value: 1}) var e = m("input", {value: 2}) - render(root, a) - var spies = $window.__getSpies(a.dom) - a.dom.focus() + m.render(G.root, a) + var spies = G.window.__getSpies(a.d) + a.d.focus() o(spies.valueSetter.callCount).equals(0) - render(root, b) + m.render(G.root, b) - o(b.dom.value).equals("1") + o(b.d.value).equals("1") o(spies.valueSetter.callCount).equals(1) - render(root, c) + m.render(G.root, c) - o(c.dom.value).equals("1") + o(c.d.value).equals("1") o(spies.valueSetter.callCount).equals(1) - render(root, d) + m.render(G.root, d) - o(d.dom.value).equals("1") + o(d.d.value).equals("1") o(spies.valueSetter.callCount).equals(1) - render(root, e) + m.render(G.root, e) - o(d.dom.value).equals("2") + o(d.d.value).equals("2") o(spies.valueSetter.callCount).equals(2) }) }) o.spec("input.type", function() { - o("the input.type setter is never used", function() { - var $window = domMock({spy: o.spy}) - var root = $window.document.body - var render = vdom($window) - + o("works", function() { var a = m("input", {type: "radio"}) var b = m("input", {type: "text"}) var c = m("input") - render(root, a) - var spies = $window.__getSpies(a.dom) + m.render(G.root, a) - o(spies.typeSetter.callCount).equals(0) - o(a.dom.getAttribute("type")).equals("radio") + o(a.d.getAttribute("type")).equals("radio") - render(root, b) + m.render(G.root, b) - o(spies.typeSetter.callCount).equals(0) - o(b.dom.getAttribute("type")).equals("text") + o(b.d.getAttribute("type")).equals("text") - render(root, c) + m.render(G.root, c) - o(spies.typeSetter.callCount).equals(0) - o(c.dom.hasAttribute("type")).equals(false) + o(c.d.hasAttribute("type")).equals(false) }) }) o.spec("textarea.value", function() { @@ -324,19 +322,17 @@ o.spec("attributes", function() { var a = m("textarea", {value:"x"}) var b = m("textarea") - render(root, a) + m.render(G.root, a) - o(a.dom.value).equals("x") + o(a.d.value).equals("x") // https://github.com/MithrilJS/mithril.js/issues/1804#issuecomment-304521235 - render(root, b) + m.render(G.root, b) - o(b.dom.value).equals("") + o(b.d.value).equals("") }) o("isn't set when equivalent to the previous value and focused", function() { - var $window = domMock({spy: o.spy}) - var root = $window.document.body - var render = vdom($window) + G.initialize({spy: o.spy}) var a = m("textarea") var b = m("textarea", {value: "1"}) @@ -344,30 +340,30 @@ o.spec("attributes", function() { var d = m("textarea", {value: 1}) var e = m("textarea", {value: 2}) - render(root, a) - var spies = $window.__getSpies(a.dom) - a.dom.focus() + m.render(G.root, a) + var spies = G.window.__getSpies(a.d) + a.d.focus() o(spies.valueSetter.callCount).equals(0) - render(root, b) + m.render(G.root, b) - o(b.dom.value).equals("1") + o(b.d.value).equals("1") o(spies.valueSetter.callCount).equals(1) - render(root, c) + m.render(G.root, c) - o(c.dom.value).equals("1") + o(c.d.value).equals("1") o(spies.valueSetter.callCount).equals(1) - render(root, d) + m.render(G.root, d) - o(d.dom.value).equals("1") + o(d.d.value).equals("1") o(spies.valueSetter.callCount).equals(1) - render(root, e) + m.render(G.root, e) - o(d.dom.value).equals("2") + o(d.d.value).equals("2") o(spies.valueSetter.callCount).equals(2) }) }) @@ -375,54 +371,54 @@ o.spec("attributes", function() { o("when link href is true, attribute is present", function() { var a = m("a", {href: true}) - render(root, a) + m.render(G.root, a) - o(a.dom.attributes["href"]).notEquals(undefined) + o(a.d.attributes["href"]).notEquals(undefined) }) o("when link href is false, attribute is not present", function() { var a = m("a", {href: false}) - render(root, a) + m.render(G.root, a) - o(a.dom.attributes["href"]).equals(undefined) + o(a.d.attributes["href"]).equals(undefined) }) }) o.spec("canvas width and height", function() { o("uses attribute API", function() { var canvas = m("canvas", {width: "100%"}) - render(root, canvas) + m.render(G.root, canvas) - o(canvas.dom.attributes["width"].value).equals("100%") - o(canvas.dom.width).equals(100) + o(canvas.d.attributes["width"].value).equals("100%") + o(canvas.d.width).equals(100) }) }) o.spec("svg", function() { o("when className is specified then it should be added as a class", function() { var a = m("svg", {className: "test"}) - render(root, a); + m.render(G.root, a); - o(a.dom.attributes["class"].value).equals("test") + o(a.d.attributes["class"].value).equals("test") }) /* eslint-disable no-script-url */ o("handles xlink:href", function() { var vnode = m("svg", {ns: "http://www.w3.org/2000/svg"}, m("a", {ns: "http://www.w3.org/2000/svg", "xlink:href": "javascript:;"}) ) - render(root, vnode) + m.render(G.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") + o(vnode.d.nodeName).equals("svg") + o(vnode.d.firstChild.attributes["href"].value).equals("javascript:;") + o(vnode.d.firstChild.attributes["href"].namespaceURI).equals("http://www.w3.org/1999/xlink") vnode = m("svg", {ns: "http://www.w3.org/2000/svg"}, m("a", {ns: "http://www.w3.org/2000/svg"}) ) - render(root, vnode) + m.render(G.root, vnode) - o(vnode.dom.nodeName).equals("svg") - o("href" in vnode.dom.firstChild.attributes).equals(false) + o(vnode.d.nodeName).equals("svg") + o("href" in vnode.d.firstChild.attributes).equals(false) }) /* eslint-enable no-script-url */ }) @@ -430,59 +426,57 @@ o.spec("attributes", function() { o("can be set as text", function() { var a = m("option", {value: "test"}) - render(root, a); + m.render(G.root, a); - o(a.dom.value).equals("test") + o(a.d.value).equals("test") }) o("can be set as number", function() { var a = m("option", {value: 1}) - render(root, a); + m.render(G.root, a); - o(a.dom.value).equals("1") + o(a.d.value).equals("1") }) o("null removes the attribute", function() { var a = m("option", {value: null}) var b = m("option", {value: "test"}) var c = m("option", {value: null}) - render(root, a); + m.render(G.root, a); - o(a.dom.value).equals("") - o(a.dom.hasAttribute("value")).equals(false) + o(a.d.value).equals("") + o(a.d.hasAttribute("value")).equals(false) - render(root, b); + m.render(G.root, b); - o(b.dom.value).equals("test") - o(b.dom.getAttribute("value")).equals("test") + o(b.d.value).equals("test") + o(b.d.getAttribute("value")).equals("test") - render(root, c); + m.render(G.root, c); - o(c.dom.value).equals("") - o(c.dom.hasAttribute("value")).equals(false) + o(c.d.value).equals("") + o(c.d.hasAttribute("value")).equals(false) }) o("'' and 0 are different values", function() { var a = m("option", {value: 0}, "") var b = m("option", {value: ""}, "") var c = m("option", {value: 0}, "") - render(root, a); + m.render(G.root, a); - o(a.dom.value).equals("0") + o(a.d.value).equals("0") - render(root, b); + m.render(G.root, b); - o(a.dom.value).equals("") + o(a.d.value).equals("") // #1595 redux - render(root, c); + m.render(G.root, c); - o(c.dom.value).equals("0") + o(c.d.value).equals("0") }) o("isn't set when equivalent to the previous value", function() { - var $window = domMock({spy: o.spy}) - var root = $window.document.body - var render = vdom($window) + G.initialize({spy: o.spy}) var a = m("option") var b = m("option", {value: "1"}) @@ -490,29 +484,29 @@ o.spec("attributes", function() { var d = m("option", {value: 1}) var e = m("option", {value: 2}) - render(root, a) - var spies = $window.__getSpies(a.dom) + m.render(G.root, a) + var spies = G.window.__getSpies(a.d) o(spies.valueSetter.callCount).equals(0) - render(root, b) + m.render(G.root, b) - o(b.dom.value).equals("1") + o(b.d.value).equals("1") o(spies.valueSetter.callCount).equals(1) - render(root, c) + m.render(G.root, c) - o(c.dom.value).equals("1") + o(c.d.value).equals("1") o(spies.valueSetter.callCount).equals(1) - render(root, d) + m.render(G.root, d) - o(d.dom.value).equals("1") + o(d.d.value).equals("1") o(spies.valueSetter.callCount).equals(1) - render(root, e) + m.render(G.root, e) - o(d.dom.value).equals("2") + o(d.d.value).equals("2") o(spies.valueSetter.callCount).equals(2) }) }) @@ -537,7 +531,7 @@ o.spec("attributes", function() { var select = m("select", {selectedIndex: 0}, m("option", {value: "1", selected: ""}) ) - render(root, select) + m.render(G.root, select) }) */ o("can be set as text", function() { @@ -545,167 +539,51 @@ o.spec("attributes", function() { var b = makeSelect("2") var c = makeSelect("a") - render(root, a) + m.render(G.root, a) - o(a.dom.value).equals("1") - o(a.dom.selectedIndex).equals(0) + o(a.d.value).equals("1") + o(a.d.selectedIndex).equals(0) - render(root, b) + m.render(G.root, b) - o(b.dom.value).equals("2") - o(b.dom.selectedIndex).equals(1) + o(b.d.value).equals("2") + o(b.d.selectedIndex).equals(1) - render(root, c) + m.render(G.root, c) - o(c.dom.value).equals("a") - o(c.dom.selectedIndex).equals(2) + o(c.d.value).equals("a") + o(c.d.selectedIndex).equals(2) }) o("setting null unsets the value", function() { var a = makeSelect(null) - render(root, a) + m.render(G.root, a) - o(a.dom.value).equals("") - o(a.dom.selectedIndex).equals(-1) + o(a.d.value).equals("") + o(a.d.selectedIndex).equals(-1) }) o("values are type converted", function() { var a = makeSelect(1) var b = makeSelect(2) - render(root, a) - - o(a.dom.value).equals("1") - o(a.dom.selectedIndex).equals(0) - - render(root, b) - - o(b.dom.value).equals("2") - o(b.dom.selectedIndex).equals(1) - }) - o("'' and 0 are different values when focused", function() { - var a = makeSelect("") - var b = makeSelect(0) - - render(root, a) - a.dom.focus() - - o(a.dom.value).equals("") - - // #1595 redux - render(root, b) - - o(b.dom.value).equals("0") - }) - o("'' and null are different values when focused", function() { - var a = makeSelect("") - var b = makeSelect(null) - var c = makeSelect("") - - render(root, a) - a.dom.focus() - - o(a.dom.value).equals("") - o(a.dom.selectedIndex).equals(4) - - render(root, b) - - o(b.dom.value).equals("") - o(b.dom.selectedIndex).equals(-1) - - render(root, c) - - o(c.dom.value).equals("") - o(c.dom.selectedIndex).equals(4) - }) - o("updates with the same value do not re-set the attribute if the select has focus", function() { - var $window = domMock({spy: o.spy}) - var root = $window.document.body - var render = vdom($window) - - var a = makeSelect() - var b = makeSelect("1") - var c = makeSelect(1) - var d = makeSelect("2") - - render(root, a) - var spies = $window.__getSpies(a.dom) - a.dom.focus() - - o(spies.valueSetter.callCount).equals(0) - o(a.dom.value).equals("1") - - render(root, b) - - o(spies.valueSetter.callCount).equals(0) - o(b.dom.value).equals("1") - - render(root, c) - - o(spies.valueSetter.callCount).equals(0) - o(c.dom.value).equals("1") - - render(root, d) - - o(spies.valueSetter.callCount).equals(1) - o(d.dom.value).equals("2") - }) - }) - o.spec("contenteditable throws on untrusted children", function() { - o("including elements", function() { - var div = m("div", {contenteditable: true}, m("script", {src: "http://evil.com"})) - var succeeded = false - - try { - render(root, div) - - succeeded = true - } - catch(e){/* ignore */} - - o(succeeded).equals(false) - }) - o("tolerating empty children", function() { - var div = m("div", {contenteditable: true}) - var succeeded = false - - try { - render(root, div) - - succeeded = true - } - catch(e){/* ignore */} - - o(succeeded).equals(true) - }) - o("tolerating trusted content", function() { - var div = m("div", {contenteditable: true}, trust("")) - var succeeded = false + m.render(G.root, a) - try { - render(root, div) + o(a.d.value).equals("1") + o(a.d.selectedIndex).equals(0) - succeeded = true - } - catch(e){/* ignore */} + m.render(G.root, b) - o(succeeded).equals(true) + o(b.d.value).equals("2") + o(b.d.selectedIndex).equals(1) }) }) o.spec("mutate attr object", function() { - o("warn when reusing attrs object", function() { - const _consoleWarn = console.warn - console.warn = o.spy() - + o("throw when reusing attrs object", function() { const attrs = {className: "on"} - render(root, {tag: "input", attrs}) + m.render(G.root, m("input", attrs)) attrs.className = "off" - render(root, {tag: "input", attrs}) - - o(console.warn.callCount).equals(1) - o(console.warn.args[0]).equals("Don't reuse attrs object, use new object for every redraw, this will throw in next major") - - console.warn = _consoleWarn + o(() => m.render(G.root, m("input", attrs))).throws(Error) }) }) }) diff --git a/tests/core/component.js b/tests/core/component.js new file mode 100644 index 000000000..de45b2f58 --- /dev/null +++ b/tests/core/component.js @@ -0,0 +1,469 @@ +import o from "ospec" + +import {setupGlobals} from "../../test-utils/global.js" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("component", function() { + var G = setupGlobals() + + o.spec("basics", function() { + o("works", function() { + var component = () => m("div", {id: "a"}, "b") + var node = m(component) + + m.render(G.root, node) + + o(G.root.firstChild.nodeName).equals("DIV") + o(G.root.firstChild.attributes["id"].value).equals("a") + o(G.root.firstChild.firstChild.nodeValue).equals("b") + }) + o("receives arguments", function() { + var component = (attrs) => m("div", attrs) + var node = m(component, {id: "a"}, "b") + + m.render(G.root, node) + + o(G.root.firstChild.nodeName).equals("DIV") + o(G.root.firstChild.attributes["id"].value).equals("a") + o(G.root.firstChild.firstChild.nodeValue).equals("b") + }) + o("updates", function() { + var component = (attrs) => m("div", attrs) + m.render(G.root, [m(component, {id: "a"}, "b")]) + m.render(G.root, [m(component, {id: "c"}, "d")]) + + o(G.root.firstChild.nodeName).equals("DIV") + o(G.root.firstChild.attributes["id"].value).equals("c") + o(G.root.firstChild.firstChild.nodeValue).equals("d") + }) + o("updates root from null", function() { + var visible = false + var component = () => (visible ? m("div") : null) + m.render(G.root, m(component)) + visible = true + m.render(G.root, m(component)) + + o(G.root.firstChild.nodeName).equals("DIV") + }) + o("updates root from primitive", function() { + var visible = false + var component = () => (visible ? m("div") : false) + m.render(G.root, m(component)) + visible = true + m.render(G.root, m(component)) + + o(G.root.firstChild.nodeName).equals("DIV") + }) + o("updates root to null", function() { + var visible = true + var component = () => (visible ? m("div") : null) + m.render(G.root, m(component)) + visible = false + m.render(G.root, m(component)) + + o(G.root.childNodes.length).equals(0) + }) + o("updates root to primitive", function() { + var visible = true + var component = () => (visible ? m("div") : false) + m.render(G.root, m(component)) + visible = false + m.render(G.root, m(component)) + + o(G.root.childNodes.length).equals(0) + }) + o("updates root from null to null", function() { + var component = () => null + m.render(G.root, m(component)) + m.render(G.root, m(component)) + + o(G.root.childNodes.length).equals(0) + }) + o("removes", function() { + var component = () => m("div") + m.render(G.root, m.keyed([[1, m(component)], [2, m("div")]])) + var div = m("div") + m.render(G.root, m.keyed([[2, div]])) + + o(G.root.childNodes.length).equals(1) + o(G.root.firstChild).equals(div.d) + }) + o("svg works when creating across component boundary", function() { + var component = () => m("g") + m.render(G.root, m("svg", m(component))) + + o(G.root.firstChild.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") + }) + o("svg works when updating across component boundary", function() { + var component = () => m("g") + m.render(G.root, m("svg", m(component))) + m.render(G.root, m("svg", m(component))) + + o(G.root.firstChild.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") + }) + }) + o.spec("return value", function() { + o("can return fragments", function() { + var component = () => [ + m("label"), + m("input"), + ] + m.render(G.root, m(component)) + + o(G.root.childNodes.length).equals(2) + o(G.root.childNodes[0].nodeName).equals("LABEL") + o(G.root.childNodes[1].nodeName).equals("INPUT") + }) + o("can return string", function() { + var component = () => "a" + m.render(G.root, m(component)) + + o(G.root.firstChild.nodeType).equals(3) + o(G.root.firstChild.nodeValue).equals("a") + }) + o("can return falsy string", function() { + var component = () => "" + m.render(G.root, m(component)) + + o(G.root.firstChild.nodeType).equals(3) + o(G.root.firstChild.nodeValue).equals("") + }) + o("can return number", function() { + var component = () => 1 + m.render(G.root, m(component)) + + o(G.root.firstChild.nodeType).equals(3) + o(G.root.firstChild.nodeValue).equals("1") + }) + o("can return falsy number", function() { + var component = () => 0 + m.render(G.root, m(component)) + + o(G.root.firstChild.nodeType).equals(3) + o(G.root.firstChild.nodeValue).equals("0") + }) + o("can return `true`", function() { + var component = () => true + m.render(G.root, m(component)) + + o(G.root.childNodes.length).equals(0) + }) + o("can return `false`", function() { + var component = () => false + m.render(G.root, m(component)) + + o(G.root.childNodes.length).equals(0) + }) + o("can return null", function() { + var component = () => null + m.render(G.root, m(component)) + + o(G.root.childNodes.length).equals(0) + }) + o("can return undefined", function() { + var component = () => undefined + m.render(G.root, m(component)) + + o(G.root.childNodes.length).equals(0) + }) + o("throws a custom error if it returns itself when created", function() { + // A view that returns its vnode would otherwise trigger an infinite loop + var component = () => vnode + + console.error = o.spy() + + var vnode = m(component) + m.render(G.root, vnode) + + o(console.error.callCount).equals(1) + o(console.error.args[0] instanceof Error).equals(true) + // Call stack exception is a RangeError + o(console.error.args[0] instanceof RangeError).equals(false) + }) + o("throws a custom error if it returns itself when updated", function() { + // A view that returns its vnode would otherwise trigger an infinite loop + var component = () => vnode + m.render(G.root, m(component)) + + o(G.root.childNodes.length).equals(0) + + console.error = o.spy() + + var vnode = m(component) + m.render(G.root, m(component)) + + o(console.error.callCount).equals(1) + o(console.error.args[0] instanceof Error).equals(true) + // Call stack exception is a RangeError + o(console.error.args[0] instanceof RangeError).equals(false) + }) + o("can update when returning fragments", function() { + var component = () => [ + m("label"), + m("input"), + ] + m.render(G.root, m(component)) + m.render(G.root, m(component)) + + o(G.root.childNodes.length).equals(2) + o(G.root.childNodes[0].nodeName).equals("LABEL") + o(G.root.childNodes[1].nodeName).equals("INPUT") + }) + o("can update when returning primitive", function() { + var component = () => "a" + m.render(G.root, m(component)) + m.render(G.root, m(component)) + + o(G.root.firstChild.nodeType).equals(3) + o(G.root.firstChild.nodeValue).equals("a") + }) + o("can update when returning null", function() { + var component = () => null + m.render(G.root, m(component)) + m.render(G.root, m(component)) + + o(G.root.childNodes.length).equals(0) + }) + o("can remove when returning fragments", function() { + var component = () => [ + m("label"), + m("input"), + ] + var div = m("div") + m.render(G.root, m.keyed([[1, m(component)], [2, div]])) + + m.render(G.root, m.keyed([[2, m("div")]])) + + o(G.root.childNodes.length).equals(1) + o(G.root.firstChild).equals(div.d) + }) + o("can remove when returning primitive", function() { + var component = () => "a" + var div = m("div") + m.render(G.root, m.keyed([[1, m(component)], [2, div]])) + + m.render(G.root, m.keyed([[2, m("div")]])) + + o(G.root.childNodes.length).equals(1) + o(G.root.firstChild).equals(div.d) + }) + }) + o.spec("lifecycle", function() { + o("constructs", function() { + var called = 0 + var component = () => { + called++ + + o(G.root.childNodes.length).equals(0) + + return () => m("div", {id: "a"}, "b") + } + + m.render(G.root, m(component)) + + o(called).equals(1) + o(G.root.firstChild.nodeName).equals("DIV") + o(G.root.firstChild.attributes["id"].value).equals("a") + o(G.root.firstChild.firstChild.nodeValue).equals("b") + }) + o("constructs when returning fragment", function() { + var called = 0 + var component = () => { + called++ + + o(G.root.childNodes.length).equals(0) + + return () => [m("div", {id: "a"}, "b")] + } + + m.render(G.root, m(component)) + + o(called).equals(1) + o(G.root.firstChild.nodeName).equals("DIV") + o(G.root.firstChild.attributes["id"].value).equals("a") + o(G.root.firstChild.firstChild.nodeValue).equals("b") + }) + o("can call view function returned on initialization", function() { + var viewCalled = false + var component = () => { + o(viewCalled).equals(false) + return () => { + viewCalled = true + return m("div", {id: "a"}, "b") + } + } + + m.render(G.root, m(component)) + }) + o("does not initialize on redraw", function() { + var component = o.spy(() => () => m("div", {id: "a"}, "b")) + + function view() { + return m(component) + } + + m.render(G.root, view()) + m.render(G.root, view()) + + o(component.callCount).equals(1) + }) + o("calls inner `m.layout` callback on render", function() { + var layoutSpy = o.spy() + var component = () => [ + m.layout(layoutSpy), + m("div", {id: "a"}, "b"), + ] + + m.render(G.root, m(component)) + + o(layoutSpy.callCount).equals(1) + o(layoutSpy.calls[0].args[0]).equals(G.root) + o(G.root.firstChild.nodeName).equals("DIV") + o(G.root.firstChild.attributes["id"].value).equals("a") + o(G.root.firstChild.firstChild.nodeValue).equals("b") + + m.render(G.root, m(component)) + + o(layoutSpy.callCount).equals(2) + o(layoutSpy.calls[1].args[0]).equals(G.root) + o(G.root.firstChild.nodeName).equals("DIV") + o(G.root.firstChild.attributes["id"].value).equals("a") + o(G.root.firstChild.firstChild.nodeValue).equals("b") + }) + o("calls inner `m.remove` callback after first render", function() { + var removeSpy = o.spy() + var component = () => [ + m.remove(removeSpy), + m("div", {id: "a"}, "b"), + ] + + m.render(G.root, m(component)) + m.render(G.root, null) + + o(removeSpy.callCount).equals(1) + o(removeSpy.args[0]).equals(G.root) + o(G.root.childNodes.length).equals(0) + }) + o("calls inner `m.remove` callback after subsequent render", function() { + var removeSpy = o.spy() + var component = () => [ + m.remove(removeSpy), + m("div", {id: "a"}, "b"), + ] + + m.render(G.root, m(component)) + m.render(G.root, m(component)) + m.render(G.root, null) + + o(removeSpy.callCount).equals(1) + o(removeSpy.args[0]).equals(G.root) + o(G.root.childNodes.length).equals(0) + }) + o("calls in-element inner `m.layout` callback on render", function() { + var layoutSpy = o.spy() + var component = () => m("div", {id: "a"}, m.layout(layoutSpy), "b") + m.render(G.root, m(component)) + + o(layoutSpy.callCount).equals(1) + o(layoutSpy.calls[0].args[0]).equals(G.root.firstChild) + o(G.root.firstChild.nodeName).equals("DIV") + o(G.root.firstChild.attributes["id"].value).equals("a") + o(G.root.firstChild.firstChild.nodeValue).equals("b") + + m.render(G.root, m(component)) + + o(layoutSpy.callCount).equals(2) + o(layoutSpy.calls[1].args[0]).equals(G.root.firstChild) + o(G.root.firstChild.nodeName).equals("DIV") + o(G.root.firstChild.attributes["id"].value).equals("a") + o(G.root.firstChild.firstChild.nodeValue).equals("b") + }) + o("calls in-element inner `m.remove` callback after first render", function() { + var removeSpy = o.spy() + var component = () => m("div", {id: "a"}, m.remove(removeSpy), "b") + m.render(G.root, m(component)) + var firstChild = G.root.firstChild + m.render(G.root, null) + + o(removeSpy.callCount).equals(1) + o(removeSpy.args[0]).equals(firstChild) + o(G.root.childNodes.length).equals(0) + }) + o("calls in-element inner `m.remove` callback after subsequent render", function() { + var removeSpy = o.spy() + var component = () => m("div", {id: "a"}, m.remove(removeSpy), "b") + m.render(G.root, m(component)) + m.render(G.root, m(component)) + var firstChild = G.root.firstChild + m.render(G.root, null) + + o(removeSpy.callCount).equals(1) + o(removeSpy.args[0]).equals(firstChild) + o(G.root.childNodes.length).equals(0) + }) + o("calls direct inner `m.layout` callback on render", function() { + var createSpy = o.spy() + var component = () => m.layout(createSpy) + + m.render(G.root, m(component)) + + o(createSpy.callCount).equals(1) + o(createSpy.calls[0].args[0]).equals(G.root) + o(G.root.childNodes.length).equals(0) + + m.render(G.root, m(component)) + + o(createSpy.callCount).equals(2) + o(createSpy.calls[1].args[0]).equals(G.root) + o(G.root.childNodes.length).equals(0) + }) + o("calls direct inner `m.remove` callback after first render", function() { + var removeSpy = o.spy() + var component = () => m.layout(removeSpy) + m.render(G.root, m(component)) + m.render(G.root, null) + + o(removeSpy.callCount).equals(1) + o(removeSpy.args[0]).equals(G.root) + o(G.root.childNodes.length).equals(0) + }) + o("calls direct inner `m.remove` callback after subsequent render", function() { + var removeSpy = o.spy() + var component = () => m.remove(removeSpy) + m.render(G.root, m(component)) + m.render(G.root, m(component)) + m.render(G.root, null) + + o(removeSpy.callCount).equals(1) + o(removeSpy.args[0]).equals(G.root) + o(G.root.childNodes.length).equals(0) + }) + o("no recycling observable with `m.layout` (was: recycled components get a fresh state)", function() { + var createSpy = o.spy() + var component = o.spy(() => m("div", m.layout(createSpy))) + + m.render(G.root, [m("div", m.keyed([[1, m(component)]]))]) + var child = G.root.firstChild.firstChild + m.render(G.root, []) + m.render(G.root, [m("div", m.keyed([[1, m(component)]]))]) + + o(child).notEquals(G.root.firstChild.firstChild) // this used to be a recycling pool test + o(component.callCount).equals(2) + }) + o("no recycling observable with `m.remove` (was: recycled components get a fresh state)", function() { + var createSpy = o.spy() + var component = o.spy(() => m("div", m.remove(createSpy))) + + m.render(G.root, [m("div", m.keyed([[1, m(component)]]))]) + var child = G.root.firstChild.firstChild + m.render(G.root, []) + m.render(G.root, [m("div", m.keyed([[1, m(component)]]))]) + var found = G.root.firstChild.firstChild + m.render(G.root, []) + + o(child).notEquals(found) // this used to be a recycling pool test + o(component.callCount).equals(2) + }) + }) +}) diff --git a/tests/core/context.js b/tests/core/context.js new file mode 100644 index 000000000..c9942f589 --- /dev/null +++ b/tests/core/context.js @@ -0,0 +1,119 @@ +import o from "ospec" + +import {setupGlobals} from "../../test-utils/global.js" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("context", () => { + var G = setupGlobals() + + function symbolsToStrings(object) { + var result = {} + for (const key of Reflect.ownKeys(object)) { + // Intentionally using `String(key)` to stringify symbols from `Symbol("foo")` to + // `"Symbol(foo)"` for deep equality. + Object.defineProperty(result, String(key), Object.getOwnPropertyDescriptor(object, key)) + } + return result + } + + function allKeys(context) { + if (context === null || typeof context !== "object") return undefined + let result = {...context} + while (context !== null && context !== Object.prototype) { + context = Object.getPrototypeOf(context) + result = {...context, ...result} + } + return symbolsToStrings(result) + } + + o("string keys are set in context", () => { + var redraw = () => {} + var Comp = o.spy() + var vnode = m.set({key: "value", one: "two"}, m(Comp)) + + m.render(G.root, vnode, {redraw}) + + o(Comp.callCount).equals(1) + o(allKeys(Comp.this)).deepEquals({ + redraw, + key: "value", + one: "two", + }) + + var vnode = m.set({key: "updated", two: "three"}, m(Comp)) + + m.render(G.root, vnode, {redraw}) + + o(Comp.callCount).equals(2) + o(allKeys(Comp.this)).deepEquals({ + redraw, + key: "updated", + two: "three", + }) + + m.render(G.root, null) + }) + + o("symbol keys are set in context", () => { + var key = Symbol("key") + var one = Symbol("one") + var two = Symbol("two") + + var redraw = () => {} + var Comp = o.spy() + var vnode = m.set({[key]: "value", [one]: "two"}, m(Comp)) + + m.render(G.root, vnode, {redraw}) + + o(Comp.callCount).equals(1) + o(allKeys(Comp.this)).deepEquals(symbolsToStrings({ + redraw, + [key]: "value", + [one]: "two", + })) + + var vnode = m.set({[key]: "updated", [two]: "three"}, m(Comp)) + + m.render(G.root, vnode, {redraw}) + + o(Comp.callCount).equals(2) + o(allKeys(Comp.this)).deepEquals(symbolsToStrings({ + redraw, + [key]: "updated", + [two]: "three", + })) + + m.render(G.root, null) + }) + + o("mixed keys are set in context", () => { + var key = Symbol("key") + + var redraw = () => {} + var Comp = o.spy() + var vnode = m.set({[key]: "value", one: "two"}, m(Comp)) + + m.render(G.root, vnode, {redraw}) + + o(Comp.callCount).equals(1) + o(allKeys(Comp.this)).deepEquals(symbolsToStrings({ + redraw, + [key]: "value", + one: "two", + })) + + var vnode = m.set({[key]: "updated", two: "three"}, m(Comp)) + + m.render(G.root, vnode, {redraw}) + + o(Comp.callCount).equals(2) + o(allKeys(Comp.this)).deepEquals(symbolsToStrings({ + redraw, + [key]: "updated", + two: "three", + })) + + m.render(G.root, null) + }) +}) diff --git a/tests/core/createElement.js b/tests/core/createElement.js new file mode 100644 index 000000000..e360ddd09 --- /dev/null +++ b/tests/core/createElement.js @@ -0,0 +1,104 @@ +import o from "ospec" + +import {setupGlobals} from "../../test-utils/global.js" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("createElement", function() { + var G = setupGlobals() + + o("creates element", function() { + var vnode = m("div") + m.render(G.root, vnode) + + o(vnode.d.nodeName).equals("DIV") + }) + o("creates attr", function() { + var vnode = m("div", {id: "a", title: "b"}) + m.render(G.root, vnode) + + o(vnode.d.nodeName).equals("DIV") + o(vnode.d.attributes["id"].value).equals("a") + o(vnode.d.attributes["title"].value).equals("b") + }) + o("creates style", function() { + var vnode = m("div", {style: {backgroundColor: "red"}}) + m.render(G.root, vnode) + + o(vnode.d.nodeName).equals("DIV") + o(vnode.d.style.backgroundColor).equals("red") + }) + o("allows css vars in style", function() { + var vnode = m("div", {style: {"--css-var": "red"}}) + m.render(G.root, vnode) + + o(vnode.d.style["--css-var"]).equals("red") + }) + o("allows css vars in style with uppercase letters", function() { + var vnode = m("div", {style: {"--cssVar": "red"}}) + m.render(G.root, vnode) + + o(vnode.d.style["--cssVar"]).equals("red") + }) + o("censors cssFloat to float", function() { + var vnode = m("a", {style: {cssFloat: "left"}}) + + m.render(G.root, vnode) + + o(vnode.d.style.float).equals("left") + }) + o("creates children", function() { + var vnode = m("div", m("a"), m("b")) + m.render(G.root, vnode) + + o(vnode.d.nodeName).equals("DIV") + o(vnode.d.childNodes.length).equals(2) + o(vnode.d.childNodes[0].nodeName).equals("A") + o(vnode.d.childNodes[1].nodeName).equals("B") + }) + o("creates attrs and children", function() { + var vnode = m("div", {id: "a", title: "b"}, m("a"), m("b")) + m.render(G.root, vnode) + + o(vnode.d.nodeName).equals("DIV") + o(vnode.d.attributes["id"].value).equals("a") + o(vnode.d.attributes["title"].value).equals("b") + o(vnode.d.childNodes.length).equals(2) + o(vnode.d.childNodes[0].nodeName).equals("A") + o(vnode.d.childNodes[1].nodeName).equals("B") + }) + /* eslint-disable no-script-url */ + o("creates svg", function() { + var vnode = m("svg", + m("a", {"xlink:href": "javascript:;"}), + m("foreignObject", m("body", {xmlns: "http://www.w3.org/1999/xhtml"})) + ) + m.render(G.root, vnode) + + o(vnode.d.nodeName).equals("svg") + o(vnode.d.namespaceURI).equals("http://www.w3.org/2000/svg") + o(vnode.d.firstChild.nodeName).equals("a") + o(vnode.d.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") + o(vnode.d.firstChild.attributes["href"].value).equals("javascript:;") + o(vnode.d.firstChild.attributes["href"].namespaceURI).equals("http://www.w3.org/1999/xlink") + o(vnode.d.childNodes[1].nodeName).equals("foreignObject") + o(vnode.d.childNodes[1].firstChild.nodeName).equals("body") + o(vnode.d.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 = m("svg", {viewBox: "0 0 100 100"}) + m.render(G.root, vnode) + + o(vnode.d.attributes["viewBox"].value).equals("0 0 100 100") + }) + o("creates mathml", function() { + var vnode = m("math", m("mrow")) + m.render(G.root, vnode) + + o(vnode.d.nodeName).equals("math") + o(vnode.d.namespaceURI).equals("http://www.w3.org/1998/Math/MathML") + o(vnode.d.firstChild.nodeName).equals("mrow") + o(vnode.d.firstChild.namespaceURI).equals("http://www.w3.org/1998/Math/MathML") + }) +}) diff --git a/tests/core/createFragment.js b/tests/core/createFragment.js new file mode 100644 index 000000000..540808486 --- /dev/null +++ b/tests/core/createFragment.js @@ -0,0 +1,46 @@ +import o from "ospec" + +import {setupGlobals} from "../../test-utils/global.js" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("createFragment", function() { + var G = setupGlobals() + + o("creates fragment", function() { + var vnode = m.normalize([m("a")]) + m.render(G.root, vnode) + + o(G.root.childNodes.length).equals(1) + o(G.root.childNodes[0].nodeName).equals("A") + }) + o("handles empty fragment", function() { + var vnode = m.normalize([]) + m.render(G.root, vnode) + + o(G.root.childNodes.length).equals(0) + }) + o("handles childless fragment", function() { + var vnode = m.normalize([]) + m.render(G.root, vnode) + + o(G.root.childNodes.length).equals(0) + }) + o("handles multiple children", function() { + var vnode = m.normalize([m("a"), m("b")]) + m.render(G.root, vnode) + + o(G.root.childNodes.length).equals(2) + o(G.root.childNodes[0].nodeName).equals("A") + o(G.root.childNodes[1].nodeName).equals("B") + o(vnode.c[0].d).equals(G.root.childNodes[0]) + }) + o("handles td", function() { + var vnode = m.normalize([m("td")]) + m.render(G.root, vnode) + + o(G.root.childNodes.length).equals(1) + o(G.root.childNodes[0].nodeName).equals("TD") + o(vnode.c[0].d).equals(G.root.childNodes[0]) + }) +}) diff --git a/tests/core/createNodes.js b/tests/core/createNodes.js new file mode 100644 index 000000000..9f99eb7ce --- /dev/null +++ b/tests/core/createNodes.js @@ -0,0 +1,51 @@ +import o from "ospec" + +import {setupGlobals} from "../../test-utils/global.js" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("createNodes", function() { + var G = setupGlobals() + + o("creates nodes", function() { + var vnodes = [ + m("a"), + "b", + ["c"], + ] + m.render(G.root, vnodes) + + o(G.root.childNodes.length).equals(3) + o(G.root.childNodes[0].nodeName).equals("A") + o(G.root.childNodes[1].nodeValue).equals("b") + o(G.root.childNodes[2].nodeValue).equals("c") + }) + o("ignores null", function() { + var vnodes = [ + m("a"), + "b", + null, + ["c"], + ] + m.render(G.root, vnodes) + + o(G.root.childNodes.length).equals(3) + o(G.root.childNodes[0].nodeName).equals("A") + o(G.root.childNodes[1].nodeValue).equals("b") + o(G.root.childNodes[2].nodeValue).equals("c") + }) + o("ignores undefined", function() { + var vnodes = [ + m("a"), + "b", + undefined, + ["c"], + ] + m.render(G.root, vnodes) + + o(G.root.childNodes.length).equals(3) + o(G.root.childNodes[0].nodeName).equals("A") + o(G.root.childNodes[1].nodeValue).equals("b") + o(G.root.childNodes[2].nodeValue).equals("c") + }) +}) diff --git a/tests/core/createText.js b/tests/core/createText.js new file mode 100644 index 000000000..745e94def --- /dev/null +++ b/tests/core/createText.js @@ -0,0 +1,64 @@ +import o from "ospec" + +import {setupGlobals} from "../../test-utils/global.js" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("createText", function() { + var G = setupGlobals() + + o("creates string", function() { + var vnode = "a" + m.render(G.root, vnode) + + o(G.root.firstChild.nodeName).equals("#text") + o(G.root.firstChild.nodeValue).equals("a") + }) + o("creates falsy string", function() { + var vnode = "" + m.render(G.root, vnode) + + o(G.root.firstChild.nodeName).equals("#text") + o(G.root.firstChild.nodeValue).equals("") + }) + o("creates number", function() { + var vnode = 1 + m.render(G.root, vnode) + + o(G.root.firstChild.nodeName).equals("#text") + o(G.root.firstChild.nodeValue).equals("1") + }) + o("creates falsy number", function() { + var vnode = 0 + m.render(G.root, vnode) + + o(G.root.firstChild.nodeName).equals("#text") + o(G.root.firstChild.nodeValue).equals("0") + }) + o("ignores true boolean", function() { + var vnode = true + m.render(G.root, vnode) + + o(G.root.childNodes.length).equals(0) + }) + o("creates false boolean", function() { + var vnode = false + m.render(G.root, vnode) + + o(G.root.childNodes.length).equals(0) + }) + o("creates spaces", function() { + var vnode = " " + m.render(G.root, vnode) + + o(G.root.firstChild.nodeName).equals("#text") + o(G.root.firstChild.nodeValue).equals(" ") + }) + o("ignores html", function() { + var vnode = "™" + m.render(G.root, vnode) + + o(G.root.firstChild.nodeName).equals("#text") + o(G.root.firstChild.nodeValue).equals("™") + }) +}) diff --git a/tests/core/event.js b/tests/core/event.js new file mode 100644 index 000000000..75767ba01 --- /dev/null +++ b/tests/core/event.js @@ -0,0 +1,313 @@ +import o from "ospec" + +import {setupGlobals} from "../../test-utils/global.js" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("event", function() { + var redraw + var G = setupGlobals({initialize() { redraw = o.spy() }}) + + function render(dom, vnode) { + return m.render(dom, vnode, {redraw}) + } + + function eventSpy(fn) { + function spy(e) { + spy.calls.push({ + this: this, type: e.type, + target: e.target, currentTarget: e.currentTarget, + }) + if (fn) return fn.apply(this, arguments) + } + spy.calls = [] + return spy + } + + o("handles onclick", function() { + var spyDiv = eventSpy() + var spyParent = eventSpy() + var div = m("div", {onclick: spyDiv}) + var parent = m("div", {onclick: spyParent}, div) + var e = G.window.document.createEvent("MouseEvents") + e.initEvent("click", true, true) + + render(G.root, parent) + div.d.dispatchEvent(e) + + o(spyDiv.calls.length).equals(1) + o(spyDiv.calls[0].this).equals(div.d) + o(spyDiv.calls[0].type).equals("click") + o(spyDiv.calls[0].target).equals(div.d) + o(spyDiv.calls[0].currentTarget).equals(div.d) + o(spyParent.calls.length).equals(1) + o(spyParent.calls[0].this).equals(parent.d) + o(spyParent.calls[0].type).equals("click") + o(spyParent.calls[0].target).equals(div.d) + o(spyParent.calls[0].currentTarget).equals(parent.d) + o(redraw.callCount).equals(2) + o(redraw.this).equals(undefined) + o(redraw.args.length).equals(0) + o(e.defaultPrevented).equals(false) + }) + + o("handles onclick asynchronously returning", function() { + var promise + var spyDiv = eventSpy(() => promise = Promise.resolve()) + var spyParent = eventSpy() + var div = m("div", {onclick: spyDiv}) + var parent = m("div", {onclick: spyParent}, div) + var e = G.window.document.createEvent("MouseEvents") + e.initEvent("click", true, true) + + render(G.root, parent) + div.d.dispatchEvent(e) + + o(spyDiv.calls.length).equals(1) + o(spyDiv.calls[0].this).equals(div.d) + o(spyDiv.calls[0].type).equals("click") + o(spyDiv.calls[0].target).equals(div.d) + o(spyDiv.calls[0].currentTarget).equals(div.d) + o(spyParent.calls.length).equals(1) + o(spyParent.calls[0].this).equals(parent.d) + o(spyParent.calls[0].type).equals("click") + o(spyParent.calls[0].target).equals(div.d) + o(spyParent.calls[0].currentTarget).equals(parent.d) + o(redraw.callCount).equals(1) + o(redraw.this).equals(undefined) + o(redraw.args.length).equals(0) + o(e.defaultPrevented).equals(false) + + return promise.then(() => { + o(redraw.callCount).equals(2) + o(redraw.this).equals(undefined) + o(redraw.args.length).equals(0) + }) + }) + + o("handles onclick returning `\"skip-redraw\"`", function() { + var spyDiv = eventSpy((e) => { m.capture(e); return "skip-redraw" }) + var spyParent = eventSpy() + var div = m("div", {onclick: spyDiv}) + var parent = m("div", {onclick: spyParent}, div) + var e = G.window.document.createEvent("MouseEvents") + e.initEvent("click", true, true) + + render(G.root, parent) + div.d.dispatchEvent(e) + + o(spyDiv.calls.length).equals(1) + o(spyDiv.calls[0].this).equals(div.d) + o(spyDiv.calls[0].type).equals("click") + o(spyDiv.calls[0].target).equals(div.d) + o(spyDiv.calls[0].currentTarget).equals(div.d) + o(spyParent.calls.length).equals(0) + o(redraw.callCount).equals(0) + o(e.defaultPrevented).equals(true) + }) + + o("handles onclick asynchronously returning `\"skip-redraw\"`", function() { + var promise + var spyDiv = eventSpy((e) => { m.capture(e); return promise = Promise.resolve("skip-redraw") }) + var spyParent = eventSpy() + var div = m("div", {onclick: spyDiv}) + var parent = m("div", {onclick: spyParent}, div) + var e = G.window.document.createEvent("MouseEvents") + e.initEvent("click", true, true) + + render(G.root, parent) + div.d.dispatchEvent(e) + + o(spyDiv.calls.length).equals(1) + o(spyDiv.calls[0].this).equals(div.d) + o(spyDiv.calls[0].type).equals("click") + o(spyDiv.calls[0].target).equals(div.d) + o(spyDiv.calls[0].currentTarget).equals(div.d) + o(spyParent.calls.length).equals(0) + o(redraw.callCount).equals(0) + o(e.defaultPrevented).equals(true) + + return promise.then(() => { + o(redraw.callCount).equals(0) + }) + }) + + o("handles onclick returning `\"skip-redraw\"` in child then bubbling to parent and not returning `\"skip-redraw\"`", function() { + var spyDiv = eventSpy(() => "skip-redraw") + var spyParent = eventSpy() + var div = m("div", {onclick: spyDiv}) + var parent = m("div", {onclick: spyParent}, div) + var e = G.window.document.createEvent("MouseEvents") + e.initEvent("click", true, true) + + render(G.root, parent) + div.d.dispatchEvent(e) + + o(spyDiv.calls.length).equals(1) + o(spyDiv.calls[0].this).equals(div.d) + o(spyDiv.calls[0].type).equals("click") + o(spyDiv.calls[0].target).equals(div.d) + o(spyDiv.calls[0].currentTarget).equals(div.d) + o(spyParent.calls.length).equals(1) + o(redraw.callCount).equals(1) + o(e.defaultPrevented).equals(false) + }) + + o("removes event", function() { + var spy = o.spy() + var vnode = m("a", {onclick: spy}) + var updated = m("a") + + render(G.root, vnode) + render(G.root, updated) + + var e = G.window.document.createEvent("MouseEvents") + e.initEvent("click", true, true) + vnode.d.dispatchEvent(e) + + o(spy.callCount).equals(0) + }) + + o("removes event when null", function() { + var spy = o.spy() + var vnode = m("a", {onclick: spy}) + var updated = m("a", {onclick: null}) + + render(G.root, vnode) + render(G.root, updated) + + var e = G.window.document.createEvent("MouseEvents") + e.initEvent("click", true, true) + vnode.d.dispatchEvent(e) + + o(spy.callCount).equals(0) + }) + + o("removes event when undefined", function() { + var spy = o.spy() + var vnode = m("a", {onclick: spy}) + var updated = m("a", {onclick: undefined}) + + render(G.root, vnode) + render(G.root, updated) + + var e = G.window.document.createEvent("MouseEvents") + e.initEvent("click", true, true) + vnode.d.dispatchEvent(e) + + o(spy.callCount).equals(0) + }) + + o("removes event added via addEventListener when null", function() { + var spy = o.spy() + var vnode = m("a", {ontouchstart: spy}) + var updated = m("a", {ontouchstart: null}) + + render(G.root, vnode) + render(G.root, updated) + + var e = G.window.document.createEvent("TouchEvents") + e.initEvent("touchstart", true, true) + vnode.d.dispatchEvent(e) + + o(spy.callCount).equals(0) + }) + + o("removes event added via addEventListener", function() { + var spy = o.spy() + var vnode = m("a", {ontouchstart: spy}) + var updated = m("a") + + render(G.root, vnode) + render(G.root, updated) + + var e = G.window.document.createEvent("TouchEvents") + e.initEvent("touchstart", true, true) + vnode.d.dispatchEvent(e) + + o(spy.callCount).equals(0) + }) + + o("removes event added via addEventListener when undefined", function() { + var spy = o.spy() + var vnode = m("a", {ontouchstart: spy}) + var updated = m("a", {ontouchstart: undefined}) + + render(G.root, vnode) + render(G.root, updated) + + var e = G.window.document.createEvent("TouchEvents") + e.initEvent("touchstart", true, true) + vnode.d.dispatchEvent(e) + + o(spy.callCount).equals(0) + }) + + o("fires onclick only once after redraw", function() { + var spy = o.spy() + var div = m("div", {id: "a", onclick: spy}) + var updated = m("div", {id: "b", onclick: spy}) + var e = G.window.document.createEvent("MouseEvents") + e.initEvent("click", true, true) + + render(G.root, div) + render(G.root, updated) + div.d.dispatchEvent(e) + + o(spy.callCount).equals(1) + o(spy.this).equals(div.d) + o(spy.args[0].type).equals("click") + o(spy.args[0].target).equals(div.d) + o(redraw.callCount).equals(1) + o(redraw.this).equals(undefined) + o(redraw.args.length).equals(0) + o(div.d).equals(updated.d) + o(div.d.attributes["id"].value).equals("b") + }) + + o("handles ontransitionend", function() { + var spy = o.spy() + var div = m("div", {ontransitionend: spy}) + var e = G.window.document.createEvent("HTMLEvents") + e.initEvent("transitionend", true, true) + + render(G.root, div) + div.d.dispatchEvent(e) + + o(spy.callCount).equals(1) + o(spy.this).equals(div.d) + o(spy.args[0].type).equals("transitionend") + o(spy.args[0].target).equals(div.d) + o(redraw.callCount).equals(1) + o(redraw.this).equals(undefined) + o(redraw.args.length).equals(0) + }) + + o("handles changed spy", function() { + var div1 = m("div", {ontransitionend: function() {}}) + + m.render(G.root, [div1], {redraw}) + var e = G.window.document.createEvent("HTMLEvents") + e.initEvent("transitionend", true, true) + div1.d.dispatchEvent(e) + + o(redraw.callCount).equals(1) + o(redraw.this).equals(undefined) + o(redraw.args.length).equals(0) + + var replacementRedraw = o.spy() + var div2 = m("div", {ontransitionend: function() {}}) + + m.render(G.root, [div2], {redraw: replacementRedraw}) + var e = G.window.document.createEvent("HTMLEvents") + e.initEvent("transitionend", true, true) + div2.d.dispatchEvent(e) + + o(redraw.callCount).equals(1) + o(redraw.this).equals(undefined) + o(redraw.args.length).equals(0) + o(replacementRedraw.callCount).equals(1) + o(replacementRedraw.this).equals(undefined) + o(replacementRedraw.args.length).equals(0) + }) +}) diff --git a/tests/core/fragment.js b/tests/core/fragment.js new file mode 100644 index 000000000..c7dd366a2 --- /dev/null +++ b/tests/core/fragment.js @@ -0,0 +1,180 @@ +/* eslint-disable no-bitwise */ +import o from "ospec" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("fragment literal", function() { + o("works", function() { + var child = m("p") + var frag = m.normalize([child]) + + o(frag.m & m.TYPE_MASK).equals(m.TYPE_FRAGMENT) + + o(Array.isArray(frag.c)).equals(true) + o(frag.c.length).equals(1) + o(frag.c[0]).equals(child) + }) + o.spec("children", function() { + o("handles string single child", function() { + var vnode = m.normalize(["a"]) + + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].a).equals("a") + }) + o("handles falsy string single child", function() { + var vnode = m.normalize([""]) + + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].a).equals("") + }) + o("handles number single child", function() { + var vnode = m.normalize([1]) + + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].a).equals("1") + }) + o("handles falsy number single child", function() { + var vnode = m.normalize([0]) + + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].a).equals("0") + }) + o("handles boolean single child", function() { + var vnode = m.normalize([true]) + + o(vnode.c).deepEquals([null]) + }) + o("handles falsy boolean single child", function() { + var vnode = m.normalize([false]) + + o(vnode.c).deepEquals([null]) + }) + o("handles null single child", function() { + var vnode = m.normalize([null]) + + o(vnode.c[0]).equals(null) + }) + o("handles undefined single child", function() { + var vnode = m.normalize([undefined]) + + o(vnode.c).deepEquals([null]) + }) + o("handles multiple string children", function() { + var vnode = m.normalize(["", "a"]) + + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].a).equals("") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[1].a).equals("a") + }) + o("handles multiple number children", function() { + var vnode = m.normalize([0, 1]) + + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].a).equals("0") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[1].a).equals("1") + }) + o("handles multiple boolean children", function() { + var vnode = m.normalize([false, true]) + + o(vnode.c).deepEquals([null, null]) + }) + o("handles multiple null/undefined child", function() { + var vnode = m.normalize([null, undefined]) + + o(vnode.c).deepEquals([null, null]) + }) + }) +}) + +o.spec("fragment component", function() { + o("works", function() { + var child = m("p") + var frag = m(m.Fragment, null, child) + + o(frag.m & m.TYPE_MASK).equals(m.TYPE_FRAGMENT) + + o(Array.isArray(frag.c)).equals(true) + o(frag.c.length).equals(1) + o(frag.c[0]).equals(child) + }) + o.spec("children", function() { + o("handles string single child", function() { + var vnode = m(m.Fragment, null, ["a"]) + + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].a).equals("a") + }) + o("handles falsy string single child", function() { + var vnode = m(m.Fragment, null, [""]) + + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].a).equals("") + }) + o("handles number single child", function() { + var vnode = m(m.Fragment, null, [1]) + + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].a).equals("1") + }) + o("handles falsy number single child", function() { + var vnode = m(m.Fragment, null, [0]) + + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].a).equals("0") + }) + o("handles boolean single child", function() { + var vnode = m(m.Fragment, null, [true]) + + o(vnode.c).deepEquals([null]) + }) + o("handles falsy boolean single child", function() { + var vnode = m(m.Fragment, null, [false]) + + o(vnode.c).deepEquals([null]) + }) + o("handles null single child", function() { + var vnode = m(m.Fragment, null, [null]) + + o(vnode.c[0]).equals(null) + }) + o("handles undefined single child", function() { + var vnode = m(m.Fragment, null, [undefined]) + + o(vnode.c).deepEquals([null]) + }) + o("handles multiple string children", function() { + var vnode = m(m.Fragment, null, ["", "a"]) + + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].a).equals("") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[1].a).equals("a") + }) + o("handles multiple number children", function() { + var vnode = m(m.Fragment, null, [0, 1]) + + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].a).equals("0") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[1].a).equals("1") + }) + o("handles multiple boolean children", function() { + var vnode = m(m.Fragment, null, [false, true]) + + o(vnode.c).deepEquals([null, null]) + }) + o("handles multiple null/undefined child", function() { + var vnode = m(m.Fragment, null, [null, undefined]) + + o(vnode.c).deepEquals([null, null]) + }) + o("handles falsy number single child without attrs", function() { + var vnode = m(m.Fragment, null, 0) + + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].a).equals("0") + }) + }) +}) diff --git a/tests/core/hyperscript.js b/tests/core/hyperscript.js new file mode 100644 index 000000000..64ef0e995 --- /dev/null +++ b/tests/core/hyperscript.js @@ -0,0 +1,740 @@ +/* eslint-disable no-bitwise */ +import o from "ospec" + +import {setupGlobals} from "../../test-utils/global.js" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("hyperscript", function() { + o.spec("selector", function() { + o("throws on null selector", function(done) { + try {m(null)} catch(e) {done()} + }) + o("throws on non-string selector w/o a view property", function(done) { + try {m({})} catch(e) {done()} + }) + o("handles tag in selector", function() { + var vnode = m("a") + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("a") + }) + o("class and className normalization", function(){ + o(m("a", { + class: null + }).a).deepEquals({ + class: null + }) + o(m("a", { + class: undefined + }).a).deepEquals({ + class: undefined + }) + o(m("a", { + class: false + }).a).deepEquals({ + class: false + }) + o(m("a", { + class: true + }).a).deepEquals({ + class: true + }) + o(m("a.x", { + class: null + }).a).deepEquals({ + class: "x" + }) + o(m("a.x", { + class: undefined + }).a).deepEquals({ + class: "x" + }) + o(m("a.x", { + class: false + }).a).deepEquals({ + class: "x false" + }) + o(m("a.x", { + class: true + }).a).deepEquals({ + class: "x true" + }) + o(m("a", { + className: null + }).a).deepEquals({ + className: null + }) + o(m("a", { + className: undefined + }).a).deepEquals({ + className: undefined + }) + o(m("a", { + className: false + }).a).deepEquals({ + className: null, + class: false + }) + o(m("a", { + className: true + }).a).deepEquals({ + className: null, + class: true + }) + o(m("a.x", { + className: null + }).a).deepEquals({ + className: null, + class: "x" + }) + o(m("a.x", { + className: undefined + }).a).deepEquals({ + className: null, + class: "x" + }) + o(m("a.x", { + className: false + }).a).deepEquals({ + className: null, + class: "x false" + }) + o(m("a.x", { + className: true + }).a).deepEquals({ + className: null, + class: "x true" + }) + }) + o("handles class in selector", function() { + var vnode = m(".a") + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals(".a") + o(vnode.a.class).equals("a") + }) + o("handles many classes in selector", function() { + var vnode = m(".a.b.c") + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals(".a.b.c") + o(vnode.a.class).equals("a b c") + }) + o("handles id in selector", function() { + var vnode = m("#a") + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("#a") + o(vnode.a.id).equals("a") + }) + o("handles attr in selector", function() { + var vnode = m("[a=b]") + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("[a=b]") + o(vnode.a.a).equals("b") + }) + o("handles many attrs in selector", function() { + var vnode = m("[a=b][c=d]") + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("[a=b][c=d]") + o(vnode.a.a).equals("b") + o(vnode.a.c).equals("d") + }) + o("handles attr w/ spaces in selector", function() { + var vnode = m("[a = b]") + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("[a = b]") + o(vnode.a.a).equals("b") + }) + o("handles attr w/ quotes in selector", function() { + var vnode = m("[a='b']") + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("[a='b']") + o(vnode.a.a).equals("b") + }) + o("handles attr w/ quoted square bracket", function() { + var vnode = m("[x][a='[b]'].c") + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("[x][a='[b]'].c") + o(vnode.a.x).equals(true) + o(vnode.a.a).equals("[b]") + o(vnode.a.class).equals("c") + }) + o("handles attr w/ unmatched square bracket", function() { + var vnode = m("[a=']'].c") + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("[a=']'].c") + o(vnode.a.a).equals("]") + o(vnode.a.class).equals("c") + }) + o("handles attr w/ quoted square bracket and quote", function() { + var vnode = m("[a='[b\"\\']'].c") // `[a='[b"\']']` + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("[a='[b\"\\']'].c") + o(vnode.a.a).equals("[b\"']") // `[b"']` + o(vnode.a.class).equals("c") + }) + o("handles attr w/ quoted square containing escaped square bracket", function() { + var vnode = m("[a='[\\]]'].c") // `[a='[\]]']` + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("[a='[\\]]'].c") + o(vnode.a.a).equals("[\\]]") // `[\]]` + o(vnode.a.class).equals("c") + }) + o("handles attr w/ backslashes", function() { + var vnode = m("[a='\\\\'].c") // `[a='\\']` + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("[a='\\\\'].c") + o(vnode.a.a).equals("\\") + o(vnode.a.class).equals("c") + }) + o("handles attr w/ quotes and spaces in selector", function() { + var vnode = m("[a = 'b']") + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("[a = 'b']") + o(vnode.a.a).equals("b") + }) + o("handles many attr w/ quotes and spaces in selector", function() { + var vnode = m("[a = 'b'][c = 'd']") + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("[a = 'b'][c = 'd']") + o(vnode.a.a).equals("b") + o(vnode.a.c).equals("d") + }) + o("handles tag, class, attrs in selector", function() { + var vnode = m("a.b[c = 'd']") + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("a.b[c = 'd']") + o(vnode.a.class).equals("b") + o(vnode.a.c).equals("d") + }) + o("handles tag, mixed classes, attrs in selector", function() { + var vnode = m("a.b[c = 'd'].e[f = 'g']") + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("a.b[c = 'd'].e[f = 'g']") + o(vnode.a.class).equals("b e") + o(vnode.a.c).equals("d") + o(vnode.a.f).equals("g") + }) + o("handles attr without value", function() { + var vnode = m("[a]") + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("[a]") + o(vnode.a.a).equals(true) + }) + o("handles explicit empty string value for input", function() { + var vnode = m('input[value=""]') + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals('input[value=""]') + o(vnode.a.value).equals("") + }) + o("handles explicit empty string value for option", function() { + var vnode = m('option[value=""]') + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals('option[value=""]') + o(vnode.a.value).equals("") + }) + }) + o.spec("attrs", function() { + o("handles string attr", function() { + var vnode = m("div", {a: "b"}) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("div") + o(vnode.a.a).equals("b") + }) + o("handles falsy string attr", function() { + var vnode = m("div", {a: ""}) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("div") + o(vnode.a.a).equals("") + }) + o("handles number attr", function() { + var vnode = m("div", {a: 1}) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("div") + o(vnode.a.a).equals(1) + }) + o("handles falsy number attr", function() { + var vnode = m("div", {a: 0}) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("div") + o(vnode.a.a).equals(0) + }) + o("handles boolean attr", function() { + var vnode = m("div", {a: true}) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("div") + o(vnode.a.a).equals(true) + }) + o("handles falsy boolean attr", function() { + var vnode = m("div", {a: false}) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("div") + o(vnode.a.a).equals(false) + }) + o("handles only key in attrs", function() { + var vnode = m("div", {key: "a"}) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("div") + o(vnode.a).deepEquals({key: "a"}) + }) + o("handles many attrs", function() { + var vnode = m("div", {a: "b", c: "d"}) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("div") + o(vnode.a.a).equals("b") + o(vnode.a.c).equals("d") + }) + o("handles className attrs property", function() { + var vnode = m("div", {className: "a"}) + + o(vnode.a.class).equals("a") + }) + o("handles 'class' as a verbose attribute declaration", function() { + var vnode = m("[class=a]") + + o(vnode.a.class).equals("a") + }) + o("handles merging classes w/ class property", function() { + var vnode = m(".a", {class: "b"}) + + o(vnode.a.class).equals("a b") + }) + o("handles merging classes w/ className property", function() { + var vnode = m(".a", {className: "b"}) + + o(vnode.a.class).equals("a b") + }) + }) + o.spec("custom element attrs", function() { + o("handles string attr", function() { + var vnode = m("custom-element", {a: "b"}) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("custom-element") + o(vnode.a.a).equals("b") + }) + o("handles falsy string attr", function() { + var vnode = m("custom-element", {a: ""}) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("custom-element") + o(vnode.a.a).equals("") + }) + o("handles number attr", function() { + var vnode = m("custom-element", {a: 1}) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("custom-element") + o(vnode.a.a).equals(1) + }) + o("handles falsy number attr", function() { + var vnode = m("custom-element", {a: 0}) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("custom-element") + o(vnode.a.a).equals(0) + }) + o("handles boolean attr", function() { + var vnode = m("custom-element", {a: true}) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("custom-element") + o(vnode.a.a).equals(true) + }) + o("handles falsy boolean attr", function() { + var vnode = m("custom-element", {a: false}) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("custom-element") + o(vnode.a.a).equals(false) + }) + o("handles only key in attrs", function() { + var vnode = m("custom-element", {key:"a"}) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("custom-element") + o(vnode.a).deepEquals({key:"a"}) + }) + o("handles many attrs", function() { + var vnode = m("custom-element", {a: "b", c: "d"}) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("custom-element") + o(vnode.a.a).equals("b") + o(vnode.a.c).equals("d") + }) + o("handles className attrs property", function() { + var vnode = m("custom-element", {className: "a"}) + + o(vnode.a.class).equals("a") + }) + o("casts className using toString like browsers", function() { + const className = { + valueOf: () => ".valueOf", + toString: () => "toString" + } + var vnode = m("custom-element" + className, {className: className}) + + o(vnode.a.class).equals("valueOf toString") + }) + }) + o.spec("children", function() { + o("handles string single child", function() { + var vnode = m("div", {}, ["a"]) + + o(vnode.c[0].a).equals("a") + }) + o("handles falsy string single child", function() { + var vnode = m("div", {}, [""]) + + o(vnode.c[0].a).equals("") + }) + o("handles number single child", function() { + var vnode = m("div", {}, [1]) + + o(vnode.c[0].a).equals("1") + }) + o("handles falsy number single child", function() { + var vnode = m("div", {}, [0]) + + o(vnode.c[0].a).equals("0") + }) + o("handles boolean single child", function() { + var vnode = m("div", {}, [true]) + + o(vnode.c).deepEquals([null]) + }) + o("handles falsy boolean single child", function() { + var vnode = m("div", {}, [false]) + + o(vnode.c).deepEquals([null]) + }) + o("handles null single child", function() { + var vnode = m("div", {}, [null]) + + o(vnode.c).deepEquals([null]) + }) + o("handles undefined single child", function() { + var vnode = m("div", {}, [undefined]) + + o(vnode.c).deepEquals([null]) + }) + o("handles multiple string children", function() { + var vnode = m("div", {}, ["", "a"]) + + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].a).equals("") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[1].a).equals("a") + }) + o("handles multiple number children", function() { + var vnode = m("div", {}, [0, 1]) + + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].a).equals("0") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[1].a).equals("1") + }) + o("handles multiple boolean children", function() { + var vnode = m("div", {}, [false, true]) + + o(vnode.c).deepEquals([null, null]) + }) + o("handles multiple null/undefined child", function() { + var vnode = m("div", {}, [null, undefined]) + + o(vnode.c).deepEquals([null, null]) + }) + o("handles falsy number single child without attrs", function() { + var vnode = m("div", 0) + + o(vnode.c[0].a).equals("0") + }) + o("handles children in attributes", function() { + var vnode = m("div", {children: ["", "a"]}) + + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].a).equals("") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[1].a).equals("a") + }) + }) + o.spec("permutations", function() { + o("handles null attr and children", function() { + var vnode = m("div", null, [m("a"), m("b")]) + + o(vnode.c.length).equals(2) + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[0].t).equals("a") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[1].t).equals("b") + }) + o("handles null attr and child unwrapped", function() { + var vnode = m("div", null, m("a")) + + o(vnode.c.length).equals(1) + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[0].t).equals("a") + }) + o("handles null attr and children unwrapped", function() { + var vnode = m("div", null, m("a"), m("b")) + + o(vnode.c.length).equals(2) + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[0].t).equals("a") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[1].t).equals("b") + }) + o("handles attr and children", function() { + var vnode = m("div", {a: "b"}, [m("i"), m("s")]) + + o(vnode.a.a).equals("b") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[0].t).equals("i") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[1].t).equals("s") + }) + o("handles attr and child unwrapped", function() { + var vnode = m("div", {a: "b"}, m("i")) + + o(vnode.a.a).equals("b") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[0].t).equals("i") + }) + o("handles attr and children unwrapped", function() { + var vnode = m("div", {a: "b"}, m("i"), m("s")) + + o(vnode.a.a).equals("b") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[0].t).equals("i") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[1].t).equals("s") + }) + o("handles attr and text children", function() { + var vnode = m("div", {a: "b"}, ["c", "d"]) + + o(vnode.a.a).equals("b") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].a).equals("c") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[1].a).equals("d") + }) + o("handles attr and single string text child", function() { + var vnode = m("div", {a: "b"}, ["c"]) + + o(vnode.a.a).equals("b") + o(vnode.c[0].a).equals("c") + }) + o("handles attr and single falsy string text child", function() { + var vnode = m("div", {a: "b"}, [""]) + + o(vnode.a.a).equals("b") + o(vnode.c[0].a).equals("") + }) + o("handles attr and single number text child", function() { + var vnode = m("div", {a: "b"}, [1]) + + o(vnode.a.a).equals("b") + o(vnode.c[0].a).equals("1") + }) + o("handles attr and single falsy number text child", function() { + var vnode = m("div", {a: "b"}, [0]) + + o(vnode.a.a).equals("b") + o(vnode.c[0].a).equals("0") + }) + o("handles attr and single boolean text child", function() { + var vnode = m("div", {a: "b"}, [true]) + + o(vnode.a.a).equals("b") + o(vnode.c).deepEquals([null]) + }) + o("handles attr and single falsy boolean text child", function() { + var vnode = m("div", {a: "b"}, [0]) + + o(vnode.a.a).equals("b") + o(vnode.c[0].a).equals("0") + }) + o("handles attr and single false boolean text child", function() { + var vnode = m("div", {a: "b"}, [false]) + + o(vnode.a.a).equals("b") + o(vnode.c).deepEquals([null]) + }) + o("handles attr and single text child unwrapped", function() { + var vnode = m("div", {a: "b"}, "c") + + o(vnode.a.a).equals("b") + o(vnode.c[0].a).equals("c") + }) + o("handles attr and text children unwrapped", function() { + var vnode = m("div", {a: "b"}, "c", "d") + + o(vnode.a.a).equals("b") + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].a).equals("c") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[1].a).equals("d") + }) + o("handles children without attr", function() { + var vnode = m("div", [m("i"), m("s")]) + + o(vnode.a).deepEquals({}) + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[0].t).equals("i") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[1].t).equals("s") + }) + o("handles child without attr unwrapped", function() { + var vnode = m("div", m("i")) + + o(vnode.a).deepEquals({}) + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[0].t).equals("i") + }) + o("handles children without attr unwrapped", function() { + var vnode = m("div", m("i"), m("s")) + + o(vnode.a).deepEquals({}) + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[0].t).equals("i") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[1].t).equals("s") + }) + o("handles shared attrs", function() { + var attrs = {a: "b"} + + var nodeA = m(".a", attrs) + var nodeB = m(".b", attrs) + + o(nodeA.a.class).equals("a") + o(nodeA.a.a).equals("b") + + o(nodeB.a.class).equals("b") + o(nodeB.a.a).equals("b") + }) + o("handles shared empty attrs (#2821)", function() { + var attrs = {} + + var nodeA = m(".a", attrs) + var nodeB = m(".b", attrs) + + o(nodeA.a.class).equals("a") + o(nodeB.a.class).equals("b") + }) + o("doesnt modify passed attributes object", function() { + var attrs = {a: "b"} + m(".a", attrs) + o(attrs).deepEquals({a: "b"}) + }) + o("non-nullish attr takes precedence over selector", function() { + o(m("[a=b]", {a: "c"}).a).deepEquals({a: "c"}) + }) + o("null attr takes precedence over selector", function() { + o(m("[a=b]", {a: null}).a).deepEquals({a: null}) + }) + o("undefined attr takes precedence over selector", function() { + o(m("[a=b]", {a: undefined}).a).deepEquals({a: undefined}) + }) + o("handles fragment children without attr unwrapped", function() { + var vnode = m("div", [m("i")], [m("s")]) + + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_FRAGMENT) + o(vnode.c[0].c[0].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[0].c[0].t).equals("i") + o(vnode.c[1].m & m.TYPE_MASK).equals(m.TYPE_FRAGMENT) + o(vnode.c[1].c[0].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[1].c[0].t).equals("s") + }) + o("handles children with nested array", function() { + var vnode = m("div", [[m("i"), m("s")]]) + + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_FRAGMENT) + o(vnode.c[0].c[0].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[0].c[0].t).equals("i") + o(vnode.c[0].c[1].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[0].c[1].t).equals("s") + }) + o("handles children with deeply nested array", function() { + var vnode = m("div", [[[m("i"), m("s")]]]) + + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_FRAGMENT) + o(vnode.c[0].c[0].m & m.TYPE_MASK).equals(m.TYPE_FRAGMENT) + o(vnode.c[0].c[0].c[0].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[0].c[0].c[0].t).equals("i") + o(vnode.c[0].c[0].c[1].m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.c[0].c[0].c[1].t).equals("s") + }) + }) + o.spec("components", 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.m & m.TYPE_MASK).equals(m.TYPE_COMPONENT) + o(vnode.t).equals(component) + o(vnode.a.id).equals("a") + o(vnode.a.children.length).equals(1) + o(vnode.a.children[0]).equals("b") + }) + o("works with closures", function () { + var component = o.spy() + + var vnode = m(component, {id: "a"}, "b") + + o(component.callCount).equals(0) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_COMPONENT) + o(vnode.t).equals(component) + o(vnode.a.id).equals("a") + o(vnode.a.children.length).equals(1) + o(vnode.a.children[0]).equals("b") + }) + }) + + o.spec("capture", () => { + var G = setupGlobals() + + o("works", () => { + var e = G.window.document.createEvent("MouseEvents") + e.initEvent("click", true, true) + + // Only doing this for the sake of initializing the required fields in the mock. + G.root.dispatchEvent(e) + + o(m.capture(e)).equals("skip-redraw") + o(e.defaultPrevented).equals(true) + o(e.cancelBubble).equals(true) + }) + }) +}) diff --git a/render/tests/test-input.js b/tests/core/input.js similarity index 57% rename from render/tests/test-input.js rename to tests/core/input.js index 214b32783..d78e128fe 100644 --- a/render/tests/test-input.js +++ b/tests/core/input.js @@ -1,91 +1,84 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var domMock = require("../../test-utils/domMock") -var vdom = require("../../render/render") -var m = require("../../render/hyperscript") +import {setupGlobals} from "../../test-utils/global.js" + +import m from "../../src/entry/mithril.esm.js" o.spec("form inputs", function() { - var $window, root, render - o.beforeEach(function() { - $window = domMock() - render = vdom($window) - root = $window.document.createElement("div") - $window.document.body.appendChild(root) - }) - o.afterEach(function() { - while (root.firstChild) root.removeChild(root.firstChild) - root.vnodes = null - }) + var G = setupGlobals() o.spec("input", function() { o("maintains focus after move", function() { - var input = m("input", {key: 1}) - var a = m("a", {key: 2}) - var b = m("b", {key: 3}) + var input - render(root, [input, a, b]) - input.dom.focus() - render(root, [a, input, b]) + m.render(G.root, m.keyed([[1, input = m("input")], [2, m("a")], [3, m("b")]])) + input.d.focus() + m.render(G.root, m.keyed([[2, m("a")], [1, input = m("input")], [3, m("b")]])) - o($window.document.activeElement).equals(input.dom) + o(G.window.document.activeElement).equals(input.d) }) o("maintains focus when changed manually in hook", function() { - var input = m("input", {oncreate: function() { - input.dom.focus(); - }}); + var input = m("input", m.layout((dom) => { + dom.focus() + })); - render(root, input) + m.render(G.root, input) - o($window.document.activeElement).equals(input.dom) + o(G.window.document.activeElement).equals(input.d) }) o("syncs input value if DOM value differs from vdom value", function() { var input = m("input", {value: "aaa", oninput: function() {}}) var updated = m("input", {value: "aaa", oninput: function() {}}) + var redraw = o.spy() - render(root, input) + m.render(G.root, input, {redraw}) //simulate user typing - var e = $window.document.createEvent("KeyboardEvent") + var e = G.window.document.createEvent("KeyboardEvent") e.initEvent("input", true, true) - input.dom.focus() - input.dom.value += "a" - input.dom.dispatchEvent(e) + input.d.focus() + input.d.value += "a" + input.d.dispatchEvent(e) + o(redraw.callCount).equals(1) //re-render may use same vdom value as previous render call - render(root, updated) + m.render(G.root, updated, {redraw}) - o(updated.dom.value).equals("aaa") + o(updated.d.value).equals("aaa") + o(redraw.callCount).equals(1) }) o("clear element value if vdom value is set to undefined (aka removed)", function() { var input = m("input", {value: "aaa", oninput: function() {}}) var updated = m("input", {value: undefined, oninput: function() {}}) - render(root, input) - render(root, updated) + m.render(G.root, input) + m.render(G.root, updated) - o(updated.dom.value).equals("") + o(updated.d.value).equals("") }) o("syncs input checked attribute if DOM value differs from vdom value", function() { var input = m("input", {type: "checkbox", checked: true, onclick: function() {}}) var updated = m("input", {type: "checkbox", checked: true, onclick: function() {}}) + var redraw = o.spy() - render(root, input) + m.render(G.root, input, {redraw}) //simulate user clicking checkbox - var e = $window.document.createEvent("MouseEvents") + var e = G.window.document.createEvent("MouseEvents") e.initEvent("click", true, true) - input.dom.focus() - input.dom.dispatchEvent(e) + input.d.focus() + input.d.dispatchEvent(e) + o(redraw.callCount).equals(1) //re-render may use same vdom value as previous render call - render(root, updated) + m.render(G.root, updated, {redraw}) - o(updated.dom.checked).equals(true) + o(updated.d.checked).equals(true) + o(redraw.callCount).equals(1) }) o("syncs file input value attribute if DOM value differs from vdom value and is empty", function() { @@ -94,18 +87,18 @@ o.spec("form inputs", function() { var spy = o.spy() var error = console.error - render(root, input) + m.render(G.root, input) - input.dom.value = "test.png" + input.d.value = "test.png" try { console.error = spy - render(root, updated) + m.render(G.root, updated) } finally { console.error = error } - o(updated.dom.value).equals("") + o(updated.d.value).equals("") o(spy.callCount).equals(0) }) @@ -115,60 +108,58 @@ o.spec("form inputs", function() { var spy = o.spy() var error = console.error - render(root, input) + m.render(G.root, input) - input.dom.value = "test.png" + input.d.value = "test.png" try { console.error = spy - render(root, updated) + m.render(G.root, updated) } finally { console.error = error } - o(updated.dom.value).equals("test.png") + o(updated.d.value).equals("test.png") o(spy.callCount).equals(1) }) o("retains file input value attribute if DOM value is the same as vdom value and is non-empty", function() { - var $window = domMock(o) - var render = vdom($window) - var root = $window.document.createElement("div") - $window.document.body.appendChild(root) + G.initialize({spy: o.spy}) + var input = m("input", {type: "file", value: "", onclick: function() {}}) var updated1 = m("input", {type: "file", value: "test.png", onclick: function() {}}) var updated2 = m("input", {type: "file", value: "test.png", onclick: function() {}}) var spy = o.spy() var error = console.error - render(root, input) + m.render(G.root, input) // Verify our assumptions about the outer element state - o($window.__getSpies(input.dom).valueSetter.callCount).equals(0) - input.dom.value = "test.png" - o($window.__getSpies(input.dom).valueSetter.callCount).equals(1) + o(G.window.__getSpies(input.d).valueSetter.callCount).equals(0) + input.d.value = "test.png" + o(G.window.__getSpies(input.d).valueSetter.callCount).equals(1) try { console.error = spy - render(root, updated1) + m.render(G.root, updated1) } finally { console.error = error } - o(updated1.dom.value).equals("test.png") + o(updated1.d.value).equals("test.png") o(spy.callCount).equals(0) - o($window.__getSpies(updated1.dom).valueSetter.callCount).equals(1) + o(G.window.__getSpies(updated1.d).valueSetter.callCount).equals(1) try { console.error = spy - render(root, updated2) + m.render(G.root, updated2) } finally { console.error = error } - o(updated2.dom.value).equals("test.png") + o(updated2.d.value).equals("test.png") o(spy.callCount).equals(0) - o($window.__getSpies(updated2.dom).valueSetter.callCount).equals(1) + o(G.window.__getSpies(updated2.d).valueSetter.callCount).equals(1) }) }) @@ -178,10 +169,10 @@ o.spec("form inputs", function() { m("option", {value: "a"}, "aaa") ) - render(root, select) + m.render(G.root, select) - o(select.dom.value).equals("a") - o(select.dom.selectedIndex).equals(0) + o(select.d.value).equals("a") + o(select.d.selectedIndex).equals(0) }) o("select option can have empty string value", function() { @@ -189,9 +180,9 @@ o.spec("form inputs", function() { m("option", {value: ""}, "aaa") ) - render(root, select) + m.render(G.root, select) - o(select.dom.firstChild.value).equals("") + o(select.d.firstChild.value).equals("") }) o("option value defaults to textContent unless explicitly set", function() { @@ -199,49 +190,49 @@ o.spec("form inputs", function() { m("option", "aaa") ) - render(root, select) + m.render(G.root, select) - o(select.dom.firstChild.value).equals("aaa") - o(select.dom.value).equals("aaa") + o(select.d.firstChild.value).equals("aaa") + o(select.d.value).equals("aaa") //test that value changes when content changes select = m("select", m("option", "bbb") ) - render(root, select) + m.render(G.root, select) - o(select.dom.firstChild.value).equals("bbb") - o(select.dom.value).equals("bbb") + o(select.d.firstChild.value).equals("bbb") + o(select.d.value).equals("bbb") //test that value can be set to "" in subsequent render select = m("select", m("option", {value: ""}, "aaa") ) - render(root, select) + m.render(G.root, select) - o(select.dom.firstChild.value).equals("") - o(select.dom.value).equals("") + o(select.d.firstChild.value).equals("") + o(select.d.value).equals("") //test that value reverts to textContent when value omitted select = m("select", m("option", "aaa") ) - render(root, select) + m.render(G.root, select) - o(select.dom.firstChild.value).equals("aaa") - o(select.dom.value).equals("aaa") + o(select.d.firstChild.value).equals("aaa") + o(select.d.value).equals("aaa") }) o("select yields invalid value without children", function() { var select = m("select", {value: "a"}) - render(root, select) + m.render(G.root, select) - o(select.dom.value).equals("") - o(select.dom.selectedIndex).equals(-1) + o(select.d.value).equals("") + o(select.d.selectedIndex).equals(-1) }) o("select value is set correctly on first render", function() { @@ -251,10 +242,10 @@ o.spec("form inputs", function() { m("option", {value: "c"}, "ccc") ) - render(root, select) + m.render(G.root, select) - o(select.dom.value).equals("b") - o(select.dom.selectedIndex).equals(1) + o(select.d.value).equals("b") + o(select.d.selectedIndex).equals(1) }) o("syncs select value if DOM value differs from vdom value", function() { @@ -266,17 +257,17 @@ o.spec("form inputs", function() { ) } - render(root, makeSelect()) + m.render(G.root, makeSelect()) //simulate user selecting option - root.firstChild.value = "c" - root.firstChild.focus() + G.root.firstChild.value = "c" + G.root.firstChild.focus() //re-render may use same vdom value as previous render call - render(root, makeSelect()) + m.render(G.root, makeSelect()) - o(root.firstChild.value).equals("b") - o(root.firstChild.selectedIndex).equals(1) + o(G.root.firstChild.value).equals("b") + o(G.root.firstChild.selectedIndex).equals(1) }) }) }) diff --git a/tests/core/keyed.js b/tests/core/keyed.js new file mode 100644 index 000000000..6b6bb26d5 --- /dev/null +++ b/tests/core/keyed.js @@ -0,0 +1,604 @@ +/* eslint-disable no-bitwise */ +import o from "ospec" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("keyed with view", function() { + o("works empty", function() { + var view = o.spy(() => {}) + var vnode = m.keyed([], view) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(view.callCount).equals(0) + o(vnode.a.size).equals(0) + }) + o("supports `undefined` keys", function() { + var child = m("p") + var view = o.spy(() => [undefined, child]) + var vnode = m.keyed([1], view) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(view.callCount).equals(1) + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(undefined) + o([...vnode.a][0][1]).equals(child) + }) + o("supports `null` keys", function() { + var child = m("p") + var view = o.spy(() => [null, child]) + var vnode = m.keyed([1], view) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(view.callCount).equals(1) + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(null) + o([...vnode.a][0][1]).equals(child) + }) + o("supports `false` keys", function() { + var child = m("p") + var view = o.spy(() => [false, child]) + var vnode = m.keyed([1], view) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(view.callCount).equals(1) + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(false) + o([...vnode.a][0][1]).equals(child) + }) + o("supports `true` keys", function() { + var child = m("p") + var view = o.spy(() => [true, child]) + var vnode = m.keyed([1], view) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(view.callCount).equals(1) + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(true) + o([...vnode.a][0][1]).equals(child) + }) + o("supports empty string keys", function() { + var child = m("p") + var view = o.spy(() => ["", child]) + var vnode = m.keyed([1], view) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(view.callCount).equals(1) + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals("") + o([...vnode.a][0][1]).equals(child) + }) + o("supports non-empty string keys", function() { + var child = m("p") + var view = o.spy(() => ["a", child]) + var vnode = m.keyed([1], view) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(view.callCount).equals(1) + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals("a") + o([...vnode.a][0][1]).equals(child) + }) + o("supports falsy number keys", function() { + var child = m("p") + var view = o.spy(() => [0, child]) + var vnode = m.keyed([1], view) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(view.callCount).equals(1) + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(0) + o([...vnode.a][0][1]).equals(child) + }) + o("supports truthy number keys", function() { + var child = m("p") + var view = o.spy(() => [123, child]) + var vnode = m.keyed([1], view) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(view.callCount).equals(1) + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(123) + o([...vnode.a][0][1]).equals(child) + }) + if (typeof BigInt === "function") { + // eslint-disable-next-line no-undef + const B = BigInt + o("supports falsy bigint keys", function() { + var child = m("p") + var view = o.spy(() => [B(0), child]) + var vnode = m.keyed([1], view) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(view.callCount).equals(1) + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(B(0)) + o([...vnode.a][0][1]).equals(child) + }) + o("supports truthy bigint keys", function() { + var child = m("p") + var view = o.spy(() => [B(123), child]) + var vnode = m.keyed([1], view) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(view.callCount).equals(1) + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(B(123)) + o([...vnode.a][0][1]).equals(child) + }) + } + o("supports symbol keys", function() { + var key = Symbol("test") + var child = m("p") + var view = o.spy(() => [key, child]) + var vnode = m.keyed([1], view) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(view.callCount).equals(1) + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(key) + o([...vnode.a][0][1]).equals(child) + }) + o("supports object keys", function() { + var key = {} + var child = m("p") + var view = o.spy(() => [key, child]) + var vnode = m.keyed([1], view) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(view.callCount).equals(1) + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(key) + o([...vnode.a][0][1]).equals(child) + }) + o("rejects duplicate `undefined` keys", function() { + var child = m("p") + var view = o.spy(() => [undefined, child]) + + o(() => m.keyed([1, 2], view)).throws(TypeError) + }) + o("rejects duplicate `null` keys", function() { + var child = m("p") + var view = o.spy(() => [null, child]) + + o(() => m.keyed([1, 2], view)).throws(TypeError) + }) + o("rejects duplicate `false` keys", function() { + var child = m("p") + var view = o.spy(() => [false, child]) + + o(() => m.keyed([1, 2], view)).throws(TypeError) + }) + o("rejects duplicate `true` keys", function() { + var child = m("p") + var view = o.spy(() => [true, child]) + + o(() => m.keyed([1, 2], view)).throws(TypeError) + }) + o("rejects duplicate empty string keys", function() { + var child = m("p") + var view = o.spy(() => ["", child]) + + o(() => m.keyed([1, 2], view)).throws(TypeError) + }) + o("rejects duplicate non-empty string keys", function() { + var child = m("p") + var view = o.spy(() => ["a", child]) + + o(() => m.keyed([1, 2], view)).throws(TypeError) + }) + o("rejects duplicate falsy number keys", function() { + var child = m("p") + var view = o.spy(() => [0, child]) + + o(() => m.keyed([1, 2], view)).throws(TypeError) + }) + o("rejects duplicate truthy number keys", function() { + var child = m("p") + var view = o.spy(() => [123, child]) + + o(() => m.keyed([1, 2], view)).throws(TypeError) + }) + if (typeof BigInt === "function") { + // eslint-disable-next-line no-undef + const B = BigInt + o("rejects duplicate falsy bigint keys", function() { + var child = m("p") + var view = o.spy(() => [B(0), child]) + + o(() => m.keyed([1, 2], view)).throws(TypeError) + }) + o("rejects duplicate truthy bigint keys", function() { + var child = m("p") + var view = o.spy(() => [B(123), child]) + + o(() => m.keyed([1, 2], view)).throws(TypeError) + }) + } + o("rejects duplicate symbol keys", function() { + var key = Symbol("test") + var child = m("p") + var view = o.spy(() => [key, child]) + + o(() => m.keyed([1, 2], view)).throws(TypeError) + }) + o("rejects duplicate object keys", function() { + var key = {} + var child = m("p") + var view = o.spy(() => [key, child]) + + o(() => m.keyed([1, 2], view)).throws(TypeError) + }) + o("handles `undefined` hole", function() { + var vnode = m.keyed(["foo", "bar"], (key) => (key === "foo" ? undefined : [key, "a"])) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].a).equals("a") + o([...vnode.a][0][0]).equals("bar") + }) + o("handles `null` hole", function() { + var vnode = m.keyed(["foo", "bar"], (key) => (key === "foo" ? null : [key, "a"])) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].a).equals("a") + o([...vnode.a][0][0]).equals("bar") + }) + o("handles `false` hole", function() { + var vnode = m.keyed(["foo", "bar"], (key) => (key === "foo" ? false : [key, "a"])) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].a).equals("a") + o([...vnode.a][0][0]).equals("bar") + }) + o("handles `true` hole", function() { + var vnode = m.keyed(["foo", "bar"], (key) => (key === "foo" ? true : [key, "a"])) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].a).equals("a") + o([...vnode.a][0][0]).equals("bar") + }) + o("handles `undefined` child", function() { + var vnode = m.keyed(["foo"], (key) => [key, undefined]) + + o([...vnode.a]).deepEquals([["foo", null]]) + }) + o("handles `null` child", function() { + var vnode = m.keyed(["foo"], (key) => [key, null]) + + o([...vnode.a]).deepEquals([["foo", null]]) + }) + o("handles `false child", function() { + var vnode = m.keyed(["foo"], (key) => [key, false]) + + o([...vnode.a]).deepEquals([["foo", null]]) + }) + o("handles `true` child", function() { + var vnode = m.keyed(["foo"], (key) => [key, true]) + + o([...vnode.a]).deepEquals([["foo", null]]) + }) + o("handles string child", function() { + var vnode = m.keyed(["foo"], (key) => [key, "a"]) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].a).equals("a") + }) + o("handles falsy string child", function() { + var vnode = m.keyed(["foo"], (key) => [key, ""]) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].a).equals("") + }) + o("handles number child", function() { + var vnode = m.keyed(["foo"], (key) => [key, 1]) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].a).equals("1") + }) + o("handles falsy number child", function() { + var vnode = m.keyed(["foo"], (key) => [key, 0]) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].a).equals("0") + }) + o("handles fragment", function() { + var vnode = m.keyed(["foo"], (key) => [key, ["", "a"]]) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_FRAGMENT) + o([...vnode.a][0][1].c.length).equals(2) + o([...vnode.a][0][1].c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].c[0].a).equals("") + o([...vnode.a][0][1].c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].c[1].a).equals("a") + }) +}) + +o.spec("keyed direct", function() { + o("works empty", function() { + var vnode = m.keyed([]) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(vnode.a.size).equals(0) + }) + o("supports `undefined` keys", function() { + var child = m("p") + var vnode = m.keyed([[undefined, child]]) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(undefined) + o([...vnode.a][0][1]).equals(child) + }) + o("supports `null` keys", function() { + var child = m("p") + var vnode = m.keyed([[null, child]]) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(null) + o([...vnode.a][0][1]).equals(child) + }) + o("supports `false` keys", function() { + var child = m("p") + var vnode = m.keyed([[false, child]]) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(false) + o([...vnode.a][0][1]).equals(child) + }) + o("supports `true` keys", function() { + var child = m("p") + var vnode = m.keyed([[true, child]]) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(true) + o([...vnode.a][0][1]).equals(child) + }) + o("supports empty string keys", function() { + var child = m("p") + var vnode = m.keyed([["", child]]) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals("") + o([...vnode.a][0][1]).equals(child) + }) + o("supports non-empty string keys", function() { + var child = m("p") + var vnode = m.keyed([["a", child]]) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals("a") + o([...vnode.a][0][1]).equals(child) + }) + o("supports falsy number keys", function() { + var child = m("p") + var vnode = m.keyed([[0, child]]) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(0) + o([...vnode.a][0][1]).equals(child) + }) + o("supports truthy number keys", function() { + var child = m("p") + var vnode = m.keyed([[123, child]]) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(123) + o([...vnode.a][0][1]).equals(child) + }) + if (typeof BigInt === "function") { + // eslint-disable-next-line no-undef + const B = BigInt + o("supports falsy bigint keys", function() { + var child = m("p") + var vnode = m.keyed([[B(0), child]]) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(B(0)) + o([...vnode.a][0][1]).equals(child) + }) + o("supports truthy bigint keys", function() { + var child = m("p") + var vnode = m.keyed([[B(123), child]]) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(B(123)) + o([...vnode.a][0][1]).equals(child) + }) + } + o("supports symbol keys", function() { + var key = Symbol("test") + var child = m("p") + var vnode = m.keyed([[key, child]]) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(key) + o([...vnode.a][0][1]).equals(child) + }) + o("supports object keys", function() { + var key = {} + var child = m("p") + var vnode = m.keyed([[key, child]]) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(key) + o([...vnode.a][0][1]).equals(child) + }) + o("rejects duplicate `undefined` keys", function() { + o(() => m.keyed([[undefined, m("p")], [undefined, m("p")]])).throws(TypeError) + }) + o("rejects duplicate `null` keys", function() { + o(() => m.keyed([[null, m("p")], [null, m("p")]])).throws(TypeError) + }) + o("rejects duplicate `false` keys", function() { + o(() => m.keyed([[false, m("p")], [false, m("p")]])).throws(TypeError) + }) + o("rejects duplicate `true` keys", function() { + o(() => m.keyed([[true, m("p")], [true, m("p")]])).throws(TypeError) + }) + o("rejects duplicate empty string keys", function() { + o(() => m.keyed([["", m("p")], ["", m("p")]])).throws(TypeError) + }) + o("rejects duplicate non-empty string keys", function() { + o(() => m.keyed([["a", m("p")], ["a", m("p")]])).throws(TypeError) + }) + o("rejects duplicate falsy number keys", function() { + o(() => m.keyed([[0, m("p")], [0, m("p")]])).throws(TypeError) + }) + o("rejects duplicate truthy number keys", function() { + o(() => m.keyed([[123, m("p")], [123, m("p")]])).throws(TypeError) + }) + if (typeof BigInt === "function") { + // eslint-disable-next-line no-undef + const B = BigInt + o("rejects duplicate falsy bigint keys", function() { + o(() => m.keyed([[B(0), m("p")], [B(0), m("p")]])).throws(TypeError) + }) + o("rejects duplicate truthy bigint keys", function() { + o(() => m.keyed([[B(123), m("p")], [B(123), m("p")]])).throws(TypeError) + }) + } + o("rejects duplicate symbol keys", function() { + var key = Symbol("test") + o(() => m.keyed([[key, m("p")], [key, m("p")]])).throws(TypeError) + }) + o("rejects duplicate object keys", function() { + var key = {} + o(() => m.keyed([[key, m("p")], [key, m("p")]])).throws(TypeError) + }) + o("handles `undefined` hole", function() { + var vnode = m.keyed([undefined, ["bar", "a"]]) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].a).equals("a") + o([...vnode.a][0][0]).equals("bar") + }) + o("handles `null` hole", function() { + var vnode = m.keyed([null, ["bar", "a"]]) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].a).equals("a") + o([...vnode.a][0][0]).equals("bar") + }) + o("handles `false` hole", function() { + var vnode = m.keyed([false, ["bar", "a"]]) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].a).equals("a") + o([...vnode.a][0][0]).equals("bar") + }) + o("handles `true` hole", function() { + var vnode = m.keyed([true, ["bar", "a"]]) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].a).equals("a") + o([...vnode.a][0][0]).equals("bar") + }) + o("handles `undefined` child", function() { + var vnode = m.keyed([["foo", undefined]]) + + o([...vnode.a]).deepEquals([["foo", null]]) + }) + o("handles `null` child", function() { + var vnode = m.keyed([["foo", null]]) + + o([...vnode.a]).deepEquals([["foo", null]]) + }) + o("handles `false child", function() { + var vnode = m.keyed([["foo", false]]) + + o([...vnode.a]).deepEquals([["foo", null]]) + }) + o("handles `true` child", function() { + var vnode = m.keyed([["foo", true]]) + + o([...vnode.a]).deepEquals([["foo", null]]) + }) + o("handles string child", function() { + var vnode = m.keyed([["foo", "a"]]) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].a).equals("a") + }) + o("handles falsy string child", function() { + var vnode = m.keyed([["foo", ""]]) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].a).equals("") + }) + o("handles number child", function() { + var vnode = m.keyed([["foo", 1]]) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].a).equals("1") + }) + o("handles falsy number child", function() { + var vnode = m.keyed([["foo", 0]]) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].a).equals("0") + }) + o("handles fragment", function() { + var vnode = m.keyed([["foo", ["", "a"]]]) + + o(vnode.a.size).equals(1) + o([...vnode.a][0][1].m & m.TYPE_MASK).equals(m.TYPE_FRAGMENT) + o([...vnode.a][0][1].c.length).equals(2) + o([...vnode.a][0][1].c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].c[0].a).equals("") + o([...vnode.a][0][1].c[1].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o([...vnode.a][0][1].c[1].a).equals("a") + }) +}) diff --git a/tests/core/mountRedraw.js b/tests/core/mountRedraw.js new file mode 100644 index 000000000..9cebce596 --- /dev/null +++ b/tests/core/mountRedraw.js @@ -0,0 +1,555 @@ +import o from "ospec" + +import {setupGlobals} from "../../test-utils/global.js" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("mount/redraw", function() { + var G = setupGlobals({ + initialize() { console.error = o.spy() }, + }) + + o("schedules correctly", function() { + var spy = o.spy() + + var redraw = m.mount(G.root, spy) + o(spy.callCount).equals(1) + redraw() + o(spy.callCount).equals(1) + G.rafMock.fire() + o(spy.callCount).equals(2) + + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) + o(G.rafMock.queueLength()).equals(0) + }) + + o("should run a single renderer entry", function() { + var spy = o.spy() + + var redraw = m.mount(G.root, spy) + + o(spy.callCount).equals(1) + + redraw() + redraw() + redraw() + + o(spy.callCount).equals(1) + G.rafMock.fire() + o(spy.callCount).equals(2) + + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) + o(G.rafMock.queueLength()).equals(0) + }) + + o("`redraw()` schedules independent handles independently", function() { + var $document = G.window.document + + 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() + + var redraw1 = m.mount(el1, spy1) + var redraw2 = m.mount(el2, spy2) + var redraw3 = m.mount(el3, spy3) + + redraw1() + redraw2() + redraw3() + + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(1) + o(spy3.callCount).equals(1) + + redraw1() + redraw2() + redraw3() + + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(1) + o(spy3.callCount).equals(1) + + G.rafMock.fire() + + o(spy1.callCount).equals(2) + o(spy2.callCount).equals(2) + o(spy3.callCount).equals(2) + + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) + o(G.rafMock.queueLength()).equals(0) + }) + + o("should not redraw when mounting another root", function() { + var $document = G.window.document + + 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() + + m.mount(el1, spy1) + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(0) + o(spy3.callCount).equals(0) + + m.mount(el2, spy2) + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(1) + o(spy3.callCount).equals(0) + + m.mount(el3, spy3) + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(1) + o(spy3.callCount).equals(1) + + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) + o(G.rafMock.queueLength()).equals(0) + }) + + o("should invoke remove callback on unmount", function() { + var onRemove = o.spy() + var spy = o.spy(() => m.remove(onRemove)) + + m.mount(G.root, spy) + o(spy.callCount).equals(1) + m.render(G.root, null) + + o(spy.callCount).equals(1) + o(onRemove.callCount).equals(1) + + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) + o(G.rafMock.queueLength()).equals(0) + }) + + o("should stop running after unsubscribe, even if it occurs after redraw is requested", function() { + var spy = o.spy() + + var redraw = m.mount(G.root, spy) + o(spy.callCount).equals(1) + redraw() + m.render(G.root, null) + + o(spy.callCount).equals(1) + G.rafMock.fire() + o(spy.callCount).equals(1) + + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) + o(G.rafMock.queueLength()).equals(0) + }) + + o("`redraw.sync()` redraws independent roots synchronously", function() { + var $document = G.window.document + + 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() + + var redraw1 = m.mount(el1, spy1) + var redraw2 = m.mount(el2, spy2) + var redraw3 = m.mount(el3, spy3) + + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(1) + o(spy3.callCount).equals(1) + + redraw1.sync() + redraw2.sync() + redraw3.sync() + + o(spy1.callCount).equals(2) + o(spy2.callCount).equals(2) + o(spy3.callCount).equals(2) + + redraw1.sync() + redraw2.sync() + redraw3.sync() + + o(spy1.callCount).equals(3) + o(spy2.callCount).equals(3) + o(spy3.callCount).equals(3) + + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) + o(G.rafMock.queueLength()).equals(0) + }) + + o("throws on invalid view", function() { + o(function() { m.mount(G.root, {}) }).throws(TypeError) + + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) + o(G.rafMock.queueLength()).equals(0) + }) + + o("keeps its place when synchronously unsubscribing previously visited roots", function() { + var $document = G.window.document + + var calls = [] + var root1 = G.register($document.createElement("div")) + var root2 = G.register($document.createElement("div")) + var root3 = G.register($document.createElement("div")) + + var redraw1 = m.mount(root1, () => { calls.push("root1") }) + var redraw2 = m.mount(root2, (isInit) => { + if (!isInit) m.render(root1, null) + calls.push("root2") + }) + var redraw3 = m.mount(root3, () => { calls.push("root3") }) + o(calls).deepEquals([ + "root1", "root2", "root3", + ]) + + redraw1.sync() + redraw2.sync() + redraw3.sync() + o(calls).deepEquals([ + "root1", "root2", "root3", + "root1", "root2", "root3", + ]) + + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) + o(G.rafMock.queueLength()).equals(0) + }) + + o("keeps its place when synchronously unsubscribing previously visited roots in the face of events", function() { + var $document = G.window.document + var calls = [] + var root1 = G.register($document.createElement("div")) + var root2 = G.register($document.createElement("div")) + var root3 = G.register($document.createElement("div")) + + var redraw1 = m.mount(root1, () => { calls.push("root1") }) + var redraw2 = m.mount(root2, (isInit) => { + if (!isInit) { m.render(root1, null); throw "fail" } + calls.push("root2") + }) + var redraw3 = m.mount(root3, () => { calls.push("root3") }) + o(calls).deepEquals([ + "root1", "root2", "root3", + ]) + + redraw1.sync() + redraw2.sync() + redraw3.sync() + o(calls).deepEquals([ + "root1", "root2", "root3", + "root1", "root3", + ]) + + o(console.error.calls.map((c) => c.args[0])).deepEquals(["fail"]) + o(G.rafMock.queueLength()).equals(0) + }) + + o("keeps its place when synchronously unsubscribing the current root", function() { + var $document = G.window.document + + var calls = [] + var root1 = G.register($document.createElement("div")) + var root2 = G.register($document.createElement("div")) + var root3 = G.register($document.createElement("div")) + + var redraw1 = m.mount(root1, () => { calls.push("root1") }) + var redraw2 = m.mount(root2, (isInit) => { + if (!isInit) m.render(root2, null) + calls.push("root2") + }) + var redraw3 = m.mount(root3, () => { calls.push("root3") }) + o(calls).deepEquals([ + "root1", "root2", "root3", + ]) + + redraw1.sync() + redraw2.sync() + redraw3.sync() + o(calls).deepEquals([ + "root1", "root2", "root3", + "root1", "root3", + ]) + + o(console.error.callCount).equals(1) + o(console.error.args[0] instanceof TypeError).equals(true) + o(G.rafMock.queueLength()).equals(0) + }) + + o("keeps its place when synchronously unsubscribing the current root in the face of an error", function() { + var $document = G.window.document + var calls = [] + var root1 = G.register($document.createElement("div")) + var root2 = G.register($document.createElement("div")) + var root3 = G.register($document.createElement("div")) + + var redraw1 = m.mount(root1, () => { calls.push("root1") }) + var redraw2 = m.mount(root2, (isInit) => { + if (!isInit) m.render(root2, null) + calls.push("root2") + }) + var redraw3 = m.mount(root3, () => { calls.push("root3") }) + o(calls).deepEquals([ + "root1", "root2", "root3", + ]) + + redraw1.sync() + redraw2.sync() + redraw3.sync() + o(calls).deepEquals([ + "root1", "root2", "root3", + "root1", "root3", + ]) + + o(console.error.callCount).equals(1) + o(console.error.args[0] instanceof TypeError).equals(true) + o(G.rafMock.queueLength()).equals(0) + }) + + o("throws on invalid `root` DOM node", function() { + o(function() { + m.mount(null, () => {}) + }).throws(TypeError) + + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) + o(G.rafMock.queueLength()).equals(0) + }) + + o("renders into `root` synchronously", function() { + m.mount(G.root, () => m("div")) + + o(G.root.firstChild.nodeName).equals("DIV") + + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) + o(G.rafMock.queueLength()).equals(0) + }) + + o("mounting null unmounts", function() { + m.mount(G.root, () => m("div")) + + m.render(G.root, null) + + o(G.root.childNodes.length).equals(0) + + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) + o(G.rafMock.queueLength()).equals(0) + }) + + o("Mounting a second root doesn't cause the first one to redraw", function() { + var $document = G.window.document + + var root1 = G.register($document.createElement("div")) + var root2 = G.register($document.createElement("div")) + var view = o.spy() + + m.mount(root1, view) + o(view.callCount).equals(1) + + m.mount(root2, () => {}) + + o(view.callCount).equals(1) + + G.rafMock.fire() + o(view.callCount).equals(1) + + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) + o(G.rafMock.queueLength()).equals(0) + }) + + o("redraws on events", function() { + var $document = G.window.document + + var layout = o.spy() + var onclick = o.spy() + var e = $document.createEvent("MouseEvents") + + e.initEvent("click", true, true) + + m.mount(G.root, () => m("div", { + onclick: onclick, + }, m.layout(layout))) + + G.root.firstChild.dispatchEvent(e) + + o(layout.callCount).equals(1) + + o(onclick.callCount).equals(1) + o(onclick.this).equals(G.root.firstChild) + o(onclick.args[0].type).equals("click") + o(onclick.args[0].target).equals(G.root.firstChild) + + G.rafMock.fire() + + o(layout.callCount).equals(2) + + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) + o(G.rafMock.queueLength()).equals(0) + }) + + o("redraws only parent mount point on events", function() { + var $document = G.window.document + + var layout0 = o.spy() + var onclick0 = o.spy() + var layout1 = o.spy() + var onclick1 = o.spy() + + var root1 = G.register($document.createElement("div")) + var root2 = G.register($document.createElement("div")) + var e = $document.createEvent("MouseEvents") + + e.initEvent("click", true, true) + + m.mount(root1, () => m("div", { + onclick: onclick0, + }, m.layout(layout0))) + + o(layout0.callCount).equals(1) + + m.mount(root2, () => m("div", { + onclick: onclick1, + }, m.layout(layout1))) + + o(layout1.callCount).equals(1) + + root1.firstChild.dispatchEvent(e) + o(onclick0.callCount).equals(1) + o(onclick0.this).equals(root1.firstChild) + + G.rafMock.fire() + + o(layout0.callCount).equals(2) + o(layout1.callCount).equals(1) + + root2.firstChild.dispatchEvent(e) + + o(onclick1.callCount).equals(1) + o(onclick1.this).equals(root2.firstChild) + + G.rafMock.fire() + + o(layout0.callCount).equals(2) + o(layout1.callCount).equals(2) + + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) + o(G.rafMock.queueLength()).equals(0) + }) + + o("event handlers can skip redraw", function() { + var $document = G.window.document + + var layout = o.spy() + var e = $document.createEvent("MouseEvents") + + e.initEvent("click", true, true) + + m.mount(G.root, () => m("div", { + onclick: () => "skip-redraw", + }, m.layout(layout))) + + G.root.firstChild.dispatchEvent(e) + + o(layout.callCount).equals(1) + + G.rafMock.fire() + + o(layout.callCount).equals(1) + + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) + o(G.rafMock.queueLength()).equals(0) + }) + + o("redraws when the render function is run", function() { + var layout = o.spy() + + var redraw = m.mount(G.root, () => m("div", m.layout(layout))) + + o(layout.callCount).equals(1) + + redraw() + + G.rafMock.fire() + + o(layout.callCount).equals(2) + + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) + o(G.rafMock.queueLength()).equals(0) + }) + + o("remounts after `m.render(root, null)` is invoked on the mounted root", function() { + var onRemove = o.spy() + var onLayout = o.spy() + + var redraw = m.mount(G.root, () => m("div", m.layout(onLayout), m.remove(onRemove))) + + o(onLayout.callCount).equals(1) + o(onRemove.callCount).equals(0) + + m.render(G.root, null) + o(onLayout.callCount).equals(1) + o(onRemove.callCount).equals(1) + + redraw() + + G.rafMock.fire() + + o(onLayout.callCount).equals(2) + o(onRemove.callCount).equals(1) + + o(console.error.calls.map((c) => c.args[0])).deepEquals([]) + o(G.rafMock.queueLength()).equals(0) + }) + + o("propagates mount errors synchronously", function() { + m.mount(G.root, () => { throw "foo" }) + o(console.error.calls.map((c) => c.args[0])).deepEquals(["foo"]) + }) + + o("propagates redraw errors synchronously", function() { + var counter = 0 + + var redraw = m.mount(G.root, () => { + switch (++counter) { + case 1: return null + case 2: throw "foo" + case 3: throw "bar" + case 4: throw "baz" + default: return null + } + }) + + redraw.sync() + redraw.sync() + redraw.sync() + + o(counter).equals(4) + o(console.error.calls.map((c) => c.args[0])).deepEquals(["foo", "bar", "baz"]) + o(G.rafMock.queueLength()).equals(0) + }) + + o("lets redraw errors fall through to the scheduler", function() { + var counter = 0 + + var redraw = m.mount(G.root, () => { + switch (++counter) { + case 1: return null + case 2: throw "foo" + case 3: throw "bar" + case 4: throw "baz" + default: return null + } + }) + + redraw() + G.rafMock.fire() + redraw() + G.rafMock.fire() + redraw() + G.rafMock.fire() + + o(counter).equals(4) + o(console.error.calls.map((c) => c.args[0])).deepEquals(["foo", "bar", "baz"]) + o(G.rafMock.queueLength()).equals(0) + }) +}) diff --git a/tests/core/normalize.js b/tests/core/normalize.js new file mode 100644 index 000000000..22795754b --- /dev/null +++ b/tests/core/normalize.js @@ -0,0 +1,83 @@ +/* eslint-disable no-bitwise */ +import o from "ospec" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("normalize", function() { + o("normalizes array into fragment", function() { + var node = m.normalize([]) + + o(node.m & m.TYPE_MASK).equals(m.TYPE_FRAGMENT) + o(node.c.length).equals(0) + }) + o("normalizes nested array into fragment", function() { + var node = m.normalize([[]]) + + o(node.m & m.TYPE_MASK).equals(m.TYPE_FRAGMENT) + o(node.c.length).equals(1) + o(node.c[0].m & m.TYPE_MASK).equals(m.TYPE_FRAGMENT) + o(node.c[0].c.length).equals(0) + }) + o("normalizes string into text node", function() { + var node = m.normalize("a") + + o(node.m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(node.a).equals("a") + }) + o("normalizes falsy string into text node", function() { + var node = m.normalize("") + + o(node.m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(node.a).equals("") + }) + o("normalizes number into text node", function() { + var node = m.normalize(1) + + o(node.m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(node.a).equals("1") + }) + o("normalizes falsy number into text node", function() { + var node = m.normalize(0) + + o(node.m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(node.a).equals("0") + }) + o("normalizes `true` to `null`", function() { + var node = m.normalize(true) + + o(node).equals(null) + }) + o("normalizes `false` to `null`", function() { + var node = m.normalize(false) + + o(node).equals(null) + }) + o("normalizes nested arrays into nested fragments", function() { + var vnode = m.normalize([[]]) + + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_FRAGMENT) + o(vnode.c[0].c.length).equals(0) + }) + o("normalizes nested strings into nested text nodes", function() { + var vnode = m.normalize(["a"]) + + o(vnode.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c[0].a).equals("a") + }) + o("normalizes nested `false` values into nested `null`s", function() { + var vnode = m.normalize([false]) + + o(vnode.c[0]).equals(null) + }) + o("retains nested element vnodes in arrays", function() { + var elem1, elem2 + var vnode = m.normalize([ + elem1 = m("foo1"), + elem2 = m("foo2"), + ]) + + o(vnode.c.length).equals(2) + o(vnode.c[0]).equals(elem1) + o(vnode.c[1]).equals(elem2) + }) +}) diff --git a/tests/core/normalizeComponentChildren.js b/tests/core/normalizeComponentChildren.js new file mode 100644 index 000000000..759a9b75a --- /dev/null +++ b/tests/core/normalizeComponentChildren.js @@ -0,0 +1,26 @@ +import o from "ospec" + +import {setupGlobals} from "../../test-utils/global.js" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("component children", function () { + var G = setupGlobals() + + o("are not normalized on ingestion", function () { + var component = (attrs) => attrs.children + var vnode = m(component, "a") + m.render(G.root, vnode) + o(vnode.a.children[0]).equals("a") + }) + + o("are normalized upon view interpolation", function () { + var component = (attrs) => attrs.children + var vnode = m(component, "a") + m.render(G.root, vnode) + o(vnode.c.c.length).equals(1) + // eslint-disable-next-line no-bitwise + o(vnode.c.c[0].m & m.TYPE_MASK).equals(m.TYPE_TEXT) + o(vnode.c.c[0].a).equals("a") + }) +}) diff --git a/tests/core/oncreate.js b/tests/core/oncreate.js new file mode 100644 index 000000000..dcaab8287 --- /dev/null +++ b/tests/core/oncreate.js @@ -0,0 +1,236 @@ +import o from "ospec" + +import {setupGlobals} from "../../test-utils/global.js" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("layout create", function() { + var G = setupGlobals() + + o.spec("m.layout", () => { + o("works when rendered directly", function() { + var layoutSpy = o.spy() + var vnode = m.layout(layoutSpy) + + m.render(G.root, vnode) + + o(layoutSpy.callCount).equals(1) + o(layoutSpy.args[0]).equals(G.root) + }) + o("works when creating element", function() { + var layoutSpy = o.spy() + var vnode = m("div", m.layout(layoutSpy)) + + m.render(G.root, vnode) + + o(layoutSpy.callCount).equals(1) + }) + o("works when creating fragment", function() { + var layoutSpy = o.spy() + var vnode = [m.layout(layoutSpy)] + + m.render(G.root, vnode) + + o(layoutSpy.callCount).equals(1) + }) + o("works when replacing same-keyed", function() { + var createDiv = o.spy() + var createA = o.spy() + var vnode = m("div", m.layout(createDiv)) + var updated = m("a", m.layout(createA)) + + m.render(G.root, m.keyed([[1, vnode]])) + m.render(G.root, m.keyed([[1, updated]])) + + o(createDiv.callCount).equals(1) + o(createA.callCount).equals(1) + }) + o("works when creating other children", function() { + var create = o.spy() + var vnode = m("div", m.layout(create), m("a")) + + m.render(G.root, vnode) + + o(create.callCount).equals(1) + o(create.args[0]).equals(G.root.firstChild) + }) + o("works inside keyed", function() { + var create = o.spy() + var vnode = m("div", m.layout(create)) + var otherVnode = m("a") + + m.render(G.root, m.keyed([[1, vnode], [2, otherVnode]])) + + o(create.callCount).equals(1) + o(create.args[0]).equals(G.root.firstChild) + }) + o("does not invoke callback when removing, but aborts the provided signal", function() { + var create = o.spy() + var vnode = m("div", m.layout(create)) + + m.render(G.root, vnode) + + o(create.callCount).equals(1) + + m.render(G.root, []) + + o(create.callCount).equals(1) + }) + o("works at the same step as layout update", function() { + var create = o.spy() + var update = o.spy() + var layoutSpy = o.spy() + var vnode = m("div", m.layout(create)) + var updated = m("div", m.layout(update), m("a", m.layout(layoutSpy))) + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(create.callCount).equals(1) + o(create.args[0]).equals(G.root.firstChild) + + o(update.callCount).equals(1) + o(update.args[0]).equals(G.root.firstChild) + + o(layoutSpy.callCount).equals(1) + o(layoutSpy.args[0]).equals(G.root.firstChild.firstChild) + }) + o("works on unkeyed that falls into reverse list diff code path", function() { + var create = o.spy() + m.render(G.root, m.keyed([[1, m("p")], [2, m("div")]])) + m.render(G.root, m.keyed([[2, m("div", m.layout(create))], [1, m("p")]])) + + o(create.callCount).equals(1) + o(create.args[0]).equals(G.root.firstChild) + }) + o("works on unkeyed that falls into forward list diff code path", function() { + var create = o.spy() + m.render(G.root, [m("div"), m("p")]) + m.render(G.root, [m("div"), m("div", m.layout(create))]) + + o(create.callCount).equals(1) + o(create.args[0]).equals(G.root.childNodes[1]) + }) + o("works after full DOM creation", function() { + var created = false + var vnode = m("div", m("a", m.layout(create), m("b"))) + + m.render(G.root, vnode) + + function create(dom) { + created = true + + o(dom.parentNode).equals(G.root.firstChild) + o(dom.childNodes.length).equals(1) + } + o(created).equals(true) + }) + }) + + o.spec("m.remove", () => { + o("works when rendered directly", function() { + var removeSpy = o.spy() + var vnode = m.remove(removeSpy) + + m.render(G.root, vnode) + + o(removeSpy.callCount).equals(0) + }) + o("works when creating element", function() { + var removeSpy = o.spy() + var vnode = m("div", m.remove(removeSpy)) + + m.render(G.root, vnode) + + o(removeSpy.callCount).equals(0) + }) + o("works when creating fragment", function() { + var removeSpy = o.spy() + var vnode = [m.remove(removeSpy)] + + m.render(G.root, vnode) + + o(removeSpy.callCount).equals(0) + }) + o("works when replacing same-keyed", function() { + var createDiv = o.spy() + var createA = o.spy() + var vnode = m("div", m.remove(createDiv)) + var updated = m("a", m.remove(createA)) + + m.render(G.root, m.keyed([[1, vnode]])) + var dom = vnode.d + m.render(G.root, m.keyed([[1, updated]])) + + o(createDiv.callCount).equals(1) + o(createDiv.args[0]).equals(dom) + o(createA.callCount).equals(0) + }) + o("works when creating other children", function() { + var create = o.spy() + var vnode = m("div", m.remove(create), m("a")) + + m.render(G.root, vnode) + + o(create.callCount).equals(0) + }) + o("works inside keyed", function() { + var create = o.spy() + var vnode = m("div", m.remove(create)) + var otherVnode = m("a") + + m.render(G.root, m.keyed([[1, vnode], [2, otherVnode]])) + + o(create.callCount).equals(0) + }) + o("does not invoke callback when removing, but aborts the provided signal", function() { + var create = o.spy() + var vnode = m("div", m.remove(create)) + + m.render(G.root, vnode) + + o(create.callCount).equals(0) + + m.render(G.root, []) + + o(create.callCount).equals(1) + }) + o("works at the same step as layout update", function() { + var create = o.spy() + var update = o.spy() + var removeSpy = o.spy() + var vnode = m("div", m.remove(create)) + var updated = m("div", m.remove(update), m("a", m.remove(removeSpy))) + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(create.callCount).equals(0) + + o(update.callCount).equals(0) + + o(removeSpy.callCount).equals(0) + }) + o("works on unkeyed that falls into reverse list diff code path", function() { + var create = o.spy() + m.render(G.root, m.keyed([[1, m("p")], [2, m("div")]])) + m.render(G.root, m.keyed([[2, m("div", m.remove(create))], [1, m("p")]])) + + o(create.callCount).equals(0) + }) + o("works on unkeyed that falls into forward list diff code path", function() { + var create = o.spy() + m.render(G.root, [m("div"), m("p")]) + m.render(G.root, [m("div"), m("div", m.remove(create))]) + + o(create.callCount).equals(0) + }) + o("works after full DOM creation", function() { + var created = false + var vnode = m("div", m("a", m.remove(() => created = true), m("b"))) + + m.render(G.root, vnode) + o(created).equals(false) + }) + }) +}) diff --git a/tests/core/onremove.js b/tests/core/onremove.js new file mode 100644 index 000000000..276a93b79 --- /dev/null +++ b/tests/core/onremove.js @@ -0,0 +1,84 @@ +import o from "ospec" + +import {setupGlobals} from "../../test-utils/global.js" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("layout remove", function() { + var G = setupGlobals() + + o("does not abort layout signal when creating", function() { + var create = o.spy() + var update = o.spy() + var vnode = m("div", m.remove(create)) + var updated = m("div", m.remove(update)) + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(create.callCount).equals(0) + }) + o("does not abort layout signal when updating", function() { + var create = o.spy() + var update = o.spy() + var vnode = m("div", m.remove(create)) + var updated = m("div", m.remove(update)) + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(create.callCount).equals(0) + o(update.callCount).equals(0) + }) + o("aborts layout signal when removing element", function() { + var remove = o.spy() + var vnode = m("div", m.remove(remove)) + + m.render(G.root, vnode) + m.render(G.root, []) + + o(remove.callCount).equals(1) + }) + o("aborts layout signal when removing fragment", function() { + var remove = o.spy() + var vnode = [m.remove(remove)] + + m.render(G.root, vnode) + m.render(G.root, []) + + o(remove.callCount).equals(1) + }) + o("aborts layout signal on keyed nodes", function() { + var remove = o.spy() + var vnode = m("div") + var temp = m("div", m.remove(remove)) + var updated = m("div") + + m.render(G.root, m.keyed([[1, vnode]])) + m.render(G.root, m.keyed([[2, temp]])) + m.render(G.root, m.keyed([[1, updated]])) + + o(vnode.d).notEquals(updated.d) // this used to be a recycling pool test + o(remove.callCount).equals(1) + }) + o("aborts layout signal on nested component", function() { + var spy = o.spy() + var comp = () => m(outer) + var outer = () => m(inner) + var inner = () => m.layout(spy) + m.render(G.root, m(comp)) + m.render(G.root, null) + + o(spy.callCount).equals(1) + }) + o("aborts layout signal on nested component child", function() { + var spy = o.spy() + var comp = () => m(outer) + var outer = () => m(inner, m("a", m.remove(spy))) + var inner = (attrs) => m("div", attrs.children) + m.render(G.root, m(comp)) + m.render(G.root, null) + + o(spy.callCount).equals(1) + }) +}) diff --git a/tests/core/onupdate.js b/tests/core/onupdate.js new file mode 100644 index 000000000..dcbb5daa8 --- /dev/null +++ b/tests/core/onupdate.js @@ -0,0 +1,110 @@ +import o from "ospec" + +import {setupGlobals} from "../../test-utils/global.js" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("layout update", function() { + var G = setupGlobals() + + o("is not invoked when removing element", function() { + var update = o.spy() + var vnode = m("div", m.layout(update)) + + m.render(G.root, vnode) + m.render(G.root, []) + + o(update.callCount).equals(1) + }) + o("is not updated when replacing keyed element", function() { + var update = o.spy() + var vnode = m.keyed([[1, m("div", m.layout(update))]]) + var updated = m.keyed([[1, m("a", m.layout(update))]]) + m.render(G.root, vnode) + m.render(G.root, updated) + + o(update.callCount).equals(2) + }) + o("does not call old callback when removing layout vnode from new vnode", function() { + var update = o.spy() + + m.render(G.root, m("a", m.layout(update))) + m.render(G.root, m("a", m.layout(update))) + m.render(G.root, m("a")) + + o(update.callCount).equals(2) + }) + o("invoked on noop", function() { + var preUpdate = o.spy() + var update = o.spy() + var vnode = m("div", m.layout(preUpdate)) + var updated = m("div", m.layout(update)) + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(preUpdate.callCount).equals(1) + o(update.callCount).equals(1) + }) + o("invoked on updating attr", function() { + var preUpdate = o.spy() + var update = o.spy() + var vnode = m("div", m.layout(preUpdate)) + var updated = m("div", {id: "a"}, m.layout(update)) + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(preUpdate.callCount).equals(1) + o(update.callCount).equals(1) + }) + o("invoked on updating children", function() { + var preUpdate = o.spy() + var update = o.spy() + var vnode = m("div", m.layout(preUpdate), m("a")) + var updated = m("div", m.layout(update), m("b")) + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(preUpdate.callCount).equals(1) + o(update.callCount).equals(1) + }) + o("invoked on updating fragment", function() { + var preUpdate = o.spy() + var update = o.spy() + var vnode = [m.layout(preUpdate)] + var updated = [m.layout(update)] + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(preUpdate.callCount).equals(1) + o(update.callCount).equals(1) + }) + o("invoked on full DOM update", function() { + var called = false + var vnode = m("div", {id: "1"}, + m("a", {id: "2"}, m.layout(() => {}), + m("b", {id: "3"}) + ) + ) + var updated = m("div", {id: "11"}, + m("a", {id: "22"}, m.layout(update), + m("b", {id: "33"}) + ) + ) + + m.render(G.root, vnode) + m.render(G.root, updated) + + function update(dom) { + called = true + + o(dom.parentNode.attributes["id"].value).equals("11") + o(dom.attributes["id"].value).equals("22") + o(dom.childNodes[0].attributes["id"].value).equals("33") + } + o(called).equals(true) + }) +}) diff --git a/tests/core/render-hyperscript-integration.js b/tests/core/render-hyperscript-integration.js new file mode 100644 index 000000000..a4922bb4c --- /dev/null +++ b/tests/core/render-hyperscript-integration.js @@ -0,0 +1,609 @@ +import o from "ospec" + +import {setupGlobals} from "../../test-utils/global.js" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("render/hyperscript integration", function() { + var G = setupGlobals() + + o.spec("setting class", function() { + o("selector only", function() { + m.render(G.root, m(".foo")) + + o(G.root.firstChild.className).equals("foo") + }) + o("class only", function() { + m.render(G.root, m("div", {class: "foo"})) + + o(G.root.firstChild.className).equals("foo") + }) + o("className only", function() { + m.render(G.root, m("div", {className: "foo"})) + + o(G.root.firstChild.className).equals("foo") + }) + o("selector and class", function() { + m.render(G.root, m(".bar", {class: "foo"})) + + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar", "foo"]) + }) + o("selector and className", function() { + m.render(G.root, m(".bar", {className: "foo"})) + + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar", "foo"]) + }) + o("selector and a null class", function() { + m.render(G.root, m(".foo", {class: null})) + + o(G.root.firstChild.className).equals("foo") + }) + o("selector and a null className", function() { + m.render(G.root, m(".foo", {className: null})) + + o(G.root.firstChild.className).equals("foo") + }) + o("selector and an undefined class", function() { + m.render(G.root, m(".foo", {class: undefined})) + + o(G.root.firstChild.className).equals("foo") + }) + o("selector and an undefined className", function() { + m.render(G.root, m(".foo", {className: undefined})) + + o(G.root.firstChild.className).equals("foo") + }) + }) + o.spec("updating class", function() { + o.spec("from selector only", function() { + o("to selector only", function() { + m.render(G.root, m(".foo1")) + m.render(G.root, m(".foo2")) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to class only", function() { + m.render(G.root, m(".foo1")) + m.render(G.root, m("div", {class: "foo2"})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to className only", function() { + m.render(G.root, m(".foo1")) + m.render(G.root, m("div", {className: "foo2"})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and class", function() { + m.render(G.root, m(".foo1")) + m.render(G.root, m(".bar2", {class: "foo2"})) + + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + }) + o("to selector and className", function() { + m.render(G.root, m(".foo1")) + m.render(G.root, m(".bar2", {className: "foo2"})) + + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + }) + o("to selector and a null class", function() { + m.render(G.root, m(".foo1")) + m.render(G.root, m(".foo2", {class: null})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and a null className", function() { + m.render(G.root, m(".foo1")) + m.render(G.root, m(".foo2", {className: null})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and an undefined class", function() { + m.render(G.root, m(".foo1")) + m.render(G.root, m(".foo2", {class: undefined})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and an undefined className", function() { + m.render(G.root, m(".foo1")) + m.render(G.root, m(".foo2", {className: undefined})) + + o(G.root.firstChild.className).equals("foo2") + }) + }) + o.spec("from class only", function() { + o("to selector only", function() { + m.render(G.root, m("div", {class: "foo2"})) + m.render(G.root, m(".foo2")) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to class only", function() { + m.render(G.root, m("div", {class: "foo2"})) + m.render(G.root, m("div", {class: "foo2"})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to className only", function() { + m.render(G.root, m("div", {class: "foo2"})) + m.render(G.root, m("div", {className: "foo2"})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and class", function() { + m.render(G.root, m("div", {class: "foo2"})) + m.render(G.root, m(".bar2", {class: "foo2"})) + + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + }) + o("to selector and className", function() { + m.render(G.root, m(".bar2", {className: "foo2"})) + + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + }) + o("to selector and a null class", function() { + m.render(G.root, m("div", {class: "foo2"})) + m.render(G.root, m(".foo2", {class: null})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and a null className", function() { + m.render(G.root, m("div", {class: "foo2"})) + m.render(G.root, m(".foo2", {className: null})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and an undefined class", function() { + m.render(G.root, m("div", {class: "foo2"})) + m.render(G.root, m(".foo2", {class: undefined})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and an undefined className", function() { + m.render(G.root, m("div", {class: "foo2"})) + m.render(G.root, m(".foo2", {className: undefined})) + + o(G.root.firstChild.className).equals("foo2") + }) + }) + o.spec("from ", function() { + o("to selector only", function() { + m.render(G.root, m(".foo2")) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to class only", function() { + m.render(G.root, m("div", {class: "foo2"})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to className only", function() { + m.render(G.root, m("div", {className: "foo2"})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and class", function() { + m.render(G.root, m(".bar2", {class: "foo2"})) + + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + }) + o("to selector and className", function() { + m.render(G.root, m(".bar2", {className: "foo2"})) + + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + }) + o("to selector and a null class", function() { + m.render(G.root, m(".foo2", {class: null})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and a null className", function() { + m.render(G.root, m(".foo2", {className: null})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and an undefined class", function() { + m.render(G.root, m(".foo2", {class: undefined})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and an undefined className", function() { + m.render(G.root, m(".foo2", {className: undefined})) + + o(G.root.firstChild.className).equals("foo2") + }) + }) + o.spec("from className only", function() { + o("to selector only", function() { + m.render(G.root, m("div", {className: "foo1"})) + m.render(G.root, m(".foo2")) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to class only", function() { + m.render(G.root, m("div", {className: "foo1"})) + m.render(G.root, m("div", {class: "foo2"})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to className only", function() { + m.render(G.root, m("div", {className: "foo1"})) + m.render(G.root, m("div", {className: "foo2"})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and class", function() { + m.render(G.root, m("div", {className: "foo1"})) + m.render(G.root, m(".bar2", {class: "foo2"})) + + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + }) + o("to selector and className", function() { + m.render(G.root, m("div", {className: "foo1"})) + m.render(G.root, m(".bar2", {className: "foo2"})) + + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + }) + o("to selector and a null class", function() { + m.render(G.root, m("div", {className: "foo1"})) + m.render(G.root, m(".foo2", {class: null})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and a null className", function() { + m.render(G.root, m("div", {className: "foo1"})) + m.render(G.root, m(".foo2", {className: null})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and an undefined class", function() { + m.render(G.root, m("div", {className: "foo1"})) + m.render(G.root, m(".foo2", {class: undefined})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and an undefined className", function() { + m.render(G.root, m("div", {className: "foo1"})) + m.render(G.root, m(".foo2", {className: undefined})) + + o(G.root.firstChild.className).equals("foo2") + }) + }) + o.spec("from selector and class", function() { + o("to selector only", function() { + m.render(G.root, m(".bar1", {class: "foo1"})) + m.render(G.root, m(".foo2")) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to class only", function() { + m.render(G.root, m(".bar1", {class: "foo1"})) + m.render(G.root, m("div", {class: "foo2"})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to className only", function() { + m.render(G.root, m(".bar1", {class: "foo1"})) + m.render(G.root, m("div", {className: "foo2"})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and class", function() { + m.render(G.root, m(".bar1", {class: "foo1"})) + m.render(G.root, m(".bar2", {class: "foo2"})) + + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + }) + o("to selector and className", function() { + m.render(G.root, m(".bar1", {class: "foo1"})) + m.render(G.root, m(".bar2", {className: "foo2"})) + + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + }) + o("to selector and a null class", function() { + m.render(G.root, m(".bar1", {class: "foo1"})) + m.render(G.root, m(".foo2", {class: null})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and a null className", function() { + m.render(G.root, m(".bar1", {class: "foo1"})) + m.render(G.root, m(".foo2", {className: null})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and an undefined class", function() { + m.render(G.root, m(".bar1", {class: "foo1"})) + m.render(G.root, m(".foo2", {class: undefined})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and an undefined className", function() { + m.render(G.root, m(".bar1", {class: "foo1"})) + m.render(G.root, m(".foo2", {className: undefined})) + + o(G.root.firstChild.className).equals("foo2") + }) + }) + o.spec("from selector and className", function() { + o("to selector only", function() { + m.render(G.root, m(".bar1", {className: "foo1"})) + m.render(G.root, m(".foo2")) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to class only", function() { + m.render(G.root, m(".bar1", {className: "foo1"})) + m.render(G.root, m("div", {class: "foo2"})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to className only", function() { + m.render(G.root, m(".bar1", {className: "foo1"})) + m.render(G.root, m("div", {className: "foo2"})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and class", function() { + m.render(G.root, m(".bar1", {className: "foo1"})) + m.render(G.root, m(".bar2", {class: "foo2"})) + + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + }) + o("to selector and className", function() { + m.render(G.root, m(".bar1", {className: "foo1"})) + m.render(G.root, m(".bar2", {className: "foo2"})) + + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + }) + o("to selector and a null class", function() { + m.render(G.root, m(".bar1", {className: "foo1"})) + m.render(G.root, m(".foo2", {class: null})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and a null className", function() { + m.render(G.root, m(".bar1", {className: "foo1"})) + m.render(G.root, m(".foo2", {className: null})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and an undefined class", function() { + m.render(G.root, m(".bar1", {className: "foo1"})) + m.render(G.root, m(".foo2", {class: undefined})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and an undefined className", function() { + m.render(G.root, m(".bar1", {className: "foo1"})) + m.render(G.root, m(".foo2", {className: undefined})) + + o(G.root.firstChild.className).equals("foo2") + }) + }) + o.spec("from and a null class", function() { + o("to selector only", function() { + m.render(G.root, m(".foo1", {class: null})) + m.render(G.root, m(".foo2")) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to class only", function() { + m.render(G.root, m(".foo1", {class: null})) + m.render(G.root, m("div", {class: "foo2"})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to className only", function() { + m.render(G.root, m(".foo1", {class: null})) + m.render(G.root, m("div", {className: "foo2"})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and class", function() { + m.render(G.root, m(".foo1", {class: null})) + m.render(G.root, m(".bar2", {class: "foo2"})) + + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + }) + o("to selector and className", function() { + m.render(G.root, m(".foo1", {class: null})) + m.render(G.root, m(".bar2", {className: "foo2"})) + + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + }) + o("to selector and a null class", function() { + m.render(G.root, m(".foo1", {class: null})) + m.render(G.root, m(".foo2", {class: null})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and a null className", function() { + m.render(G.root, m(".foo1", {class: null})) + m.render(G.root, m(".foo2", {className: null})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and an undefined class", function() { + m.render(G.root, m(".foo1", {class: null})) + m.render(G.root, m(".foo2", {class: undefined})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and an undefined className", function() { + m.render(G.root, m(".foo1", {class: null})) + m.render(G.root, m(".foo2", {className: undefined})) + + o(G.root.firstChild.className).equals("foo2") + }) + }) + o.spec("from selector and a null className", function() { + o("to selector only", function() { + m.render(G.root, m(".foo1", {className: null})) + m.render(G.root, m(".foo2")) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to class only", function() { + m.render(G.root, m(".foo1", {className: null})) + m.render(G.root, m("div", {class: "foo2"})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to className only", function() { + m.render(G.root, m(".foo1", {className: null})) + m.render(G.root, m("div", {className: "foo2"})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and class", function() { + m.render(G.root, m(".foo1", {className: null})) + m.render(G.root, m(".bar2", {class: "foo2"})) + + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + }) + o("to selector and className", function() { + m.render(G.root, m(".foo1", {className: null})) + m.render(G.root, m(".bar2", {className: "foo2"})) + + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + }) + o("to selector and a null class", function() { + m.render(G.root, m(".foo1", {className: null})) + m.render(G.root, m(".foo2", {class: null})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and a null className", function() { + m.render(G.root, m(".foo1", {className: null})) + m.render(G.root, m(".foo2", {className: null})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and an undefined class", function() { + m.render(G.root, m(".foo1", {className: null})) + m.render(G.root, m(".foo2", {class: undefined})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and an undefined className", function() { + m.render(G.root, m(".foo1", {className: null})) + m.render(G.root, m(".foo2", {className: undefined})) + + o(G.root.firstChild.className).equals("foo2") + }) + }) + o.spec("from selector and an undefined class", function() { + o("to selector only", function() { + m.render(G.root, m(".foo1", {class: undefined})) + m.render(G.root, m(".foo2")) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to class only", function() { + m.render(G.root, m(".foo1", {class: undefined})) + m.render(G.root, m("div", {class: "foo2"})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to className only", function() { + m.render(G.root, m(".foo1", {class: undefined})) + m.render(G.root, m("div", {className: "foo2"})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and class", function() { + m.render(G.root, m(".foo1", {class: undefined})) + m.render(G.root, m(".bar2", {class: "foo2"})) + + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + }) + o("to selector and className", function() { + m.render(G.root, m(".foo1", {class: undefined})) + m.render(G.root, m(".bar2", {className: "foo2"})) + + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + }) + o("to selector and a null class", function() { + m.render(G.root, m(".foo1", {class: undefined})) + m.render(G.root, m(".foo2", {class: null})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and a null className", function() { + m.render(G.root, m(".foo1", {class: undefined})) + m.render(G.root, m(".foo2", {className: null})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and an undefined class", function() { + m.render(G.root, m(".foo1", {class: undefined})) + m.render(G.root, m(".foo2", {class: undefined})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and an undefined className", function() { + m.render(G.root, m(".foo1", {class: undefined})) + m.render(G.root, m(".foo2", {className: undefined})) + + o(G.root.firstChild.className).equals("foo2") + }) + }) + o.spec("from selector and an undefined className", function() { + o("to selector only", function() { + m.render(G.root, m(".foo1", {className: undefined})) + m.render(G.root, m(".foo2")) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to class only", function() { + m.render(G.root, m(".foo1", {className: undefined})) + m.render(G.root, m("div", {class: "foo2"})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to className only", function() { + m.render(G.root, m(".foo1", {className: undefined})) + m.render(G.root, m("div", {className: "foo2"})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and class", function() { + m.render(G.root, m(".foo1", {className: undefined})) + m.render(G.root, m(".bar2", {class: "foo2"})) + + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + }) + o("to selector and className", function() { + m.render(G.root, m(".foo1", {className: undefined})) + m.render(G.root, m(".bar2", {className: "foo2"})) + + o(G.root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"]) + }) + o("to selector and a null class", function() { + m.render(G.root, m(".foo1", {className: undefined})) + m.render(G.root, m(".foo2", {class: null})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and a null className", function() { + m.render(G.root, m(".foo1", {className: undefined})) + m.render(G.root, m(".foo2", {className: null})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and an undefined class", function() { + m.render(G.root, m(".foo1", {className: undefined})) + m.render(G.root, m(".foo2", {class: undefined})) + + o(G.root.firstChild.className).equals("foo2") + }) + o("to selector and an undefined className", function() { + m.render(G.root, m(".foo1", {className: undefined})) + m.render(G.root, m(".foo2", {className: undefined})) + + o(G.root.firstChild.className).equals("foo2") + }) + }) + }) +}) diff --git a/tests/core/render.js b/tests/core/render.js new file mode 100644 index 000000000..9d66006ae --- /dev/null +++ b/tests/core/render.js @@ -0,0 +1,236 @@ +import o from "ospec" + +import {setupGlobals} from "../../test-utils/global.js" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("render", function() { + var G = setupGlobals() + + o("renders plain text", function() { + m.render(G.root, "a") + o(G.root.childNodes.length).equals(1) + o(G.root.childNodes[0].nodeValue).equals("a") + }) + + o("updates plain text", function() { + m.render(G.root, "a") + m.render(G.root, "b") + o(G.root.childNodes.length).equals(1) + o(G.root.childNodes[0].nodeValue).equals("b") + }) + + o("renders a number", function() { + m.render(G.root, 1) + o(G.root.childNodes.length).equals(1) + o(G.root.childNodes[0].nodeValue).equals("1") + }) + + o("updates a number", function() { + m.render(G.root, 1) + m.render(G.root, 2) + o(G.root.childNodes.length).equals(1) + o(G.root.childNodes[0].nodeValue).equals("2") + }) + + o("overwrites existing content", function() { + var vnodes = [] + + G.root.appendChild(G.window.document.createElement("div")); + + m.render(G.root, vnodes) + + o(G.root.childNodes.length).equals(0) + }) + + o("throws on invalid root node", function() { + var threw = false + try { + m.render(null, []) + } catch (e) { + threw = true + } + o(threw).equals(true) + }) + + o("tries to re-initialize a component that threw on create", function() { + var A = o.spy(() => { throw "error" }) + console.error = o.spy() + + m.render(G.root, m(A)) + + o(A.callCount).equals(1) + + m.render(G.root, m(A)) + + o(A.callCount).equals(2) + + o(console.error.calls.map((c) => c.args[0])).deepEquals(["error", "error"]) + }) + o("tries to re-initialize a stateful component whose view threw on create", function() { + var A = o.spy(() => view) + var view = o.spy(() => { throw "error" }) + console.error = o.spy() + + m.render(G.root, m(A)) + + o(A.callCount).equals(1) + o(view.callCount).equals(1) + + m.render(G.root, m(A)) + + o(A.callCount).equals(1) + o(view.callCount).equals(2) + + o(console.error.calls.map((c) => c.args[0])).deepEquals(["error", "error"]) + }) + o("lifecycle methods work in keyed children of recycled keyed", function() { + var removeA = o.spy() + var removeB = o.spy() + var layoutA = o.spy() + var layoutB = o.spy() + var a = () => m.keyed([[1, m("div", m.keyed([ + [11, m("div", m.layout(layoutA), m.remove(removeA))], + [12, m("div")], + ]))]]) + var b = () => m.keyed([[2, m("div", m.keyed([ + [21, m("div", m.layout(layoutB), m.remove(removeB))], + [22, m("div")], + ]))]]) + m.render(G.root, a()) + var first = G.root.firstChild.firstChild + m.render(G.root, b()) + var second = G.root.firstChild.firstChild + m.render(G.root, a()) + var third = G.root.firstChild.firstChild + + o(layoutA.callCount).equals(2) + o(layoutA.calls[0].args[0]).equals(first) + o(layoutA.calls[1].args[0]).equals(third) + o(removeA.callCount).equals(1) + + o(layoutB.callCount).equals(1) + o(layoutB.calls[0].args[0]).equals(second) + o(removeB.callCount).equals(1) + }) + o("lifecycle methods work in unkeyed children of recycled keyed", function() { + var removeA = o.spy() + var removeB = o.spy() + var layoutA = o.spy() + var layoutB = o.spy() + var a = () => m.keyed([[1, m("div", + m("div", m.layout(layoutA), m.remove(removeA)) + )]]) + var b = () => m.keyed([[2, m("div", + m("div", m.layout(layoutB), m.remove(removeB)) + )]]) + m.render(G.root, a()) + var first = G.root.firstChild.firstChild + m.render(G.root, b()) + var second = G.root.firstChild.firstChild + m.render(G.root, a()) + var third = G.root.firstChild.firstChild + + o(layoutA.callCount).equals(2) + o(layoutA.calls[0].args[0]).equals(first) + o(layoutA.calls[1].args[0]).equals(third) + o(removeA.callCount).equals(1) + + o(layoutB.callCount).equals(1) + o(layoutB.calls[0].args[0]).equals(second) + o(removeB.callCount).equals(1) + }) + o("update lifecycle methods work on children of recycled keyed", function() { + var removeA = o.spy() + var removeB = o.spy() + var layoutA = o.spy() + var layoutB = o.spy() + + var a = () => m.keyed([[1, m("div", + m("div", m.layout(layoutA), m.remove(removeA)) + )]]) + var b = () => m.keyed([[2, m("div", + m("div", m.layout(layoutB), m.remove(removeB)) + )]]) + m.render(G.root, a()) + m.render(G.root, a()) + var first = G.root.firstChild.firstChild + o(layoutA.callCount).equals(2) + o(layoutA.calls[0].args[0]).equals(first) + o(layoutA.calls[1].args[0]).equals(first) + o(removeA.callCount).equals(0) + + m.render(G.root, b()) + var second = G.root.firstChild.firstChild + o(layoutA.callCount).equals(2) + o(removeA.callCount).equals(1) + + o(layoutB.callCount).equals(1) + o(layoutB.calls[0].args[0]).equals(second) + o(removeB.callCount).equals(0) + + m.render(G.root, a()) + m.render(G.root, a()) + var third = G.root.firstChild.firstChild + o(layoutB.callCount).equals(1) + o(removeB.callCount).equals(1) + + o(layoutA.callCount).equals(4) + o(layoutA.calls[2].args[0]).equals(third) + o(layoutA.calls[3].args[0]).equals(third) + o(removeA.callCount).equals(1) + }) + o("svg namespace is preserved in keyed diff (#1820)", function(){ + var svg = m("svg", m.keyed([ + [0, m("g")], + [1, m("g")], + ])) + m.render(G.root, svg) + + o(svg.d.namespaceURI).equals("http://www.w3.org/2000/svg") + o(svg.d.childNodes[0].namespaceURI).equals("http://www.w3.org/2000/svg") + o(svg.d.childNodes[1].namespaceURI).equals("http://www.w3.org/2000/svg") + + svg = m("svg", m.keyed([ + [1, m("g", {x: 1})], + [2, m("g", {x: 2})], + ])) + m.render(G.root, svg) + + o(svg.d.namespaceURI).equals("http://www.w3.org/2000/svg") + o(svg.d.childNodes[0].namespaceURI).equals("http://www.w3.org/2000/svg") + o(svg.d.childNodes[1].namespaceURI).equals("http://www.w3.org/2000/svg") + }) + o("the namespace of the root is passed to children", function() { + m.render(G.root, m("svg")) + o(G.root.childNodes[0].namespaceURI).equals("http://www.w3.org/2000/svg") + m.render(G.root.childNodes[0], m("g")) + o(G.root.childNodes[0].childNodes[0].namespaceURI).equals("http://www.w3.org/2000/svg") + }) + o("does not allow reentrant invocations", function() { + var thrown = [] + function A() { + try {m.render(G.root, m(A))} catch (e) {thrown.push("construct")} + return () => { + try {m.render(G.root, m(A))} catch (e) {thrown.push("view")} + } + } + m.render(G.root, m(A)) + o(thrown).deepEquals([ + "construct", + "view", + ]) + m.render(G.root, m(A)) + o(thrown).deepEquals([ + "construct", + "view", + "view", + ]) + m.render(G.root, []) + o(thrown).deepEquals([ + "construct", + "view", + "view", + ]) + }) +}) diff --git a/tests/core/retain.js b/tests/core/retain.js new file mode 100644 index 000000000..dc1cd746e --- /dev/null +++ b/tests/core/retain.js @@ -0,0 +1,88 @@ +import o from "ospec" + +import {setupGlobals} from "../../test-utils/global.js" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("retain", function() { + var G = setupGlobals() + + o("prevents update in element", function() { + var vnode = m("div", {id: "a"}, "b") + var updated = m.retain() + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(G.root.firstChild.attributes["id"].value).equals("a") + o(G.root.firstChild.childNodes.length).equals(1) + o(G.root.firstChild.childNodes[0].nodeValue).equals("b") + o(updated).deepEquals(vnode) + }) + + o("prevents update in fragment", function() { + var vnode = m.normalize(["a"]) + var updated = m.retain() + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(G.root.firstChild.nodeValue).equals("a") + o(updated).deepEquals(vnode) + }) + + o("ignored if used on creation", function() { + var retain = m.retain() + + m.render(G.root, retain) + + o(G.root.childNodes.length).equals(0) + o(retain.m).equals(-1) + }) + + o("prevents update in component", function() { + var component = (attrs, old) => (old ? m.retain() : m("div", attrs.children)) + var vnode = m(component, "a") + var updated = m(component, "b") + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(G.root.firstChild.firstChild.nodeValue).equals("a") + o(updated.c).deepEquals(vnode.c) + }) + + o("prevents update in component and for component", function() { + var component = ({id}, old) => (old ? m.retain() : m("div", {id})) + var vnode = m(component, {id: "a"}) + var updated = m.retain() + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(G.root.firstChild.attributes["id"].value).equals("a") + o(updated).deepEquals(vnode) + }) + + o("prevents update for component but not in component", function() { + var component = ({id}) => m("div", {id}) + var vnode = m(component, {id: "a"}) + var updated = m.retain() + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(G.root.firstChild.attributes["id"].value).equals("a") + o(updated).deepEquals(vnode) + }) + + o("ignored if used on component creation", function() { + var retain = m.retain() + var component = () => retain + + m.render(G.root, m(component)) + + o(G.root.childNodes.length).equals(0) + o(retain.m).equals(-1) + }) +}) diff --git a/tests/core/textContent.js b/tests/core/textContent.js new file mode 100644 index 000000000..1dfe9d5f8 --- /dev/null +++ b/tests/core/textContent.js @@ -0,0 +1,191 @@ +import o from "ospec" + +import {setupGlobals} from "../../test-utils/global.js" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("textContent", function() { + var G = setupGlobals() + + o("ignores null", function() { + var vnode = m("a", null) + + m.render(G.root, vnode) + + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(0) + o(vnode.d).equals(G.root.childNodes[0]) + }) + o("ignores undefined", function() { + var vnode = m("a", undefined) + + m.render(G.root, vnode) + + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(0) + o(vnode.d).equals(G.root.childNodes[0]) + }) + o("creates string", function() { + var vnode = m("a", "a") + + m.render(G.root, vnode) + + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(1) + o(vnode.d.childNodes[0].nodeValue).equals("a") + o(vnode.d).equals(G.root.childNodes[0]) + }) + o("creates falsy string", function() { + var vnode = m("a", "") + + m.render(G.root, vnode) + + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(1) + o(vnode.d.childNodes[0].nodeValue).equals("") + o(vnode.d).equals(G.root.childNodes[0]) + }) + o("creates number", function() { + var vnode = m("a", 1) + + m.render(G.root, vnode) + + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(1) + o(vnode.d.childNodes[0].nodeValue).equals("1") + o(vnode.d).equals(G.root.childNodes[0]) + }) + o("creates falsy number", function() { + var vnode = m("a", 0) + + m.render(G.root, vnode) + + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(1) + o(vnode.d.childNodes[0].nodeValue).equals("0") + o(vnode.d).equals(G.root.childNodes[0]) + }) + o("creates boolean", function() { + var vnode = m("a", true) + + m.render(G.root, vnode) + + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(0) + o(vnode.d).equals(G.root.childNodes[0]) + }) + o("creates falsy boolean", function() { + var vnode = m("a", false) + + m.render(G.root, vnode) + + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(0) + o(vnode.d).equals(G.root.childNodes[0]) + }) + o("updates to string", function() { + var vnode = m("a", "a") + var updated = m("a", "b") + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(1) + o(vnode.d.childNodes[0].nodeValue).equals("b") + o(updated.d).equals(G.root.childNodes[0]) + }) + o("updates to falsy string", function() { + var vnode = m("a", "a") + var updated = m("a", "") + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(1) + o(vnode.d.childNodes[0].nodeValue).equals("") + o(updated.d).equals(G.root.childNodes[0]) + }) + o("updates to number", function() { + var vnode = m("a", "a") + var updated = m("a", 1) + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(1) + o(vnode.d.childNodes[0].nodeValue).equals("1") + o(updated.d).equals(G.root.childNodes[0]) + }) + o("updates to falsy number", function() { + var vnode = m("a", "a") + var updated = m("a", 0) + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(1) + o(vnode.d.childNodes[0].nodeValue).equals("0") + o(updated.d).equals(G.root.childNodes[0]) + }) + o("updates true to nothing", function() { + var vnode = m("a", "a") + var updated = m("a", true) + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(0) + o(updated.d).equals(G.root.childNodes[0]) + }) + o("updates false to nothing", function() { + var vnode = m("a", "a") + var updated = m("a", false) + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(0) + o(updated.d).equals(G.root.childNodes[0]) + }) + o("updates with typecasting", function() { + var vnode = m("a", "1") + var updated = m("a", 1) + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(1) + o(vnode.d.childNodes[0].nodeValue).equals("1") + o(updated.d).equals(G.root.childNodes[0]) + }) + o("updates from without text to with text", function() { + var vnode = m("a") + var updated = m("a", "b") + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(1) + o(vnode.d.childNodes[0].nodeValue).equals("b") + o(updated.d).equals(G.root.childNodes[0]) + }) + o("updates from with text to without text", function() { + var vnode = m("a", "a") + var updated = m("a") + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(G.root.childNodes.length).equals(1) + o(vnode.d.childNodes.length).equals(0) + o(updated.d).equals(G.root.childNodes[0]) + }) +}) diff --git a/tests/core/updateElement.js b/tests/core/updateElement.js new file mode 100644 index 000000000..565e7a83b --- /dev/null +++ b/tests/core/updateElement.js @@ -0,0 +1,282 @@ +import o from "ospec" + +import {setupGlobals} from "../../test-utils/global.js" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("updateElement", function() { + var G = setupGlobals() + + o("updates attr", function() { + var vnode = m("a", {id: "b"}) + var updated = m("a", {id: "c"}) + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(updated.d).equals(vnode.d) + o(updated.d).equals(G.root.firstChild) + o(updated.d.attributes["id"].value).equals("c") + }) + o("adds attr", function() { + var vnode = m("a", {id: "b"}) + var updated = m("a", {id: "c", title: "d"}) + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(updated.d).equals(vnode.d) + o(updated.d).equals(G.root.firstChild) + o(updated.d.attributes["title"].value).equals("d") + }) + o("adds attr from empty attrs", function() { + var vnode = m("a") + var updated = m("a", {title: "d"}) + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(updated.d).equals(vnode.d) + o(updated.d).equals(G.root.firstChild) + o(updated.d.attributes["title"].value).equals("d") + }) + o("removes attr", function() { + var vnode = m("a", {id: "b", title: "d"}) + var updated = m("a", {id: "c"}) + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(updated.d).equals(vnode.d) + o(updated.d).equals(G.root.firstChild) + o("title" in updated.d.attributes).equals(false) + }) + o("removes class", function() { + var vnode = m("a", {id: "b", className: "d"}) + var updated = m("a", {id: "c"}) + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(updated.d).equals(vnode.d) + o(updated.d).equals(G.root.firstChild) + o("class" in updated.d.attributes).equals(false) + }) + o("creates style object", function() { + var vnode = m("a") + var updated = m("a", {style: {backgroundColor: "green"}}) + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(updated.d.style.backgroundColor).equals("green") + }) + o("creates style string", function() { + var vnode = m("a") + var updated = m("a", {style: "background-color:green"}) + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(updated.d.style.backgroundColor).equals("green") + }) + o("updates style from object to object", function() { + var vnode = m("a", {style: {backgroundColor: "red"}}) + var updated = m("a", {style: {backgroundColor: "green"}}) + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(updated.d.style.backgroundColor).equals("green") + }) + o("updates style from object to string", function() { + var vnode = m("a", {style: {backgroundColor: "red"}}) + var updated = m("a", {style: "background-color:green;"}) + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(updated.d.style.backgroundColor).equals("green") + }) + o("handles noop style change when style is string", function() { + var vnode = m("a", {style: "background-color:green;"}) + var updated = m("a", {style: "background-color:green;"}) + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(updated.d.style.backgroundColor).equals("green") + }) + o("handles noop style change when style is object", function() { + var vnode = m("a", {style: {backgroundColor: "red"}}) + var updated = m("a", {style: {backgroundColor: "red"}}) + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(updated.d.style.backgroundColor).equals("red") + }) + o("updates style from string to object", function() { + var vnode = m("a", {style: "background-color:red;"}) + var updated = m("a", {style: {backgroundColor: "green"}}) + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(updated.d.style.backgroundColor).equals("green") + }) + o("updates style from string to string", function() { + var vnode = m("a", {style: "background-color:red;"}) + var updated = m("a", {style: "background-color:green;"}) + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(updated.d.style.backgroundColor).equals("green") + }) + o("removes style from object to object", function() { + var vnode = m("a", {style: {backgroundColor: "red", border: "1px solid red"}}) + var updated = m("a", {style: {backgroundColor: "red"}}) + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(updated.d.style.backgroundColor).equals("red") + o(updated.d.style.border).equals("") + }) + o("removes style from string to object", function() { + var vnode = m("a", {style: "background-color:red;border:1px solid red"}) + var updated = m("a", {style: {backgroundColor: "red"}}) + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(updated.d.style.backgroundColor).equals("red") + o(updated.d.style.border).notEquals("1px solid red") + }) + o("removes style from object to string", function() { + var vnode = m("a", {style: {backgroundColor: "red", border: "1px solid red"}}) + var updated = m("a", {style: "background-color:red"}) + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(updated.d.style.backgroundColor).equals("red") + o(updated.d.style.border).equals("") + }) + o("removes style from string to string", function() { + var vnode = m("a", {style: "background-color:red;border:1px solid red"}) + var updated = m("a", {style: "background-color:red"}) + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(updated.d.style.backgroundColor).equals("red") + o(updated.d.style.border).equals("") + }) + o("does not re-render element styles for equivalent style objects", function() { + var style = {color: "gold"} + var vnode = m("a", {style: style}) + + m.render(G.root, vnode) + + G.root.firstChild.style.color = "red" + style = {color: "gold"} + var updated = m("a", {style: style}) + m.render(G.root, updated) + + o(updated.d.style.color).equals("red") + }) + o("setting style to `null` removes all styles", function() { + var vnode = m("p", {style: "background-color: red"}) + var updated = m("p", {style: null}) + + m.render(G.root, vnode) + + o("style" in vnode.d.attributes).equals(true) + o(vnode.d.attributes.style.value).equals("background-color: red;") + + m.render(G.root, updated) + + //browsers disagree here + try { + o(updated.d.attributes.style.value).equals("") + + } catch (e) { + o("style" in updated.d.attributes).equals(false) + + } + }) + o("setting style to `undefined` removes all styles", function() { + var vnode = m("p", {style: "background-color: red"}) + var updated = m("p", {style: undefined}) + + m.render(G.root, vnode) + + o("style" in vnode.d.attributes).equals(true) + o(vnode.d.attributes.style.value).equals("background-color: red;") + + m.render(G.root, updated) + + //browsers disagree here + try { + + o(updated.d.attributes.style.value).equals("") + + } catch (e) { + + o("style" in updated.d.attributes).equals(false) + + } + }) + o("not setting style removes all styles", function() { + var vnode = m("p", {style: "background-color: red"}) + var updated = m("p") + + m.render(G.root, vnode) + + o("style" in vnode.d.attributes).equals(true) + o(vnode.d.attributes.style.value).equals("background-color: red;") + + m.render(G.root, updated) + + //browsers disagree here + try { + + o(updated.d.attributes.style.value).equals("") + + } catch (e) { + + o("style" in updated.d.attributes).equals(false) + + } + }) + o("replaces el", function() { + var vnode = m("a") + var updated = m("b") + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(updated.d).equals(G.root.firstChild) + o(updated.d.nodeName).equals("B") + }) + o("updates svg class", function() { + var vnode = m("svg", {className: "a"}) + var updated = m("svg", {className: "b"}) + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(updated.d.attributes["class"].value).equals("b") + }) + o("updates svg child", function() { + var vnode = m("svg", m("circle")) + var updated = m("svg", m("line")) + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(updated.d.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") + }) +}) diff --git a/tests/core/updateFragment.js b/tests/core/updateFragment.js new file mode 100644 index 000000000..daf4ab9b7 --- /dev/null +++ b/tests/core/updateFragment.js @@ -0,0 +1,60 @@ +import o from "ospec" + +import {setupGlobals} from "../../test-utils/global.js" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("updateFragment", function() { + var G = setupGlobals() + + o("updates fragment", function() { + var vnode = [m("a")] + var updated = [m("b")] + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(updated[0].d).equals(G.root.firstChild) + o(updated[0].d.nodeName).equals("B") + }) + o("adds els", function() { + var vnode = [] + var updated = [m("a"), m("b")] + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(updated[0].d).equals(G.root.firstChild) + o(G.root.childNodes.length).equals(2) + o(G.root.childNodes[0].nodeName).equals("A") + o(G.root.childNodes[1].nodeName).equals("B") + }) + o("removes els", function() { + var vnode = [m("a"), m("b")] + var updated = [] + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(G.root.childNodes.length).equals(0) + }) + o("updates from childless fragment", function() { + var vnode = [] + var updated = [m("a")] + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(updated[0].d).equals(G.root.firstChild) + o(updated[0].d.nodeName).equals("A") + }) + o("updates to childless fragment", function() { + var vnode = [m("a")] + var updated = [] + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(G.root.childNodes.length).equals(0) + }) +}) diff --git a/tests/core/updateNodes.js b/tests/core/updateNodes.js new file mode 100644 index 000000000..fa5fd49e9 --- /dev/null +++ b/tests/core/updateNodes.js @@ -0,0 +1,957 @@ +import o from "ospec" + +import {setupGlobals} from "../../test-utils/global.js" + +import m from "../../src/entry/mithril.esm.js" + +function vnodify(str) { + return m.keyed(str.split(","), (k) => [k, m(k)]) +} + +o.spec("updateNodes", function() { + var G = setupGlobals() + + o("handles keyed noop", function() { + var vnodes = m.keyed([[1, m("a")], [2, m("b")]]) + var updated = m.keyed([[1, m("a")], [2, m("b")]]) + + m.render(G.root, vnodes) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + }) + o("handles el noop without key", function() { + var vnodes = [m("a"), m("b")] + var updated = [m("a"), m("b")] + + m.render(G.root, vnodes) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) + o(updated[0].d).equals(G.root.childNodes[0]) + o(updated[1].d).equals(G.root.childNodes[1]) + }) + o("handles text noop", function() { + var vnodes = "a" + var updated = "a" + + m.render(G.root, vnodes) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeValue)).deepEquals(["a"]) + }) + o("handles text noop w/ type casting", function() { + var vnodes = 1 + var updated = "1" + + m.render(G.root, vnodes) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeValue)).deepEquals(["1"]) + }) + o("handles falsy text noop w/ type casting", function() { + var vnodes = 0 + var updated = "0" + + m.render(G.root, vnodes) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeValue)).deepEquals(["0"]) + }) + o("handles fragment noop", function() { + var vnodes = [m("a")] + var updated = [m("a")] + + m.render(G.root, vnodes) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A"]) + o(updated[0].d).equals(G.root.childNodes[0]) + }) + o("handles fragment noop w/ text child", function() { + var vnodes = [m.normalize("a")] + var updated = [m.normalize("a")] + + m.render(G.root, vnodes) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeValue)).deepEquals(["a"]) + o(updated[0].d).equals(G.root.childNodes[0]) + }) + o("handles undefined to null noop", function() { + var vnodes = [null, m("div")] + var updated = [undefined, m("div")] + + m.render(G.root, vnodes) + m.render(G.root, updated) + + o(G.root.childNodes.length).equals(1) + }) + o("reverses els w/ even count", function() { + var vnodes = m.keyed([[1, m("a")], [2, m("b")], [3, m("i")], [4, m("s")]]) + var updated = m.keyed([[4, m("s")], [3, m("i")], [2, m("b")], [1, m("a")]]) + + m.render(G.root, vnodes) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["S", "I", "B", "A"]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + o([...updated.a][2][1].d).equals(G.root.childNodes[2]) + o([...updated.a][3][1].d).equals(G.root.childNodes[3]) + }) + o("reverses els w/ odd count", function() { + var vnodes = m.keyed([[1, m("a")], [2, m("b")], [3, m("i")]]) + var updated = m.keyed([[3, m("i")], [2, m("b")], [1, m("a")]]) + m.render(G.root, vnodes) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["I", "B", "A"]) + }) + o("creates el at start", function() { + var vnodes = m.keyed([[1, m("a")]]) + var updated = m.keyed([[2, m("b")], [1, m("a")]]) + + m.render(G.root, vnodes) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["B", "A"]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + }) + o("creates el at end", function() { + var vnodes = m.keyed([[1, m("a")]]) + var updated = m.keyed([[1, m("a")], [2, m("b")]]) + + m.render(G.root, vnodes) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + }) + o("creates el in middle", function() { + var vnodes = m.keyed([[1, m("a")], [2, m("b")]]) + var updated = m.keyed([[1, m("a")], [3, m("i")], [2, m("b")]]) + + m.render(G.root, vnodes) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "I", "B"]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + o([...updated.a][2][1].d).equals(G.root.childNodes[2]) + }) + o("creates el while reversing", function() { + var vnodes = m.keyed([[1, m("a")], [2, m("b")]]) + var updated = m.keyed([[2, m("b")], [3, m("i")], [1, m("a")]]) + + m.render(G.root, vnodes) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["B", "I", "A"]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + o([...updated.a][2][1].d).equals(G.root.childNodes[2]) + }) + o("deletes el at start", function() { + var vnodes = m.keyed([[2, m("b")], [1, m("a")]]) + var updated = m.keyed([[1, m("a")]]) + + m.render(G.root, vnodes) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A"]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + }) + o("deletes el at end", function() { + var vnodes = m.keyed([[1, m("a")], [2, m("b")]]) + var updated = m.keyed([[1, m("a")]]) + + m.render(G.root, vnodes) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A"]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + }) + o("deletes el at middle", function() { + var vnodes = m.keyed([[1, m("a")], [3, m("i")], [2, m("b")]]) + var updated = m.keyed([[1, m("a")], [2, m("b")]]) + + m.render(G.root, vnodes) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + }) + o("deletes el while reversing", function() { + var vnodes = m.keyed([[1, m("a")], [3, m("i")], [2, m("b")]]) + var updated = m.keyed([[2, m("b")], [1, m("a")]]) + + m.render(G.root, vnodes) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["B", "A"]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + }) + o("creates, deletes, reverses els at same time", function() { + var vnodes = m.keyed([[1, m("a")], [3, m("i")], [2, m("b")]]) + var updated = m.keyed([[2, m("b")], [1, m("a")], [4, m("s")]]) + + m.render(G.root, vnodes) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["B", "A", "S"]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + o([...updated.a][2][1].d).equals(G.root.childNodes[2]) + }) + o("creates, deletes, reverses els at same time with '__proto__' key", function() { + var vnodes = m.keyed([["__proto__", m("a")], [3, m("i")], [2, m("b")]]) + var updated = m.keyed([[2, m("b")], ["__proto__", m("a")], [4, m("s")]]) + + m.render(G.root, vnodes) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["B", "A", "S"]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + o([...updated.a][2][1].d).equals(G.root.childNodes[2]) + }) + o("adds to empty fragment followed by el", function() { + var vnodes = m.keyed([[1, []], [2, m("b")]]) + var updated = m.keyed([[1, m("a")], [2, m("b")]]) + + m.render(G.root, vnodes) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + }) + o("reverses followed by el", function() { + var vnodes = m.keyed([[1, m.keyed([[2, m("a")], [3, m("b")]])], [4, m("i")]]) + var updated = m.keyed([[1, m.keyed([[3, m("b")], [2, m("a")]])], [4, m("i")]]) + + m.render(G.root, vnodes) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["B", "A", "I"]) + o([...[...updated.a][0][1].a][0][1].d).equals(G.root.childNodes[0]) + o([...[...updated.a][0][1].a][1][1].d).equals(G.root.childNodes[1]) + o([...updated.a][1][1].d).equals(G.root.childNodes[2]) + }) + o("populates fragment followed by el keyed", function() { + var vnodes = m.keyed([[1, []], [2, m("i")]]) + var updated = m.keyed([[1, [m("a"), m("b")]], [2, m("i")]]) + + m.render(G.root, vnodes) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B", "I"]) + o([...updated.a][0][1].c[0].d).equals(G.root.childNodes[0]) + o([...updated.a][0][1].c[1].d).equals(G.root.childNodes[1]) + o([...updated.a][1][1].d).equals(G.root.childNodes[2]) + }) + o("populates childless fragment replaced followed by el keyed", function() { + var vnodes = m.keyed([[1, []], [2, m("i")]]) + var updated = m.keyed([[1, [m("a"), m("b")]], [2, m("i")]]) + + m.render(G.root, vnodes) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B", "I"]) + o([...updated.a][0][1].c[0].d).equals(G.root.childNodes[0]) + o([...updated.a][0][1].c[1].d).equals(G.root.childNodes[1]) + o([...updated.a][1][1].d).equals(G.root.childNodes[2]) + }) + o("moves from end to start", function() { + var vnodes = m.keyed([[1, m("a")], [2, m("b")], [3, m("i")], [4, m("s")]]) + var updated = m.keyed([[4, m("s")], [1, m("a")], [2, m("b")], [3, m("i")]]) + + m.render(G.root, vnodes) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["S", "A", "B", "I"]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + o([...updated.a][2][1].d).equals(G.root.childNodes[2]) + o([...updated.a][3][1].d).equals(G.root.childNodes[3]) + }) + o("moves from start to end", function() { + var vnodes = m.keyed([[1, m("a")], [2, m("b")], [3, m("i")], [4, m("s")]]) + var updated = m.keyed([[2, m("b")], [3, m("i")], [4, m("s")], [1, m("a")]]) + + m.render(G.root, vnodes) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["B", "I", "S", "A"]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + o([...updated.a][2][1].d).equals(G.root.childNodes[2]) + o([...updated.a][3][1].d).equals(G.root.childNodes[3]) + }) + o("removes then recreate", function() { + var vnodes = m.keyed([[1, m("a")], [2, m("b")], [3, m("i")], [4, m("s")]]) + var temp = m.keyed([]) + var updated = m.keyed([[1, m("a")], [2, m("b")], [3, m("i")], [4, m("s")]]) + + m.render(G.root, vnodes) + m.render(G.root, temp) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B", "I", "S"]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + o([...updated.a][2][1].d).equals(G.root.childNodes[2]) + o([...updated.a][3][1].d).equals(G.root.childNodes[3]) + }) + o("removes then recreate reversed", function() { + var vnodes = m.keyed([[1, m("a")], [2, m("b")], [3, m("i")], [4, m("s")]]) + var temp = m.keyed([]) + var updated = m.keyed([[4, m("s")], [3, m("i")], [2, m("b")], [1, m("a")]]) + + m.render(G.root, vnodes) + m.render(G.root, temp) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["S", "I", "B", "A"]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + o([...updated.a][2][1].d).equals(G.root.childNodes[2]) + o([...updated.a][3][1].d).equals(G.root.childNodes[3]) + }) + o("removes then recreate smaller", function() { + var vnodes = m.keyed([[1, m("a")], [2, m("b")]]) + var temp = m.keyed([]) + var updated = m.keyed([[1, m("a")]]) + + m.render(G.root, vnodes) + m.render(G.root, temp) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A"]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + }) + o("removes then recreate bigger", function() { + var vnodes = m.keyed([[1, m("a")], [2, m("b")]]) + var temp = m.keyed([]) + var updated = m.keyed([[1, m("a")], [2, m("b")], [3, m("i")]]) + + m.render(G.root, vnodes) + m.render(G.root, temp) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B", "I"]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + o([...updated.a][2][1].d).equals(G.root.childNodes[2]) + }) + o("removes then create different", function() { + var vnodes = m.keyed([[1, m("a")], [2, m("b")]]) + var temp = m.keyed([]) + var updated = m.keyed([[3, m("i")], [4, m("s")]]) + + m.render(G.root, vnodes) + m.render(G.root, temp) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["I", "S"]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + }) + o("removes then create different smaller", function() { + var vnodes = m.keyed([[1, m("a")], [2, m("b")]]) + var temp = m.keyed([]) + var updated = m.keyed([[3, m("i")]]) + + m.render(G.root, vnodes) + m.render(G.root, temp) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["I"]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + }) + o("removes then create different bigger", function() { + var vnodes = m.keyed([[1, m("a")], [2, m("b")]]) + var temp = m.keyed([]) + var updated = m.keyed([[3, m("i")], [4, m("s")], [5, m("div")]]) + + m.render(G.root, vnodes) + m.render(G.root, temp) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["I", "S", "DIV"]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + o([...updated.a][2][1].d).equals(G.root.childNodes[2]) + }) + o("removes then create mixed", function() { + var vnodes = m.keyed([[1, m("a")], [2, m("b")]]) + var temp = m.keyed([]) + var updated = m.keyed([[1, m("a")], [4, m("s")]]) + + m.render(G.root, vnodes) + m.render(G.root, temp) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "S"]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + }) + o("removes then create mixed reversed", function() { + var vnodes = m.keyed([[1, m("a")], [2, m("b")]]) + var temp = m.keyed([]) + var updated = m.keyed([[4, m("s")], [1, m("a")]]) + + m.render(G.root, vnodes) + m.render(G.root, temp) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["S", "A"]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + }) + o("removes then create mixed smaller", function() { + var vnodes = m.keyed([[1, m("a")], [2, m("b")], [3, m("i")]]) + var temp = m.keyed([]) + var updated = m.keyed([[1, m("a")], [4, m("s")]]) + + m.render(G.root, vnodes) + m.render(G.root, temp) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "S"]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + }) + o("removes then create mixed smaller reversed", function() { + var vnodes = m.keyed([[1, m("a")], [2, m("b")], [3, m("i")]]) + var temp = m.keyed([]) + var updated = m.keyed([[4, m("s")], [1, m("a")]]) + + m.render(G.root, vnodes) + m.render(G.root, temp) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["S", "A"]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + }) + o("removes then create mixed bigger", function() { + var vnodes = m.keyed([[1, m("a")], [2, m("b")]]) + var temp = m.keyed([]) + var updated = m.keyed([[1, m("a")], [3, m("i")], [4, m("s")]]) + + m.render(G.root, vnodes) + m.render(G.root, temp) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "I", "S"]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + o([...updated.a][2][1].d).equals(G.root.childNodes[2]) + }) + o("removes then create mixed bigger reversed", function() { + var vnodes = m.keyed([[1, m("a")], [2, m("b")]]) + var temp = m.keyed([]) + var updated = m.keyed([[4, m("s")], [3, m("i")], [1, m("a")]]) + + m.render(G.root, vnodes) + m.render(G.root, temp) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["S", "I", "A"]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + o([...updated.a][2][1].d).equals(G.root.childNodes[2]) + }) + o("in fragment, nest text inside fragment and add hole", function() { + var vnodes = ["a"] + var updated = [["b"], undefined] + + m.render(G.root, vnodes) + m.render(G.root, updated) + + o(G.root.childNodes.length).equals(1) + }) + o("in element, nest text inside fragment and add hole", function() { + var vnodes = m("div", "a") + var updated = m("div", ["b"], undefined) + + m.render(G.root, vnodes) + m.render(G.root, updated) + + o(G.root.firstChild.childNodes.length).equals(1) + }) + o("change type, position and length", function() { + var vnodes = m("div", {}, undefined, "a") + var updated = m("div", {}, ["b"], undefined, undefined) + + m.render(G.root, vnodes) + m.render(G.root, updated) + + o(G.root.firstChild.childNodes.length).equals(1) + }) + o("removes then recreates then reverses children", function() { + var vnodes = m.keyed([[1, m("a", m.keyed([[3, m("i")], [4, m("s")]]))], [2, m("b")]]) + var temp1 = m.keyed([]) + var temp2 = m.keyed([[1, m("a", m.keyed([[3, m("i")], [4, m("s")]]))], [2, m("b")]]) + var updated = m.keyed([[1, m("a", m.keyed([[4, m("s")], [3, m("i")]]))], [2, m("b")]]) + + m.render(G.root, vnodes) + m.render(G.root, temp1) + m.render(G.root, temp2) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) + o(Array.from(G.root.childNodes[0].childNodes, (n) => n.nodeName)).deepEquals(["S", "I"]) + o([...updated.a][0][1].d).equals(G.root.childNodes[0]) + o([...updated.a][1][1].d).equals(G.root.childNodes[1]) + o([...[...updated.a][0][1].c[0].a][0][1].d).equals(G.root.childNodes[0].childNodes[0]) + o([...[...updated.a][0][1].c[0].a][1][1].d).equals(G.root.childNodes[0].childNodes[1]) + }) + o("removes then recreates nested", function() { + var vnodes = m.keyed([[1, m("a", m.keyed([[3, m("a", m.keyed([[5, m("a")]]))], [4, m("a", m.keyed([[5, m("a")]]))]]))], [2, m("a")]]) + var temp = m.keyed([]) + var updated = m.keyed([[1, m("a", m.keyed([[3, m("a", m.keyed([[5, m("a")]]))], [4, m("a", m.keyed([[5, m("a")]]))]]))], [2, m("a")]]) + + m.render(G.root, vnodes) + m.render(G.root, temp) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "A"]) + o(Array.from(G.root.childNodes[0].childNodes, (n) => n.nodeName)).deepEquals(["A", "A"]) + o(Array.from(G.root.childNodes[0].childNodes[0].childNodes, (n) => n.nodeName)).deepEquals(["A"]) + o(Array.from(G.root.childNodes[1].childNodes, (n) => n.nodeName)).deepEquals([]) + }) + o("reused top-level element children are retained if against the same root and from the most recent render", function () { + var cached = m("a") + + m.render(G.root, cached) + m.render(G.root, cached) + }) + o("reused top-level element children are rejected against a different root", function () { + var cached = m("a") + var otherRoot = G.window.document.createElement("div") + + m.render(G.root, cached) + o(() => m.render(otherRoot, cached)).throws(Error) + }) + o("reused inner fragment element children are retained if against the same root and from the most recent render", function () { + var cached = m("a") + + m.render(G.root, [cached]) + m.render(G.root, [cached]) + }) + o("reused inner fragment element children are rejected against a different root", function () { + var cached = m("a") + var otherRoot = G.window.document.createElement("div") + + m.render(G.root, [cached]) + o(() => m.render(otherRoot, [cached])).throws(Error) + }) + o("reused inner element element children are retained if against the same root and from the most recent render", function () { + var cached = m("a") + + m.render(G.root, m("div", cached)) + m.render(G.root, m("div", cached)) + }) + o("reused inner element element children are rejected against a different root", function () { + var cached = m("a") + var otherRoot = G.window.document.createElement("div") + + m.render(G.root, m("div", cached)) + o(() => m.render(otherRoot, m("div", cached))).throws(Error) + }) + o("reused top-level retain children are retained if against the same root and from the most recent render", function () { + var cached = m.retain() + + m.render(G.root, m("a")) + m.render(G.root, cached) + m.render(G.root, cached) + }) + o("reused top-level retain children are rejected against a different root", function () { + var cached = m.retain() + var otherRoot = G.window.document.createElement("div") + + m.render(G.root, m("a")) + m.render(G.root, cached) + o(() => m.render(otherRoot, cached)).throws(Error) + }) + o("reused inner fragment retain children are retained if against the same root and from the most recent render", function () { + var cached = m.retain() + + m.render(G.root, [m("a")]) + m.render(G.root, [cached]) + m.render(G.root, [cached]) + }) + o("reused inner fragment retain children are rejected against a different root", function () { + var cached = m.retain() + var otherRoot = G.window.document.createElement("div") + + m.render(G.root, [m("a")]) + m.render(G.root, [cached]) + o(() => m.render(otherRoot, [cached])).throws(Error) + }) + o("reused inner element retain children are retained if against the same root and from the most recent render", function () { + var cached = m.retain() + + m.render(G.root, m("div", m("a"))) + m.render(G.root, m("div", cached)) + m.render(G.root, m("div", cached)) + }) + o("reused inner element retain children are rejected against a different root", function () { + var cached = m.retain() + var otherRoot = G.window.document.createElement("div") + + m.render(G.root, m("div", m("a"))) + m.render(G.root, m("div", cached)) + o(() => m.render(otherRoot, m("div", cached))).throws(Error) + }) + o("cross-removal reused top-level element children are rejected against the same root", function () { + var cached = m("a") + + m.render(G.root, cached) + m.render(G.root, null) + o(() => m.render(G.root, cached)).throws(Error) + }) + o("cross-removal reused inner fragment element children are rejected against the same root", function () { + var cached = m("a") + + m.render(G.root, [cached]) + m.render(G.root, [null]) + o(() => m.render(G.root, [cached])).throws(Error) + }) + o("cross-removal reused inner element element children are rejected against the same root", function () { + var cached = m("a") + + m.render(G.root, m("div", cached)) + m.render(G.root, null) + o(() => m.render(G.root, m("div", cached))).throws(Error) + }) + o("cross-removal reused top-level retain children are rejected against the same root", function () { + var cached = m.retain() + + m.render(G.root, m("a")) + m.render(G.root, cached) + m.render(G.root, null) + m.render(G.root, m("a")) + o(() => m.render(G.root, cached)).throws(Error) + }) + o("cross-removal reused inner fragment retain children are rejected against the same root", function () { + var cached = m.retain() + + m.render(G.root, [m("a")]) + m.render(G.root, [cached]) + m.render(G.root, [null]) + m.render(G.root, [m("a")]) + o(() => m.render(G.root, [cached])).throws(Error) + }) + o("cross-removal reused inner element retain children are rejected against the same root", function () { + var cached = m.retain() + + m.render(G.root, m("div", m("a"))) + m.render(G.root, m("div", cached)) + m.render(G.root, m("b")) + m.render(G.root, m("div", m("a"))) + o(() => m.render(G.root, m("div", cached))).throws(Error) + }) + o("cross-replacement reused top-level element children are rejected against the same root", function () { + var cached = m("a") + + m.render(G.root, cached) + m.render(G.root, m("b")) + o(() => m.render(G.root, cached)).throws(Error) + }) + o("cross-replacement reused inner fragment element children are rejected against the same root", function () { + var cached = m("a") + + m.render(G.root, [cached]) + m.render(G.root, [m("b")]) + o(() => m.render(G.root, [cached])).throws(Error) + }) + o("cross-replacement reused inner element element children are rejected against the same root", function () { + var cached = m("a") + + m.render(G.root, m("div", cached)) + m.render(G.root, m("b")) + o(() => m.render(G.root, m("div", cached))).throws(Error) + }) + o("cross-replacement reused top-level retain children are rejected against the same root", function () { + var cached = m.retain() + + m.render(G.root, m("a")) + m.render(G.root, cached) + m.render(G.root, m("b")) + m.render(G.root, m("a")) + o(() => m.render(G.root, cached)).throws(Error) + }) + o("cross-replacement reused inner fragment retain children are rejected against the same root", function () { + var cached = m.retain() + + m.render(G.root, [m("a")]) + m.render(G.root, [cached]) + m.render(G.root, [m("b")]) + m.render(G.root, [m("a")]) + o(() => m.render(G.root, [cached])).throws(Error) + }) + o("cross-replacement reused inner element retain children are rejected against the same root", function () { + var cached = m.retain() + + m.render(G.root, m("div", m("a"))) + m.render(G.root, m("div", cached)) + m.render(G.root, null) + m.render(G.root, m("div", m("a"))) + o(() => m.render(G.root, m("div", cached))).throws(Error) + }) + + o("null stays in place", function() { + var remove = o.spy() + var layout = o.spy() + var vnodes = [m("div"), m("a", m.layout(layout), m.remove(remove))] + var temp = [null, m("a", m.layout(layout), m.remove(remove))] + var updated = [m("div"), m("a", m.layout(layout), m.remove(remove))] + + m.render(G.root, vnodes) + var before = vnodes[1].d + + o(layout.callCount).equals(1) + o(remove.callCount).equals(0) + + m.render(G.root, temp) + + o(layout.callCount).equals(2) + o(remove.callCount).equals(0) + + m.render(G.root, updated) + var after = updated[1].d + + o(layout.callCount).equals(3) + o(remove.callCount).equals(0) + o(before).equals(after) + }) + o("null stays in place if not first", function() { + var remove = o.spy() + var layout = o.spy() + var vnodes = [m("b"), m("div"), m("a", m.layout(layout), m.remove(remove))] + var temp = [m("b"), null, m("a", m.layout(layout), m.remove(remove))] + var updated = [m("b"), m("div"), m("a", m.layout(layout), m.remove(remove))] + + m.render(G.root, vnodes) + var before = vnodes[2].d + + o(layout.callCount).equals(1) + o(remove.callCount).equals(0) + + m.render(G.root, temp) + + o(layout.callCount).equals(2) + o(remove.callCount).equals(0) + + m.render(G.root, updated) + var after = updated[2].d + + o(layout.callCount).equals(3) + o(remove.callCount).equals(0) + o(before).equals(after) + }) + o("node is recreated if unwrapped from a key", function () { + var vnode = m.keyed([[1, m("b")]]) + var updated = m("b") + + m.render(G.root, vnode) + m.render(G.root, updated) + + o([...vnode.a][0][1].d).notEquals(updated.d) + }) + o("don't add back elements from fragments that are restored from the pool #1991", function() { + m.render(G.root, [ + [], + [] + ]) + m.render(G.root, [ + [], + [m("div")] + ]) + m.render(G.root, [ + [null] + ]) + m.render(G.root, [ + [], + [] + ]) + + o(G.root.childNodes.length).equals(0) + }) + o("don't add back elements from fragments that are being removed #1991", function() { + m.render(G.root, [ + [], + m("p"), + ]) + m.render(G.root, [ + [m("div", 5)] + ]) + m.render(G.root, [ + [], + [] + ]) + + o(G.root.childNodes.length).equals(0) + }) + o("handles null values in unkeyed lists of different length (#2003)", function() { + var remove = o.spy() + var layout = o.spy() + + m.render(G.root, [m("div", m.layout(layout), m.remove(remove)), null]) + m.render(G.root, [null, m("div", m.layout(layout), m.remove(remove)), null]) + + o(layout.callCount).equals(2) + o(remove.callCount).equals(1) + }) + o("supports changing the element of a keyed element in a list when traversed bottom-up", function() { + m.render(G.root, m.keyed([[2, m("a")]])) + m.render(G.root, m.keyed([[1, m("b")], [2, m("b")]])) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["B", "B"]) + }) + o("supports changing the element of a keyed element in a list when looking up nodes using the map", function() { + m.render(G.root, m.keyed([[1, m("x")], [2, m("y")], [3, m("z")]])) + m.render(G.root, m.keyed([[2, m("b")], [1, m("c")], [4, m("d")], [3, m("e")]])) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["B", "C", "D", "E"]) + }) + o("don't fetch the nextSibling from the pool", function() { + m.render(G.root, [m.keyed([[1, m("div")], [2, m("div")]]), m("p")]) + m.render(G.root, [m.keyed([]), m("p")]) + m.render(G.root, [m.keyed([[2, m("div")], [1, m("div")]]), m("p")]) + + o(Array.from(G.root.childNodes, (el) => el.nodeName)).deepEquals(["DIV", "DIV", "P"]) + }) + o("reverses a keyed lists with an odd number of items", function() { + var vnodes = vnodify("a,b,c,d") + var updated = vnodify("d,c,b,a") + var expectedTagNames = [...updated.a.keys()] + + m.render(G.root, vnodes) + m.render(G.root, updated) + + var tagNames = Array.from(G.root.childNodes, (n) => n.nodeName.toLowerCase()) + + o(tagNames).deepEquals(expectedTagNames) + }) + o("reverses a keyed lists with an even number of items", function() { + var vnodes = vnodify("a,b,c") + var updated = vnodify("c,b,a") + var vnodes = m.keyed([["a", m("a")], ["b", m("b")], ["c", m("c")]]) + var updated = m.keyed([["c", m("c")], ["b", m("b")], ["a", m("a")]]) + var expectedTagNames = [...updated.a.keys()] + + m.render(G.root, vnodes) + m.render(G.root, updated) + + var tagNames = Array.from(G.root.childNodes, (n) => n.nodeName.toLowerCase()) + + o(tagNames).deepEquals(expectedTagNames) + }) + o("scrambles a keyed lists with prefixes and suffixes", function() { + var vnodes = vnodify("i,a,b,c,d,j") + var updated = vnodify("i,b,a,d,c,j") + var expectedTagNames = [...updated.a.keys()] + + m.render(G.root, vnodes) + m.render(G.root, updated) + + var tagNames = Array.from(G.root.childNodes, (n) => n.nodeName.toLowerCase()) + + o(tagNames).deepEquals(expectedTagNames) + }) + o("reverses a keyed lists with an odd number of items with prefixes and suffixes", function() { + var vnodes = vnodify("i,a,b,c,d,j") + var updated = vnodify("i,d,c,b,a,j") + var expectedTagNames = [...updated.a.keys()] + + m.render(G.root, vnodes) + m.render(G.root, updated) + + var tagNames = Array.from(G.root.childNodes, (n) => n.nodeName.toLowerCase()) + + o(tagNames).deepEquals(expectedTagNames) + }) + o("reverses a keyed lists with an even number of items with prefixes and suffixes", function() { + var vnodes = vnodify("i,a,b,c,j") + var updated = vnodify("i,c,b,a,j") + var expectedTagNames = [...updated.a.keys()] + + m.render(G.root, vnodes) + m.render(G.root, updated) + + var tagNames = Array.from(G.root.childNodes, (n) => n.nodeName.toLowerCase()) + + o(tagNames).deepEquals(expectedTagNames) + }) + o("scrambling sample 1", function() { + 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.a.keys()] + + m.render(G.root, vnodes) + m.render(G.root, updated) + + var tagNames = Array.from(G.root.childNodes, (n) => n.nodeName.toLowerCase()) + + o(tagNames).deepEquals(expectedTagNames) + }) + o("scrambling sample 2", function() { + 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.a.keys()] + + m.render(G.root, vnodes) + m.render(G.root, updated) + + var tagNames = Array.from(G.root.childNodes, (n) => n.nodeName.toLowerCase()) + + o(tagNames).deepEquals(expectedTagNames) + }) + + o("fragment child toggles from null when followed by null component then tag", function() { + var component = () => null + var vnodes = [[m("a"), m(component), m("b")]] + var temp = [[null, m(component), m("b")]] + var updated = [[m("a"), m(component), m("b")]] + + m.render(G.root, vnodes) + m.render(G.root, temp) + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "B"]) + }) + o("fragment child toggles from null in component when followed by null component then tag", function() { + var flag = true + var a = () => (flag ? m("a") : null) + var b = () => null + var vnodes = [[m(a), m(b), m("s")]] + var temp = [[m(a), m(b), m("s")]] + var updated = [[m(a), m(b), m("s")]] + + m.render(G.root, vnodes) + flag = false + m.render(G.root, temp) + flag = true + m.render(G.root, updated) + + o(Array.from(G.root.childNodes, (n) => n.nodeName)).deepEquals(["A", "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 = () => [m("a"), m("b")] + m.render(G.root, [m(component)]) + m.render(G.root, []) + + o(G.root.childNodes.length).equals(0) + }) +}) diff --git a/tests/core/updateNodesFuzzer.js b/tests/core/updateNodesFuzzer.js new file mode 100644 index 000000000..46b527631 --- /dev/null +++ b/tests/core/updateNodesFuzzer.js @@ -0,0 +1,72 @@ +import o from "ospec" + +import {setupGlobals} from "../../test-utils/global.js" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("updateNodes keyed list Fuzzer", () => { + const maxLength = 12 + const testCount = 250 + + const fromUsed = new Set() + const toUsed = new Set() + + function randomInt(max) { + // eslint-disable-next-line no-bitwise + return (Math.random() * max) | 0 + } + + function randomUnique(used) { + for (;;) { + let max = randomInt(maxLength) + const keys = Array.from({length: max}, (_, i) => i) + // Perform a simple Fisher-Yates shuffle on the generated key range. + while (max) { + const index = randomInt(max--) + const temp = keys[index] + keys[index] = keys[max] + keys[max] = temp + } + + const serialized = keys.join() + if (!used.has(serialized)) { + used.add(serialized) + return keys + } + } + } + + var G = setupGlobals() + + function fuzzGroup(label, view, assert) { + o.spec(label, () => { + for (let i = 0; i < testCount; i++) { + const from = randomUnique(fromUsed) + const to = randomUnique(toUsed) + o(`${i}: ${from} -> ${to}`, () => { + m.render(G.root, m.keyed(from, (x) => [x, view(x)])) + m.render(G.root, m.keyed(to, (x) => [x, view(x)])) + assert(G.root, to) + }) + } + }) + } + + fuzzGroup( + "element tag", + (i) => m(`t${i}`), + (root, to) => o(Array.from(root.childNodes, (n) => n.nodeName)).deepEquals(to.map((i) => `T${i}`)) + ) + + fuzzGroup( + "text value", + (i) => `${i}`, + (root, to) => o(Array.from(root.childNodes, (n) => n.nodeValue)).deepEquals(to.map((i) => `${i}`)) + ) + + fuzzGroup( + "text value in element", + (i) => m("div", `${i}`), + (root, to) => o(Array.from(root.childNodes, (n) => n.childNodes[0].nodeValue)).deepEquals(to.map((i) => `${i}`)) + ) +}) diff --git a/tests/core/updateText.js b/tests/core/updateText.js new file mode 100644 index 000000000..2f21b5084 --- /dev/null +++ b/tests/core/updateText.js @@ -0,0 +1,91 @@ +import o from "ospec" + +import {setupGlobals} from "../../test-utils/global.js" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("updateText", function() { + var G = setupGlobals() + + o("updates to string", function() { + var vnode = "a" + var updated = "b" + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(G.root.firstChild.nodeValue).equals("b") + }) + o("updates to falsy string", function() { + var vnode = "a" + var updated = "" + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(G.root.firstChild.nodeValue).equals("") + }) + o("updates from falsy string", function() { + var vnode = "" + var updated = "b" + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(G.root.firstChild.nodeValue).equals("b") + }) + o("updates to number", function() { + var vnode = "a" + var updated = 1 + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(G.root.firstChild.nodeValue).equals("1") + }) + o("updates to falsy number", function() { + var vnode = "a" + var updated = 0 + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(G.root.firstChild.nodeValue).equals("0") + }) + o("updates from falsy number", function() { + var vnode = 0 + var updated = "b" + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(G.root.firstChild.nodeValue).equals("b") + }) + o("updates to boolean", function() { + var vnode = "a" + var updated = true + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(G.root.childNodes.length).equals(0) + }) + o("updates to falsy boolean", function() { + var vnode = "a" + var updated = false + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(G.root.childNodes.length).equals(0) + }) + o("updates from falsy boolean", function() { + var vnode = false + var updated = "b" + + m.render(G.root, vnode) + m.render(G.root, updated) + + o(G.root.firstChild.nodeValue).equals("b") + }) +}) diff --git a/tests/exported-api.js b/tests/exported-api.js new file mode 100644 index 000000000..64a3fe87f --- /dev/null +++ b/tests/exported-api.js @@ -0,0 +1,109 @@ +/* eslint-disable no-bitwise */ +import o from "ospec" + +import {setupGlobals} from "../test-utils/global.js" + +import m from "../src/entry/mithril.esm.js" + +o.spec("api", function() { + var G = setupGlobals() + + o.spec("m", function() { + o("works", function() { + var vnode = m("div") + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_ELEMENT) + o(vnode.t).equals("div") + }) + }) + o.spec("m.normalize", function() { + o("works", function() { + var vnode = m.normalize([m("div")]) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_FRAGMENT) + o(vnode.c.length).equals(1) + o(vnode.c[0].t).equals("div") + }) + }) + o.spec("m.keyed", function() { + o("works", function() { + var vnode = m.keyed([123], (k) => [k, [m("div")]]) + + o(vnode.m & m.TYPE_MASK).equals(m.TYPE_KEYED) + o(vnode.a.size).equals(1) + o([...vnode.a][0][0]).equals(123) + o([...vnode.a][0][1].c.length).equals(1) + o([...vnode.a][0][1].c[0].t).equals("div") + }) + }) + o.spec("m.p", function() { + o("works", function() { + var query = m.p("/foo/:c", {a: 1, b: 2, c: 3}) + + o(query).equals("/foo/3?a=1&b=2") + }) + }) + o.spec("m.render", function() { + o("works", function() { + m.render(G.root, m("div")) + + o(G.root.childNodes.length).equals(1) + o(G.root.firstChild.nodeName).equals("DIV") + }) + }) + + o.spec("m.mount", function() { + o("works", function() { + var count = 0 + var redraw = m.mount(G.root, () => { + count++ + return m("div") + }) + + o(G.root.childNodes.length).equals(1) + o(G.root.firstChild.nodeName).equals("DIV") + + redraw() + o(count).equals(1) + G.rafMock.fire() + o(count).equals(2) + + redraw.sync() + o(count).equals(3) + }) + }) + + o.spec("m.route, m.link", function() { + o("works", async() => { + var route + var App = function () { + route = this.route + if (route.path === "/a") { + return m("div") + } else if (route.path === "/b") { + return m("a", m.link("/a")) + } else { + route.set("/a") + } + } + + m.mount(G.root, () => m.route("#", () => m(App))) + + await Promise.resolve() + G.rafMock.fire() + o(G.rafMock.queueLength()).equals(0) + + o(G.root.childNodes.length).equals(1) + o(G.root.firstChild.nodeName).equals("DIV") + o(route.current).equals("/a") + + route.set("/b") + + await Promise.resolve() + G.rafMock.fire() + o(G.rafMock.queueLength()).equals(0) + + o(route.current).equals("/b") + }) + }) +}) diff --git a/tests/std/debouncer.js b/tests/std/debouncer.js new file mode 100644 index 000000000..b6d1a2e6a --- /dev/null +++ b/tests/std/debouncer.js @@ -0,0 +1,170 @@ +import o from "ospec" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("debouncer", () => { + function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)) + } + + var debounced + + o.afterEach(() => { + if (debounced) debounced.dispose() + }) + + o("validates create input", () => { + o(() => m.debouncer(NaN)).throws(RangeError) + o(() => m.debouncer(+1/0)).throws(RangeError) + o(() => m.debouncer(-1/0)).throws(RangeError) + o(() => m.debouncer("")).throws(RangeError) + o(() => m.debouncer("123")).throws(RangeError) + o(() => m.debouncer(true)).throws(RangeError) + o(() => m.debouncer(false)).throws(RangeError) + o(() => m.debouncer(null)).throws(RangeError) + o(() => m.debouncer([])).throws(RangeError) + o(() => m.debouncer({})).throws(RangeError) + o(() => m.debouncer(Symbol("wat"))).throws(RangeError) + m.debouncer() + m.debouncer(100) + }) + + o("validates update input", () => { + debounced = m.debouncer() + + o(() => debounced.update(NaN)).throws(RangeError) + o(() => debounced.update(+1/0)).throws(RangeError) + o(() => debounced.update(-1/0)).throws(RangeError) + o(() => debounced.update("")).throws(RangeError) + o(() => debounced.update("123")).throws(RangeError) + o(() => debounced.update(true)).throws(RangeError) + o(() => debounced.update(false)).throws(RangeError) + o(() => debounced.update(null)).throws(RangeError) + o(() => debounced.update([])).throws(RangeError) + o(() => debounced.update({})).throws(RangeError) + o(() => debounced.update(Symbol("wat"))).throws(RangeError) + o(() => debounced.update()).throws(RangeError) + debounced.update(100) + }) + + o("detects edges correctly", async () => { + o.timeout(1000) + + debounced = m.debouncer(100) + + var p1 = debounced() + var p2 = debounced() + await sleep(10) + var p3 = debounced() + await sleep(140) + var p4 = debounced() + o(await p1).equals(undefined) + o(await p2).equals(true) + o(await p3).equals(false) + o(await p4).equals(undefined) + + var p5 = debounced() + await sleep(150) + var p6 = debounced() + o(await p5).equals(false) + o(await p6).equals(undefined) + }) + + o("resets the timer on early hit", async () => { + o.timeout(1000) + + debounced = m.debouncer(100) + + var slept = false + setTimeout(() => { slept = true }, 125) + void debounced() + await sleep(50) + await debounced() + o(slept).equals(true) + }) + + o("handles dynamic changes to higher delays", async () => { + o.timeout(1000) + + debounced = m.debouncer(100) + + var p1 = debounced() + var p2 = debounced() + await sleep(10) + var p3 = debounced() + debounced.update(200) + await sleep(140) + var p4 = debounced() + o(await p1).equals(undefined) + o(await p2).equals(true) + o(await p3).equals(true) + o(await p4).equals(false) + + var p5 = debounced() + await sleep(250) + var p6 = debounced() + o(await p5).equals(undefined) + o(await p6).equals(undefined) + }) + + o("handles dynamic changes to lower delays", async () => { + o.timeout(1000) + + debounced = m.debouncer(100) + + var p1 = debounced() + var p2 = debounced() + await sleep(10) + var p3 = debounced() + debounced.update(50) + await sleep(100) + var p4 = debounced() + o(await p1).equals(undefined) + o(await p2).equals(true) + o(await p3).equals(false) + o(await p4).equals(undefined) + + var p5 = debounced() + await sleep(100) + var p6 = debounced() + o(await p5).equals(false) + o(await p6).equals(undefined) + }) + + o("handles same-duration changes", async () => { + o.timeout(1000) + + debounced = m.debouncer(100) + + var p1 = debounced() + debounced.update(100) + var p2 = debounced() + debounced.update(100) + await sleep(10) + debounced.update(100) + var p3 = debounced() + debounced.update(100) + await sleep(140) + debounced.update(100) + var p4 = debounced() + debounced.update(100) + o(await p1).equals(undefined) + debounced.update(100) + o(await p2).equals(true) + debounced.update(100) + o(await p3).equals(false) + debounced.update(100) + o(await p4).equals(undefined) + debounced.update(100) + + var p5 = debounced() + debounced.update(100) + await sleep(150) + debounced.update(100) + var p6 = debounced() + debounced.update(100) + o(await p5).equals(false) + debounced.update(100) + o(await p6).equals(undefined) + }) +}) diff --git a/tests/std/fetch.js b/tests/std/fetch.js new file mode 100644 index 000000000..dbcc1d45f --- /dev/null +++ b/tests/std/fetch.js @@ -0,0 +1,2007 @@ +/* global FormData */ + +// This alone amounts to over 200k assertions total, but that's because it almost fully +// exhaustively tests the function. (Turns out it's not all that hard.) The function's pretty +// simple, so it doesn't take as long as you'd think. + +import o from "ospec" + +import m from "../../src/entry/mithril.esm.js" +import {setupGlobals} from "../../test-utils/global.js" + +o.spec("fetch", () => { + let global, oldFetch + setupGlobals({ + initialize(g) { + global = g + oldFetch = g.fetch + }, + cleanup(g) { + global = null + g.fetch = oldFetch + }, + }) + + const methods = [ + "HEAD", + "GET", + "PATCH", + "POST", + "PUT", + "DELETE", + ] + + const okStatuses = { + 200: "OK", + 201: "Created", + 202: "Accepted", + 203: "Non-authoritative Information", + 206: "Partial Content", + 207: "Multi-Status", + 208: "Already Reported", + 226: "IM Used", + } + + const emptyStatuses = { + 204: "No Content", + 205: "Reset Content", + } + + const emptyErrorStatuses = { + // 1xx statuses aren't supported: https://github.com/whatwg/fetch/issues/1759 + // It's likely that in the future, 101 may be supported, but not 103. + // 101: "Switching Protocols", + // 103: "Early Hints", + 304: "Not Modified", + } + + const errorStatuses = { + // 1xx statuses aren't supported: https://github.com/whatwg/fetch/issues/1759 + // 100: "Continue", + // 102: "Processing", + 300: "Multiple Choices", + 301: "Moved Permanently", + 302: "Found", + 303: "See Other", + 305: "Use Proxy", + 307: "Temporary Redirect", + 308: "Permanent Redirect", + 400: "Bad Request", + 401: "Unauthorized", + 402: "Payment Required", + 403: "Forbidden", + 404: "Not Found", + 405: "Method Not Allowed", + 406: "Not Acceptable", + 407: "Proxy Authentication Required", + 408: "Request Timeout", + 409: "Conflict", + 410: "Gone", + 411: "Length Required", + 412: "Precondition Failed", + 413: "Payload Too Large", + 414: "Request-URI Too Long", + 415: "Unsupported Media Type", + 416: "Requested Range Not Satisfiable", + 417: "Expectation Failed", + 418: "I'm a teapot", + 421: "Misdirected Request", + 422: "Unprocessable Entity", + 423: "Locked", + 424: "Failed Dependency", + 425: "Too Early", + 426: "Upgrade Required", + 428: "Precondition Required", + 429: "Too Many Requests", + 431: "Request Header Fields Too Large", + 444: "Connection Closed Without Response", + 451: "Unavailable For Legal Reasons", + 499: "Client Closed Request", + 500: "Internal Server Error", + 501: "Not Implemented", + 502: "Bad Gateway", + 503: "Service Unavailable", + 504: "Gateway Timeout", + 505: "HTTP Version Not Supported", + 506: "Variant Also Negotiates", + 507: "Insufficient Storage", + 508: "Loop Detected", + 510: "Not Extended", + 511: "Network Authentication Required", + 599: "Network Connect Timeout Error", + } + + const allStatuses = {...okStatuses, ...emptyStatuses, ...emptyErrorStatuses, ...errorStatuses} + + const allResponseTypes = ["json", "formdata", "arraybuffer", "blob", "text", "document"] + + /** + * @param {object} options + * @param {number} options.status + * @param {string} [options.contentType] + * @param {boolean} [options.contentLength] + * @param {null | Array} options.body + */ + const setupFetch = ({status, headers = {}, contentLength, body}) => { + global.fetch = o.spy(() => { + const encoder = new TextEncoder() + const chunks = body == null ? null : body.map((chunk) => ( + typeof chunk === "string" ? encoder.encode(chunk) : Uint8Array.from(chunk) + )) + if (contentLength) headers["content-length"] = chunks == null ? 0 : chunks.reduce((s, c) => s + c.length, 0) + let i = 0 + return new Response(body == null ? null : new ReadableStream({ + type: "bytes", + pull(ctrl) { + if (i === chunks.length) { + ctrl.close() + } else { + ctrl.enqueue(Uint8Array.from(chunks[i++])) + } + }, + }), {status, statusText: allStatuses[status], headers}) + }) + } + + const bufferToArray = (v) => [...new Uint8Array(v)] + + for (const method of methods) { + for (const status of Object.keys(okStatuses)) { + o.spec(`method ${method}, status ${status}`, () => { + o.spec("arraybuffer, no content length", () => { + o("null body", async () => { + setupFetch({status: Number(status), body: null}) + + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body", async () => { + setupFetch({status: Number(status), body: []}) + + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk", async () => { + setupFetch({status: Number(status), body: [[10]]}) + + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([10]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks", async () => { + setupFetch({status: Number(status), body: [[10], [20]]}) + + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([10, 20]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("null body + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([]) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: []}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([]) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: [[10]]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([10]) + o(reports).deepEquals([[1, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: [[10], [20]]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([10, 20]) + o(reports).deepEquals([[1, -1], [2, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec("arraybuffer, has content length", () => { + o("null body", async () => { + setupFetch({status: Number(status), contentLength: true, body: null}) + + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body", async () => { + setupFetch({status: Number(status), contentLength: true, body: []}) + + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10]]}) + + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([10]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10], [20]]}) + + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([10, 20]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("null body + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: null}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([]) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: []}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([]) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10]]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([10]) + o(reports).deepEquals([[1, 1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10], [20]]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([10, 20]) + o(reports).deepEquals([[1, 2], [2, 2]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec("text, no content length", () => { + o("null body", async () => { + setupFetch({status: Number(status), body: null}) + + const result = await m.fetch("/url", { + method, + responseType: "text", + }) + + o(result).equals("") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body", async () => { + setupFetch({status: Number(status), body: []}) + + const result = await m.fetch("/url", { + method, + responseType: "text", + }) + + o(result).equals("") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk", async () => { + setupFetch({status: Number(status), body: [[10]]}) + + const result = await m.fetch("/url", { + method, + responseType: "text", + }) + + o(result).equals("\x0A") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks", async () => { + setupFetch({status: Number(status), body: [[10], [20]]}) + + const result = await m.fetch("/url", { + method, + responseType: "text", + }) + + o(result).equals("\x0A\x14") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("null body + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "text", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals("") + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: []}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "text", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals("") + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: [[10]]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "text", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals("\x0A") + o(reports).deepEquals([[1, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: [[10], [20]]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "text", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals("\x0A\x14") + o(reports).deepEquals([[1, -1], [2, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec("text, has content length", () => { + o("null body", async () => { + setupFetch({status: Number(status), contentLength: true, body: null}) + + const result = await m.fetch("/url", { + method, + responseType: "text", + }) + + o(result).equals("") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body", async () => { + setupFetch({status: Number(status), contentLength: true, body: []}) + + const result = await m.fetch("/url", { + method, + responseType: "text", + }) + + o(result).equals("") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10]]}) + + const result = await m.fetch("/url", { + method, + responseType: "text", + }) + + o(result).equals("\x0A") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10], [20]]}) + + const result = await m.fetch("/url", { + method, + responseType: "text", + }) + + o(result).equals("\x0A\x14") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("null body + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: null}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "text", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals("") + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: []}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "text", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals("") + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10]]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "text", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals("\x0A") + o(reports).deepEquals([[1, 1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10], [20]]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "text", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals("\x0A\x14") + o(reports).deepEquals([[1, 2], [2, 2]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec("blob, no content length", () => { + o("null body", async () => { + setupFetch({status: Number(status), body: null}) + + const blob = await m.fetch("/url", { + method, + responseType: "blob", + }) + const result = await blob.text() + + o(result).equals("") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body", async () => { + setupFetch({status: Number(status), body: []}) + + const blob = await m.fetch("/url", { + method, + responseType: "blob", + }) + const result = await blob.text() + + o(result).equals("") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk", async () => { + setupFetch({status: Number(status), body: [[10]]}) + + const blob = await m.fetch("/url", { + method, + responseType: "blob", + }) + const result = await blob.text() + + o(result).equals("\x0A") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks", async () => { + setupFetch({status: Number(status), body: [[10], [20]]}) + + const blob = await m.fetch("/url", { + method, + responseType: "blob", + }) + const result = await blob.text() + + o(result).equals("\x0A\x14") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("null body + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const reports = [] + const blob = await m.fetch("/url", { + method, + responseType: "blob", + onprogress: (current, total) => reports.push([current, total]), + }) + const result = await blob.text() + + o(result).equals("") + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: []}) + + const reports = [] + const blob = await m.fetch("/url", { + method, + responseType: "blob", + onprogress: (current, total) => reports.push([current, total]), + }) + const result = await blob.text() + + o(result).equals("") + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: [[10]]}) + + const reports = [] + const blob = await m.fetch("/url", { + method, + responseType: "blob", + onprogress: (current, total) => reports.push([current, total]), + }) + const result = await blob.text() + + o(result).equals("\x0A") + o(reports).deepEquals([[1, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: [[10], [20]]}) + + const reports = [] + const blob = await m.fetch("/url", { + method, + responseType: "blob", + onprogress: (current, total) => reports.push([current, total]), + }) + const result = await blob.text() + + o(result).equals("\x0A\x14") + o(reports).deepEquals([[1, -1], [2, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec("blob, has content length", () => { + o("null body", async () => { + setupFetch({status: Number(status), contentLength: true, body: null}) + + const blob = await m.fetch("/url", { + method, + responseType: "blob", + }) + const result = await blob.text() + + o(result).equals("") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body", async () => { + setupFetch({status: Number(status), contentLength: true, body: []}) + + const blob = await m.fetch("/url", { + method, + responseType: "blob", + }) + const result = await blob.text() + + o(result).equals("") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10]]}) + + const blob = await m.fetch("/url", { + method, + responseType: "blob", + }) + const result = await blob.text() + + o(result).equals("\x0A") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10], [20]]}) + + const blob = await m.fetch("/url", { + method, + responseType: "blob", + }) + const result = await blob.text() + + o(result).equals("\x0A\x14") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("null body + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: null}) + + const reports = [] + const blob = await m.fetch("/url", { + method, + responseType: "blob", + onprogress: (current, total) => reports.push([current, total]), + }) + const result = await blob.text() + + o(result).equals("") + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: []}) + + const reports = [] + const blob = await m.fetch("/url", { + method, + responseType: "blob", + onprogress: (current, total) => reports.push([current, total]), + }) + const result = await blob.text() + + o(result).equals("") + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10]]}) + + const reports = [] + const blob = await m.fetch("/url", { + method, + responseType: "blob", + onprogress: (current, total) => reports.push([current, total]), + }) + const result = await blob.text() + + o(result).equals("\x0A") + o(reports).deepEquals([[1, 1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10], [20]]}) + + const reports = [] + const blob = await m.fetch("/url", { + method, + responseType: "blob", + onprogress: (current, total) => reports.push([current, total]), + }) + const result = await blob.text() + + o(result).equals("\x0A\x14") + o(reports).deepEquals([[1, 2], [2, 2]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec("json, no content length", () => { + o("null body", async () => { + setupFetch({status: Number(status), body: null}) + + let error + try { + await m.fetch("/url", { + method, + responseType: "json", + }) + } catch (e) { + error = e + } + + o(error).notEquals(undefined) + o(error.cause).notEquals(undefined) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body", async () => { + setupFetch({status: Number(status), body: []}) + + let error + try { + await m.fetch("/url", { + method, + responseType: "json", + }) + } catch (e) { + error = e + } + + o(error).notEquals(undefined) + o(error.cause).notEquals(undefined) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk", async () => { + setupFetch({status: Number(status), body: ["123"]}) + + const result = await m.fetch("/url", { + method, + responseType: "json", + }) + + o(result).equals(123) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks", async () => { + setupFetch({status: Number(status), body: ["123", "456"]}) + + const result = await m.fetch("/url", { + method, + responseType: "json", + }) + + o(result).equals(123456) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("null body + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const reports = [] + + let error + try { + await m.fetch("/url", { + method, + responseType: "json", + }) + } catch (e) { + error = e + } + + o(error).notEquals(undefined) + o(error.cause).notEquals(undefined) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: []}) + + const reports = [] + + let error + try { + await m.fetch("/url", { + method, + responseType: "json", + }) + } catch (e) { + error = e + } + + o(error).notEquals(undefined) + o(error.cause).notEquals(undefined) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: ["123"]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "json", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals(123) + o(reports).deepEquals([[3, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: ["123", "456"]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "json", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals(123456) + o(reports).deepEquals([[3, -1], [6, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec("json, has content length", () => { + o("null body", async () => { + setupFetch({status: Number(status), contentLength: true, body: null}) + + let error + try { + await m.fetch("/url", { + method, + responseType: "json", + }) + } catch (e) { + error = e + } + + o(error).notEquals(undefined) + o(error.cause).notEquals(undefined) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body", async () => { + setupFetch({status: Number(status), contentLength: true, body: []}) + + let error + try { + await m.fetch("/url", { + method, + responseType: "json", + }) + } catch (e) { + error = e + } + + o(error).notEquals(undefined) + o(error.cause).notEquals(undefined) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk", async () => { + setupFetch({status: Number(status), contentLength: true, body: ["123"]}) + + const result = await m.fetch("/url", { + method, + responseType: "json", + }) + + o(result).equals(123) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks", async () => { + setupFetch({status: Number(status), contentLength: true, body: ["123", "456"]}) + + const result = await m.fetch("/url", { + method, + responseType: "json", + }) + + o(result).equals(123456) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("null body + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: null}) + + const reports = [] + + let error + try { + await m.fetch("/url", { + method, + responseType: "json", + }) + } catch (e) { + error = e + } + + o(error).notEquals(undefined) + o(error.cause).notEquals(undefined) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: []}) + + const reports = [] + + let error + try { + await m.fetch("/url", { + method, + responseType: "json", + }) + } catch (e) { + error = e + } + + o(error).notEquals(undefined) + o(error.cause).notEquals(undefined) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: ["123"]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "json", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals(123) + o(reports).deepEquals([[3, 3]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: ["123", "456"]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "json", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals(123456) + o(reports).deepEquals([[3, 6], [6, 6]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + if (typeof FormData === "function") { + o.spec("form data", () => { + o("works", async () => { + setupFetch({ + status: Number(status), + headers: { + "content-type": "multipart/form-data; boundary=123456", + }, + contentLength: true, + body: [ + "--123456\r\n", + "Content-Disposition: form-data; name=\"test\"\r\n", + "\r\n", + "value\r\n", + "--123456--\r\n", + ], + }) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "formdata", + onprogress: (current, total) => reports.push([current, total]), + }) + + o([...result]).deepEquals([ + ["test", "value"], + ]) + o(reports).deepEquals([ + [10, 76], + [55, 76], + [57, 76], + [64, 76], + [76, 76], + ]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + } + + if (typeof DOMParser === "function") { + o.spec("document", () => { + o("works without content type", async () => { + setupFetch({ + status: Number(status), + body: ["
"], + }) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "formdata", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result.getElementById("foo")).notEquals(null) + o(reports).deepEquals([[33, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("works with content type text/html", async () => { + setupFetch({ + status: Number(status), + headers: { + "content-type": "text/html", + }, + body: ["
"], + }) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "formdata", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result.getElementById("foo")).notEquals(null) + o(reports).deepEquals([[33, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("works with content type application/xhtml+xml", async () => { + setupFetch({ + status: Number(status), + headers: { + "content-type": "application/xhtml+xml", + }, + body: ['test
'], + }) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "formdata", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result.getElementById("foo")).notEquals(null) + o(reports).deepEquals([[33, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("works with content type application/xml", async () => { + setupFetch({ + status: Number(status), + headers: { + "content-type": "application/xml", + }, + body: ['test
'], + }) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "formdata", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result.getElementById("foo")).notEquals(null) + o(reports).deepEquals([[33, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("works with content type text/xml", async () => { + setupFetch({ + status: Number(status), + headers: { + "content-type": "text/xml", + }, + body: ['test
'], + }) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "formdata", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result.getElementById("foo")).notEquals(null) + o(reports).deepEquals([[33, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("works with content type image/svg+xml", async () => { + setupFetch({ + status: Number(status), + headers: { + "content-type": "image/svg+xml", + }, + body: [''], + }) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "formdata", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result.getElementById("foo")).notEquals(null) + o(reports).deepEquals([[33, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + } + + o.spec("custom extract", () => { + o("works", async () => { + setupFetch({ + status: Number(status), + body: ["123"], + }) + + const reports = [] + const result = await m.fetch("/url", { + method, + onprogress: (current, total) => reports.push([current, total]), + extract: async (response) => `${await response.text()}456`, + }) + + o(result).equals("123456") + o(reports).deepEquals([ + [3, -1], + ]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + }) + } + + for (const status of Object.keys(emptyStatuses)) { + o.spec(`method ${method}, status ${status}`, () => { + o.spec("arraybuffer", () => { + o("no `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("with `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([]) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec("text", () => { + o("no `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const result = await m.fetch("/url", { + method, + responseType: "text", + }) + + o(result).equals("") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("with `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "text", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals("") + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec("blob", () => { + o("no `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const blob = await m.fetch("/url", { + method, + responseType: "blob", + }) + const result = await blob.text() + + o(result).equals("") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("with `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const reports = [] + const blob = await m.fetch("/url", { + method, + responseType: "blob", + onprogress: (current, total) => reports.push([current, total]), + }) + const result = await blob.text() + + o(result).equals("") + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec("json", () => { + o("no `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + let error + try { + await m.fetch("/url", { + method, + responseType: "json", + }) + } catch (e) { + error = e + } + + o(error).notEquals(undefined) + o(error.cause).notEquals(undefined) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("with `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const reports = [] + + let error + try { + await m.fetch("/url", { + method, + responseType: "json", + }) + } catch (e) { + error = e + } + + o(error).notEquals(undefined) + o(error.cause).notEquals(undefined) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec("custom extract", () => { + o("works", async () => { + setupFetch({ + status: Number(status), + body: null, + }) + + const reports = [] + const result = await m.fetch("/url", { + method, + onprogress: (current, total) => reports.push([current, total]), + extract: async (response) => `${await response.text()}456`, + }) + + o(result).equals("456") + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + }) + } + + for (const status of Object.keys(emptyErrorStatuses)) { + o.spec(`method ${method}, status ${status}`, () => { + for (const responseType of allResponseTypes) { + o.spec(responseType, () => { + o("no `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + let error + try { + await m.fetch("/url", { + method, + responseType, + }) + } catch (e) { + error = e + } + + o(error.message).equals(emptyErrorStatuses[status]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("with `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const reports = [] + let error + try { + await m.fetch("/url", { + method, + responseType, + onprogress: (current, total) => reports.push([current, total]), + }) + } catch (e) { + error = e + } + + o(error.message).equals(emptyErrorStatuses[status]) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + } + }) + } + + for (const status of Object.keys(errorStatuses)) { + o.spec(`method ${method}, status ${status}`, () => { + for (const responseType of allResponseTypes) { + o.spec(`${responseType}, no content length`, () => { + o("null body", async () => { + setupFetch({status: Number(status), body: null}) + + let error + try { + await m.fetch("/url", { + method, + responseType, + }) + } catch (e) { + error = e + } + + o(error.message).equals(errorStatuses[status]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body", async () => { + setupFetch({status: Number(status), body: []}) + + let error + try { + await m.fetch("/url", { + method, + responseType, + }) + } catch (e) { + error = e + } + + o(error.message).equals(errorStatuses[status]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk", async () => { + setupFetch({status: Number(status), body: [[10]]}) + + let error + try { + await m.fetch("/url", { + method, + responseType, + }) + } catch (e) { + error = e + } + + o(error.message).equals("\x0A") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks", async () => { + setupFetch({status: Number(status), body: [[10], [20]]}) + + let error + try { + await m.fetch("/url", { + method, + responseType, + }) + } catch (e) { + error = e + } + + o(error.message).equals("\x0A\x14") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("null body + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const reports = [] + let error + try { + await m.fetch("/url", { + method, + responseType, + onprogress: (current, total) => reports.push([current, total]), + }) + } catch (e) { + error = e + } + + o(error.message).equals(errorStatuses[status]) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: []}) + + const reports = [] + let error + try { + await m.fetch("/url", { + method, + responseType, + onprogress: (current, total) => reports.push([current, total]), + }) + } catch (e) { + error = e + } + + o(error.message).equals(errorStatuses[status]) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: [[10]]}) + + const reports = [] + let error + try { + await m.fetch("/url", { + method, + responseType, + onprogress: (current, total) => reports.push([current, total]), + }) + } catch (e) { + error = e + } + + o(error.message).equals("\x0A") + o(reports).deepEquals([[1, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: [[10], [20]]}) + + const reports = [] + let error + try { + await m.fetch("/url", { + method, + responseType, + onprogress: (current, total) => reports.push([current, total]), + }) + } catch (e) { + error = e + } + + o(error.message).equals("\x0A\x14") + o(reports).deepEquals([[1, -1], [2, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec(`${responseType}, has content length`, () => { + o("null body", async () => { + setupFetch({status: Number(status), contentLength: true, body: null}) + + let error + try { + await m.fetch("/url", { + method, + responseType, + }) + } catch (e) { + error = e + } + + o(error.message).equals(errorStatuses[status]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body", async () => { + setupFetch({status: Number(status), contentLength: true, body: []}) + + let error + try { + await m.fetch("/url", { + method, + responseType, + }) + } catch (e) { + error = e + } + + o(error.message).equals(errorStatuses[status]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10]]}) + + let error + try { + await m.fetch("/url", { + method, + responseType, + }) + } catch (e) { + error = e + } + + o(error.message).equals("\x0A") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10], [20]]}) + + let error + try { + await m.fetch("/url", { + method, + responseType, + }) + } catch (e) { + error = e + } + + o(error.message).equals("\x0A\x14") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("null body + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: null}) + + const reports = [] + let error + try { + await m.fetch("/url", { + method, + responseType, + onprogress: (current, total) => reports.push([current, total]), + }) + } catch (e) { + error = e + } + + o(error.message).equals(errorStatuses[status]) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: []}) + + const reports = [] + let error + try { + await m.fetch("/url", { + method, + responseType, + onprogress: (current, total) => reports.push([current, total]), + }) + } catch (e) { + error = e + } + + o(error.message).equals(errorStatuses[status]) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10]]}) + + const reports = [] + let error + try { + await m.fetch("/url", { + method, + responseType, + onprogress: (current, total) => reports.push([current, total]), + }) + } catch (e) { + error = e + } + + o(error.message).equals("\x0A") + o(reports).deepEquals([[1, 1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10], [20]]}) + + const reports = [] + let error + try { + await m.fetch("/url", { + method, + responseType, + onprogress: (current, total) => reports.push([current, total]), + }) + } catch (e) { + error = e + } + + o(error.message).equals("\x0A\x14") + o(reports).deepEquals([[1, 2], [2, 2]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + } + }) + } + } +}) diff --git a/tests/std/init.js b/tests/std/init.js new file mode 100644 index 000000000..413f2b472 --- /dev/null +++ b/tests/std/init.js @@ -0,0 +1,225 @@ +import o from "ospec" + +import {setupGlobals} from "../../test-utils/global.js" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("m.init", () => { + var G = setupGlobals() + + o("works when returning `undefined`", async () => { + var onabort = o.spy() + var initializer = o.spy((signal) => { signal.onabort = onabort; return undefined }) + var redraw = o.spy() + + m.render(G.root, m.init(initializer), {redraw}) + o(initializer.callCount).equals(0) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(0) + + await Promise.resolve() + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(1) + m.render(G.root, m.init(initializer), {redraw}) + + await Promise.resolve() + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(1) + m.render(G.root, null, {redraw}) + + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(1) + o(redraw.callCount).equals(1) + }) + + o("works when resolving to `undefined`", async () => { + var onabort = o.spy() + var initializer = o.spy((signal) => { signal.onabort = onabort; return Promise.resolve(undefined) }) + var redraw = o.spy() + + m.render(G.root, m.init(initializer), {redraw}) + o(initializer.callCount).equals(0) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(0) + + await Promise.resolve() + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(0) + m.render(G.root, m.init(initializer), {redraw}) + + await Promise.resolve() + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(1) + m.render(G.root, null, {redraw}) + + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(1) + o(redraw.callCount).equals(1) + }) + + o("works when returning `null`", async () => { + var onabort = o.spy() + var initializer = o.spy((signal) => { signal.onabort = onabort; return null }) + var redraw = o.spy() + + m.render(G.root, m.init(initializer), {redraw}) + o(initializer.callCount).equals(0) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(0) + + await Promise.resolve() + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(1) + m.render(G.root, m.init(initializer), {redraw}) + + await Promise.resolve() + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(1) + m.render(G.root, null, {redraw}) + + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(1) + o(redraw.callCount).equals(1) + }) + + o("works when resolving to `null`", async () => { + var onabort = o.spy() + var initializer = o.spy((signal) => { signal.onabort = onabort; return Promise.resolve(null) }) + var redraw = o.spy() + + m.render(G.root, m.init(initializer), {redraw}) + o(initializer.callCount).equals(0) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(0) + + await Promise.resolve() + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(0) + m.render(G.root, m.init(initializer), {redraw}) + + await Promise.resolve() + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(1) + m.render(G.root, null, {redraw}) + + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(1) + o(redraw.callCount).equals(1) + }) + + o("works when returning `true`", async () => { + var onabort = o.spy() + var initializer = o.spy((signal) => { signal.onabort = onabort; return true }) + var redraw = o.spy() + + m.render(G.root, m.init(initializer), {redraw}) + o(initializer.callCount).equals(0) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(0) + + await Promise.resolve() + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(1) + m.render(G.root, m.init(initializer), {redraw}) + + await Promise.resolve() + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(1) + m.render(G.root, null, {redraw}) + + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(1) + o(redraw.callCount).equals(1) + }) + + o("works when resolving to `true`", async () => { + var onabort = o.spy() + var initializer = o.spy((signal) => { signal.onabort = onabort; return Promise.resolve(true) }) + var redraw = o.spy() + + m.render(G.root, m.init(initializer), {redraw}) + o(initializer.callCount).equals(0) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(0) + + await Promise.resolve() + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(0) + m.render(G.root, m.init(initializer), {redraw}) + + await Promise.resolve() + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(1) + m.render(G.root, null, {redraw}) + + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(1) + o(redraw.callCount).equals(1) + }) + + o("works when returning `\"skip-redraw\"`", async () => { + var onabort = o.spy() + var initializer = o.spy((signal) => { signal.onabort = onabort; return "skip-redraw" }) + var redraw = o.spy() + + m.render(G.root, m.init(initializer), {redraw}) + o(initializer.callCount).equals(0) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(0) + + await Promise.resolve() + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(0) + m.render(G.root, m.init(initializer), {redraw}) + + await Promise.resolve() + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(0) + m.render(G.root, null, {redraw}) + + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(1) + o(redraw.callCount).equals(0) + }) + + o("works when resolving to `\"skip-redraw\"`", async () => { + var onabort = o.spy() + var initializer = o.spy((signal) => { signal.onabort = onabort; return Promise.resolve("skip-redraw") }) + var redraw = o.spy() + + m.render(G.root, m.init(initializer), {redraw}) + o(initializer.callCount).equals(0) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(0) + + await Promise.resolve() + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(0) + m.render(G.root, m.init(initializer), {redraw}) + + await Promise.resolve() + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(0) + o(redraw.callCount).equals(0) + m.render(G.root, null, {redraw}) + + o(initializer.callCount).equals(1) + o(onabort.callCount).equals(1) + o(redraw.callCount).equals(0) + }) +}) diff --git a/tests/std/lazy.js b/tests/std/lazy.js new file mode 100644 index 000000000..953499fe7 --- /dev/null +++ b/tests/std/lazy.js @@ -0,0 +1,605 @@ +import o from "ospec" + +import {setupGlobals} from "../../test-utils/global.js" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("lazy", () => { + var G = setupGlobals({expectNoConsoleError: true}) + + void [{name: "direct", wrap: (v) => v}, {name: "in module with default", wrap: (v) => ({default:v})}].forEach(({name, wrap}) => { + o.spec(name, () => { + o("works with only fetch and success", async () => { + var calls = [] + var scheduled = 1 + var component = wrap(({name}) => { + calls.push(`view ${name}`) + return m("div", {id: "a"}, "b") + }) + var send, notifyRedrawn + var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) + var redraw = () => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) + } + var C = m.lazy({ + fetch() { + calls.push("fetch") + return new Promise((resolve) => send = resolve) + }, + }) + + o(calls).deepEquals([]) + + m.render(G.root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ], {redraw}) + + o(calls).deepEquals([ + "fetch", + ]) + + send(component) + + await fetchRedrawn + + o(calls).deepEquals([ + "fetch", + "scheduled 1", + ]) + + m.render(G.root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ], {redraw}) + + o(calls).deepEquals([ + "fetch", + "scheduled 1", + "view one", + "view two", + ]) + + m.render(G.root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ], {redraw}) + + o(calls).deepEquals([ + "fetch", + "scheduled 1", + "view one", + "view two", + "view one", + "view two", + ]) + }) + + o("works with only fetch and failure", async () => { + var error = new Error("test") + var calls = [] + console.error = (e) => { + calls.push("console.error", e.message) + } + var scheduled = 1 + var send, notifyRedrawn + var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) + var redraw = () => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) + } + var C = m.lazy({ + fetch() { + calls.push("fetch") + return new Promise((_, reject) => send = reject) + }, + }) + + o(calls).deepEquals([]) + + m.render(G.root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ], {redraw}) + + o(calls).deepEquals([ + "fetch", + ]) + + send(error) + + await fetchRedrawn + + o(calls).deepEquals([ + "fetch", + "console.error", "test", + "scheduled 1", + ]) + + m.render(G.root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ], {redraw}) + + o(calls).deepEquals([ + "fetch", + "console.error", "test", + "scheduled 1", + ]) + + m.render(G.root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ], {redraw}) + + o(calls).deepEquals([ + "fetch", + "console.error", "test", + "scheduled 1", + ]) + }) + + o("works with fetch + pending and success", async () => { + var calls = [] + var scheduled = 1 + var component = wrap(({name}) => { + calls.push(`view ${name}`) + return m("div", {id: "a"}, "b") + }) + var send, notifyRedrawn + var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) + var redraw = () => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) + } + var C = m.lazy({ + fetch() { + calls.push("fetch") + return new Promise((resolve) => send = resolve) + }, + pending() { + calls.push("pending") + }, + }) + + o(calls).deepEquals([]) + + m.render(G.root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ], {redraw}) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + ]) + + send(component) + + await fetchRedrawn + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "scheduled 1", + ]) + + m.render(G.root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ], {redraw}) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "scheduled 1", + "view one", + "view two", + ]) + + m.render(G.root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ], {redraw}) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "scheduled 1", + "view one", + "view two", + "view one", + "view two", + ]) + }) + + o("works with fetch + pending and failure", async () => { + var error = new Error("test") + var calls = [] + console.error = (e) => { + calls.push("console.error", e.message) + } + var scheduled = 1 + var send, notifyRedrawn + var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) + var redraw = () => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) + } + var C = m.lazy({ + fetch() { + calls.push("fetch") + return new Promise((_, reject) => send = reject) + }, + pending() { + calls.push("pending") + }, + }) + + o(calls).deepEquals([]) + + m.render(G.root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ], {redraw}) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + ]) + + send(error) + + await fetchRedrawn + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "console.error", "test", + "scheduled 1", + ]) + + m.render(G.root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ], {redraw}) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "console.error", "test", + "scheduled 1", + ]) + + m.render(G.root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ], {redraw}) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "console.error", "test", + "scheduled 1", + ]) + }) + + o("works with fetch + error and success", async () => { + var calls = [] + var scheduled = 1 + var component = wrap(({name}) => { + calls.push(`view ${name}`) + return m("div", {id: "a"}, "b") + }) + var send, notifyRedrawn + var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) + var redraw = () => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) + } + var C = m.lazy({ + fetch() { + calls.push("fetch") + return new Promise((resolve) => send = resolve) + }, + error() { + calls.push("error") + }, + }) + + o(calls).deepEquals([]) + + m.render(G.root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ], {redraw}) + + o(calls).deepEquals([ + "fetch", + ]) + + send(component) + + await fetchRedrawn + + o(calls).deepEquals([ + "fetch", + "scheduled 1", + ]) + + m.render(G.root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ], {redraw}) + + o(calls).deepEquals([ + "fetch", + "scheduled 1", + "view one", + "view two", + ]) + + m.render(G.root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ], {redraw}) + + o(calls).deepEquals([ + "fetch", + "scheduled 1", + "view one", + "view two", + "view one", + "view two", + ]) + }) + + o("works with fetch + error and failure", async () => { + var error = new Error("test") + var calls = [] + console.error = (e) => { + calls.push("console.error", e.message) + } + var scheduled = 1 + var send, notifyRedrawn + var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) + var redraw = () => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) + } + var C = m.lazy({ + fetch() { + calls.push("fetch") + return new Promise((_, reject) => send = reject) + }, + error(e) { + calls.push("error", e.message) + }, + }) + + o(calls).deepEquals([]) + + m.render(G.root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ], {redraw}) + + o(calls).deepEquals([ + "fetch", + ]) + + send(error) + + await fetchRedrawn + + o(calls).deepEquals([ + "fetch", + "console.error", "test", + "scheduled 1", + ]) + + m.render(G.root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ], {redraw}) + + o(calls).deepEquals([ + "fetch", + "console.error", "test", + "scheduled 1", + "error", "test", + "error", "test", + ]) + + m.render(G.root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ], {redraw}) + + o(calls).deepEquals([ + "fetch", + "console.error", "test", + "scheduled 1", + "error", "test", + "error", "test", + "error", "test", + "error", "test", + ]) + }) + + o("works with all hooks and success", async() => { + var calls = [] + var scheduled = 1 + var component = wrap(({name}) => { + calls.push(`view ${name}`) + return m("div", {id: "a"}, "b") + }) + var send, notifyRedrawn + var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) + var redraw = () => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) + } + var C = m.lazy({ + fetch() { + calls.push("fetch") + return new Promise((resolve) => send = resolve) + }, + pending() { + calls.push("pending") + }, + error() { + calls.push("error") + }, + }) + + o(calls).deepEquals([]) + + m.render(G.root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ], {redraw}) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + ]) + + send(component) + + await fetchRedrawn + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "scheduled 1", + ]) + + m.render(G.root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ], {redraw}) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "scheduled 1", + "view one", + "view two", + ]) + + m.render(G.root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ], {redraw}) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "scheduled 1", + "view one", + "view two", + "view one", + "view two", + ]) + }) + + o("works with all hooks and failure", async () => { + var error = new Error("test") + var calls = [] + console.error = (e) => { + calls.push("console.error", e.message) + } + var scheduled = 1 + var send, notifyRedrawn + var fetchRedrawn = new Promise((resolve) => notifyRedrawn = resolve) + var redraw = () => { + notifyRedrawn() + calls.push(`scheduled ${scheduled++}`) + } + var C = m.lazy({ + fetch() { + calls.push("fetch") + return new Promise((_, reject) => send = reject) + }, + pending() { + calls.push("pending") + }, + error(e) { + calls.push("error", e.message) + }, + }) + + o(calls).deepEquals([]) + + m.render(G.root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ], {redraw}) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + ]) + + send(error) + + await fetchRedrawn + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "console.error", "test", + "scheduled 1", + ]) + + m.render(G.root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ], {redraw}) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "console.error", "test", + "scheduled 1", + "error", "test", + "error", "test", + ]) + + m.render(G.root, [ + m(C, {name: "one"}), + m(C, {name: "two"}), + ], {redraw}) + + o(calls).deepEquals([ + "fetch", + "pending", + "pending", + "console.error", "test", + "scheduled 1", + "error", "test", + "error", "test", + "error", "test", + "error", "test", + ]) + }) + }) + }) +}) diff --git a/tests/std/match.js b/tests/std/match.js new file mode 100644 index 000000000..44686379f --- /dev/null +++ b/tests/std/match.js @@ -0,0 +1,130 @@ +import o from "ospec" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("match", () => { + var match = (path, pattern) => { + var url = new URL(path, "http://localhost/") + return m.match({path: url.pathname, params: url.searchParams}, pattern) + } + + o("checks empty string", function() { + o(match("/", "/")).deepEquals({}) + }) + o("checks identical match", function() { + o(match("/foo", "/foo")).deepEquals({}) + }) + o("checks identical mismatch", function() { + o(match("/bar", "/foo")).deepEquals(undefined) + }) + o("checks single parameter", function() { + o(match("/1", "/:id")).deepEquals({id: "1"}) + }) + o("checks single variadic parameter", function() { + o(match("/some/path", "/*id")).deepEquals({id: "some/path"}) + }) + o("checks single parameter with extra match", function() { + o(match("/1/foo", "/:id/foo")).deepEquals({id: "1"}) + }) + o("checks single parameter with extra mismatch", function() { + o(match("/1/bar", "/:id/foo")).deepEquals(undefined) + }) + o("rejects single variadic parameter with extra match", function() { + o(() => match("/some/path/foo", "/*id/foo")).throws(SyntaxError) + }) + o("checks single variadic parameter with extra mismatch", function() { + o(() => match("/some/path/bar", "/*id2/foo")).throws(SyntaxError) + }) + o("checks multiple parameters", function() { + o(match("/1/2", "/:id/:name")).deepEquals({id: "1", name: "2"}) + }) + o("checks incomplete multiple parameters", function() { + o(match("/1", "/:id/:name")).deepEquals(undefined) + }) + o("checks multiple parameters with extra match", function() { + o(match("/1/2/foo", "/:id/:name/foo")).deepEquals({id: "1", name: "2"}) + }) + o("checks multiple parameters with extra mismatch", function() { + o(match("/1/2/bar", "/:id/:name/foo")).deepEquals(undefined) + }) + o("checks multiple parameters, last variadic, with extra match", function() { + o(() => match("/1/some/path/foo", "/:id/*name/foo")).throws(SyntaxError) + }) + o("checks multiple parameters, last variadic, with extra mismatch", function() { + o(() => match("/1/some/path/bar", "/:id/*name2/foo")).throws(SyntaxError) + }) + o("checks multiple separated parameters", function() { + o(match("/1/sep/2", "/:id/sep/:name")).deepEquals({id: "1", name: "2"}) + }) + o("checks incomplete multiple separated parameters", function() { + o(match("/1", "/:id/sep/:name")).deepEquals(undefined) + o(match("/1/sep", "/:id/sep/:name")).deepEquals(undefined) + }) + o("checks multiple separated parameters missing sep", function() { + o(match("/1/2", "/:id/sep/:name")).deepEquals(undefined) + }) + o("checks multiple separated parameters with extra match", function() { + o(match("/1/sep/2/foo", "/:id/sep/:name/foo")).deepEquals({id: "1", name: "2"}) + }) + o("checks multiple separated parameters with extra mismatch", function() { + o(match("/1/sep/2/bar", "/:id/sep/:name/foo")).deepEquals(undefined) + }) + o("checks multiple separated parameters, last variadic, with extra match", function() { + o(() => match("/1/sep/some/path/foo", "/:id/sep/*name/foo")).throws(SyntaxError) + }) + o("checks multiple separated parameters, last variadic, with extra mismatch", function() { + o(() => match("/1/sep/some/path/bar", "/:id/sep/*name2/foo")).throws(SyntaxError) + }) + o("checks multiple parameters + prefix", function() { + o(match("/route/1/2", "/route/:id/:name")).deepEquals({id: "1", name: "2"}) + }) + o("checks incomplete multiple parameters + prefix", function() { + o(match("/route/1", "/route/:id/:name")).deepEquals(undefined) + }) + o("checks multiple parameters + prefix with extra match", function() { + o(match("/route/1/2/foo", "/route/:id/:name/foo")).deepEquals({id: "1", name: "2"}) + }) + o("checks multiple parameters + prefix with extra mismatch", function() { + o(match("/route/1/2/bar", "/route/:id/:name/foo")).deepEquals(undefined) + }) + o("checks multiple parameters + prefix, last variadic, with extra match", function() { + o(() => match("/route/1/some/path/foo", "/route/:id/*name/foo")).throws(SyntaxError) + }) + o("checks multiple parameters + prefix, last variadic, with extra mismatch", function() { + o(() => match("/route/1/some/path/bar", "/route/:id/*name/foo")).throws(SyntaxError) + }) + o("checks multiple separated parameters + prefix", function() { + o(match("/route/1/sep/2", "/route/:id/sep/:name")).deepEquals({id: "1", name: "2"}) + }) + o("checks incomplete multiple separated parameters + prefix", function() { + o(match("/route/1", "/route/:id/sep/:name")).deepEquals(undefined) + o(match("/route/1/sep", "/route/:id/sep/:name")).deepEquals(undefined) + }) + o("checks multiple separated parameters + prefix missing sep", function() { + o(match("/route/1/2", "/route/:id/sep/:name")).deepEquals(undefined) + }) + o("checks multiple separated parameters + prefix with extra match", function() { + o(match("/route/1/sep/2/foo", "/route/:id/sep/:name/foo")).deepEquals({id: "1", name: "2"}) + }) + o("checks multiple separated parameters + prefix with extra mismatch", function() { + o(match("/route/1/sep/2/bar", "/route/:id/sep/:name/foo")).deepEquals(undefined) + }) + o("checks multiple separated parameters + prefix, last variadic, with extra match", function() { + o(() => match("/route/1/sep/some/path/foo", "/route/:id/sep/*name/foo")).throws(SyntaxError) + }) + o("checks multiple separated parameters + prefix, last variadic, with extra mismatch", function() { + o(() => match("/route/1/sep/some/path/bar", "/route/:id/sep/*name2/foo")).throws(SyntaxError) + }) + o("checks dot before dot", function() { + o(match("/file.test.png/edit", "/:file.:ext/edit")).deepEquals({file: "file.test", ext: "png"}) + }) + o("checks dash before dot", function() { + o(match("/file-test.png/edit", "/:file.:ext/edit")).deepEquals({file: "file-test", ext: "png"}) + }) + o("checks dot before dash", function() { + o(match("/file.test-png/edit", "/:file-:ext/edit")).deepEquals({file: "file.test", ext: "png"}) + }) + o("checks dash before dash", function() { + o(match("/file-test-png/edit", "/:file-:ext/edit")).deepEquals({file: "file-test", ext: "png"}) + }) +}) diff --git a/tests/std/p.js b/tests/std/p.js new file mode 100644 index 000000000..c1b65b7f5 --- /dev/null +++ b/tests/std/p.js @@ -0,0 +1,233 @@ +import o from "ospec" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("p", () => { + function test(prefix) { + o("returns path if no params", () => { + var string = m.p(`${prefix}/route/foo`, undefined) + + o(string).equals(`${prefix}/route/foo`) + }) + o("skips interpolation if no params", () => { + var string = m.p(`${prefix}/route/:id`, undefined) + + o(string).equals(`${prefix}/route/:id`) + }) + o("appends query strings", () => { + var string = m.p(`${prefix}/route/foo`, {a: "b", c: 1}) + + o(string).equals(`${prefix}/route/foo?a=b&c=1`) + }) + o("inserts template parameters at end", () => { + var string = m.p(`${prefix}/route/:id`, {id: "1"}) + + o(string).equals(`${prefix}/route/1`) + }) + o("inserts template parameters at beginning", () => { + var string = m.p(`${prefix}/:id/foo`, {id: "1"}) + + o(string).equals(`${prefix}/1/foo`) + }) + o("inserts template parameters at middle", () => { + var string = m.p(`${prefix}/route/:id/foo`, {id: "1"}) + + o(string).equals(`${prefix}/route/1/foo`) + }) + o("inserts non-special escapes", () => { + var string = m.p(`${prefix}/route/\\a`) + + o(string).equals(`${prefix}/route/a`) + }) + o("inserts normal interpolation escapes without parameters", () => { + var string = m.p(`${prefix}/route/\\:id/foo`) + + o(string).equals(`${prefix}/route/:id/foo`) + }) + o("inserts raw interpolation escapes without parameters", () => { + var string = m.p(`${prefix}/route/\\*foo`) + + o(string).equals(`${prefix}/route/*foo`) + }) + o("inserts normal interpolation escapes", () => { + var string = m.p(`${prefix}/route/\\:id/foo`, {id: "1"}) + + o(string).equals(`${prefix}/route/:id/foo?id=1`) + }) + o("inserts raw interpolation escapes", () => { + var string = m.p(`${prefix}/route/\\*foo`, {foo: "id/1"}) + + o(string).equals(`${prefix}/route/*foo?foo=id%2F1`) + }) + o("inserts variadic paths", () => { + var string = m.p(`${prefix}/route/*foo`, {foo: "id/1"}) + + o(string).equals(`${prefix}/route/id/1`) + }) + o("inserts variadic paths with initial slashes", () => { + var string = m.p(`${prefix}/route/*foo`, {foo: "/id/1"}) + + o(string).equals(`${prefix}/route//id/1`) + }) + o("skips template parameters at end if param missing", () => { + var string = m.p(`${prefix}/route/:id`, {param: 1}) + + o(string).equals(`${prefix}/route/:id?param=1`) + }) + o("skips template parameters at beginning if param missing", () => { + var string = m.p(`${prefix}/:id/foo`, {param: 1}) + + o(string).equals(`${prefix}/:id/foo?param=1`) + }) + o("skips template parameters at middle if param missing", () => { + var string = m.p(`${prefix}/route/:id/foo`, {param: 1}) + + o(string).equals(`${prefix}/route/:id/foo?param=1`) + }) + o("skips variadic template parameters if param missing", () => { + var string = m.p(`${prefix}/route/*foo`, {param: "/id/1"}) + + o(string).equals(`${prefix}/route/*foo?param=%2Fid%2F1`) + }) + o("handles escaped values", () => { + var data = m.p(`${prefix}/route/:foo`, {"foo": ";:@&=+$,/?%#"}) + + o(data).equals(`${prefix}/route/%3B%3A%40%26%3D%2B%24%2C%2F%3F%25%23`) + }) + o("handles unicode", () => { + var data = m.p(`${prefix}/route/:ΓΆ`, {"ΓΆ": "ΓΆ"}) + + o(data).equals(`${prefix}/route/%C3%B6`) + }) + o("handles zero", () => { + var string = m.p(`${prefix}/route/:a`, {a: 0}) + + o(string).equals(`${prefix}/route/0`) + }) + o("handles false", () => { + var string = m.p(`${prefix}/route/:a`, {a: false}) + + o(string).equals(`${prefix}/route/false`) + }) + o("handles dashes", () => { + var string = m.p(`${prefix}/:lang-:region/route`, { + lang: "en", + region: "US" + }) + + o(string).equals(`${prefix}/en-US/route`) + }) + o("handles dots", () => { + var string = m.p(`${prefix}/:file.:ext/view`, { + file: "image", + ext: "png" + }) + + o(string).equals(`${prefix}/image.png/view`) + }) + o("merges query strings", () => { + var string = m.p(`${prefix}/item?a=1&b=2`, {c: 3}) + + o(string).equals(`${prefix}/item?a=1&b=2&c=3`) + }) + o("merges query strings with other parameters", () => { + var string = m.p(`${prefix}/item/:id?a=1&b=2`, {id: "foo", c: 3}) + + o(string).equals(`${prefix}/item/foo?a=1&b=2&c=3`) + }) + o("consumes template parameters without modifying query string", () => { + var string = m.p(`${prefix}/item/:id?a=1&b=2`, {id: "foo"}) + + o(string).equals(`${prefix}/item/foo?a=1&b=2`) + }) + o("handles flat object in query string", () => { + var string = m.p(prefix, {a: "b", c: 1}) + + o(string).equals(`${prefix}?a=b&c=1`) + }) + o("handles escaped values in query string", () => { + var data = m.p(prefix, {";:@&=+$,/?%#": ";:@&=+$,/?%#"}) + + o(data).equals(`${prefix}?%3B%3A%40%26%3D%2B%24%2C%2F%3F%25%23=%3B%3A%40%26%3D%2B%24%2C%2F%3F%25%23`) + }) + o("handles unicode in query string", () => { + var data = m.p(prefix, {"ΓΆ": "ΓΆ"}) + + o(data).equals(`${prefix}?%C3%B6=%C3%B6`) + }) + o("handles nested object in query string", () => { + var string = m.p(prefix, {a: {b: 1, c: 2}}) + + o(string).equals(`${prefix}?a%5Bb%5D=1&a%5Bc%5D=2`) + }) + o("handles deep nested object in query string", () => { + var string = m.p(prefix, {a: {b: {c: 1, d: 2}}}) + + o(string).equals(`${prefix}?a%5Bb%5D%5Bc%5D=1&a%5Bb%5D%5Bd%5D=2`) + }) + o("handles nested array in query string", () => { + var string = m.p(prefix, {a: ["x", "y"]}) + + o(string).equals(`${prefix}?a%5B%5D=x&a%5B%5D=y`) + }) + o("handles array w/ dupe values in query string", () => { + var string = m.p(prefix, {a: ["x", "x"]}) + + o(string).equals(`${prefix}?a%5B%5D=x&a%5B%5D=x`) + }) + o("handles deep nested array in query string", () => { + var string = m.p(prefix, {a: [["x", "y"]]}) + + o(string).equals(`${prefix}?a%5B%5D%5B%5D=x&a%5B%5D%5B%5D=y`) + }) + o("handles deep nested array in object in query string", () => { + var string = m.p(prefix, {a: {b: ["x", "y"]}}) + + o(string).equals(`${prefix}?a%5Bb%5D%5B%5D=x&a%5Bb%5D%5B%5D=y`) + }) + o("handles deep nested object in array in query string", () => { + var string = m.p(prefix, {a: [{b: 1, c: 2}]}) + + o(string).equals(`${prefix}?a%5B%5D%5Bb%5D=1&a%5B%5D%5Bc%5D=2`) + }) + o("handles date in query string", () => { + var string = m.p(prefix, {a: new Date(0)}) + + o(string).equals(`${prefix}?a=${encodeURIComponent(new Date(0).toString())}`) + }) + o("handles zero in query string", () => { + var string = m.p(prefix, {a: 0}) + + o(string).equals(`${prefix}?a=0`) + }) + o("retains empty string literally", () => { + var string = m.p(prefix, {a: ""}) + + o(string).equals(`${prefix}?a=`) + }) + o("drops `null` from query string", () => { + var string = m.p(prefix, {a: null}) + + o(string).equals(prefix) + }) + o("drops `undefined` from query string", () => { + var string = m.p(prefix, {a: undefined}) + + o(string).equals(prefix) + }) + o("turns `true` into value-less string in query string", () => { + var string = m.p(prefix, {a: true}) + + o(string).equals(`${prefix}?a`) + }) + o("drops `false` from query string", () => { + var string = m.p(prefix, {a: false}) + + o(string).equals(prefix) + }) + } + o.spec("absolute", () => { test("") }) + o.spec("relative", () => { test("..") }) + o.spec("absolute + domain", () => { test("https://example.com") }) + o.spec("absolute + `file:`", () => { test("file://") }) +}) diff --git a/tests/std/query.js b/tests/std/query.js new file mode 100644 index 000000000..f7a507f13 --- /dev/null +++ b/tests/std/query.js @@ -0,0 +1,91 @@ +import o from "ospec" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("query", () => { + o("handles flat object", () => { + var string = m.query({a: "b", c: 1}) + + o(string).equals("a=b&c=1") + }) + o("handles escaped values", () => { + var data = m.query({";:@&=+$,/?%#": ";:@&=+$,/?%#"}) + + o(data).equals("%3B%3A%40%26%3D%2B%24%2C%2F%3F%25%23=%3B%3A%40%26%3D%2B%24%2C%2F%3F%25%23") + }) + o("handles unicode", () => { + var data = m.query({"ΓΆ": "ΓΆ"}) + + o(data).equals("%C3%B6=%C3%B6") + }) + o("handles nested object in query string", () => { + var string = m.query({a: {b: 1, c: 2}}) + + o(string).equals("a%5Bb%5D=1&a%5Bc%5D=2") + }) + o("handles deep nested object in query string", () => { + var string = m.query({a: {b: {c: 1, d: 2}}}) + + o(string).equals("a%5Bb%5D%5Bc%5D=1&a%5Bb%5D%5Bd%5D=2") + }) + o("handles nested array in query string", () => { + var string = m.query({a: ["x", "y"]}) + + o(string).equals("a%5B%5D=x&a%5B%5D=y") + }) + o("handles array w/ dupe values in query string", () => { + var string = m.query({a: ["x", "x"]}) + + o(string).equals("a%5B%5D=x&a%5B%5D=x") + }) + o("handles deep nested array in query string", () => { + var string = m.query({a: [["x", "y"]]}) + + o(string).equals("a%5B%5D%5B%5D=x&a%5B%5D%5B%5D=y") + }) + o("handles deep nested array in object in query string", () => { + var string = m.query({a: {b: ["x", "y"]}}) + + o(string).equals("a%5Bb%5D%5B%5D=x&a%5Bb%5D%5B%5D=y") + }) + o("handles deep nested object in array in query string", () => { + var string = m.query({a: [{b: 1, c: 2}]}) + + o(string).equals("a%5B%5D%5Bb%5D=1&a%5B%5D%5Bc%5D=2") + }) + o("handles date in query string", () => { + var string = m.query({a: new Date(0)}) + + o(string).equals(`a=${encodeURIComponent(new Date(0).toString())}`) + }) + o("handles zero in query string", () => { + var string = m.query({a: 0}) + + o(string).equals("a=0") + }) + o("retains empty string literally", () => { + var string = m.query({a: ""}) + + o(string).equals("a=") + }) + o("drops `null` from query string", () => { + var string = m.query({a: null}) + + o(string).equals("") + }) + o("drops `undefined` from query string", () => { + var string = m.query({a: undefined}) + + o(string).equals("") + }) + o("turns `true` into value-less string in query string", () => { + var string = m.query({a: true}) + + o(string).equals("a") + }) + o("drops `false` from query string", () => { + var string = m.query({a: false}) + + o(string).equals("") + }) +}) diff --git a/tests/std/router.js b/tests/std/router.js new file mode 100644 index 000000000..d7c8da359 --- /dev/null +++ b/tests/std/router.js @@ -0,0 +1,475 @@ +import o from "ospec" + +import {setupGlobals} from "../../test-utils/global.js" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("route", () => { + void [{protocol: "http:", hostname: "localhost"}, {protocol: "file:", hostname: "/"}, {protocol: "http:", hostname: "ΓΆΓΆΓΆ"}].forEach((env) => { + void ["#", "?", "", "#!", "?!", "/foo", "/fΓΆΓΆ"].forEach((prefix) => { + o.spec(`using prefix \`${prefix}\` starting on ${env.protocol}//${env.hostname}`, () => { + var fullHost = `${env.protocol}//${env.hostname === "/" ? "" : env.hostname}` + var fullPrefix = `${fullHost}${prefix[0] === "/" ? "" : "/"}${prefix ? `${prefix}/` : ""}` + + var G = setupGlobals({...env, expectNoConsoleError: true}) + + o("returns the right route on init", () => { + G.window.location.href = `${prefix}/` + + var App = o.spy() + var redraw = o.spy() + + m.render(G.root, m.route(prefix, App), {redraw}) + + o(App.callCount).equals(1) + o(App.this.route.path).equals("/") + o([...App.this.route.params]).deepEquals([]) + o(G.rafMock.queueLength()).equals(0) + o(redraw.callCount).equals(0) + }) + + o("returns alternate right route on init", () => { + G.window.location.href = `${prefix}/test` + + var App = o.spy() + var redraw = o.spy() + + m.render(G.root, m.route(prefix, App), {redraw}) + + o(App.callCount).equals(1) + o(App.this.route.path).equals("/test") + o([...App.this.route.params]).deepEquals([]) + o(G.rafMock.queueLength()).equals(0) + o(redraw.callCount).equals(0) + }) + + o("returns right route on init with escaped unicode", () => { + G.window.location.href = `${prefix}/%C3%B6?%C3%B6=%C3%B6` + + var App = o.spy() + var redraw = o.spy() + + m.render(G.root, m.route(prefix, App), {redraw}) + + o(App.callCount).equals(1) + o(App.this.route.path).equals("/ΓΆ") + o([...App.this.route.params]).deepEquals([["ΓΆ", "ΓΆ"]]) + o(G.rafMock.queueLength()).equals(0) + o(redraw.callCount).equals(0) + }) + + o("returns right route on init with unescaped unicode", () => { + G.window.location.href = `${prefix}/ΓΆ?ΓΆ=ΓΆ` + + var App = o.spy() + var redraw = o.spy() + + m.render(G.root, m.route(prefix, App), {redraw}) + + o(App.callCount).equals(1) + o(App.this.route.path).equals("/ΓΆ") + o([...App.this.route.params]).deepEquals([["ΓΆ", "ΓΆ"]]) + o(G.rafMock.queueLength()).equals(0) + o(redraw.callCount).equals(0) + }) + + o("sets path asynchronously", async () => { + G.window.location.href = `${prefix}/a` + var spy1 = o.spy() + var spy2 = o.spy() + var route + + var App = function () { + route = this.route + if (this.route.path === "/a") { + spy1() + } else if (this.route.path === "/b") { + spy2() + } else { + throw new Error(`Unknown path ${route.path}`) + } + } + + m.mount(G.root, () => m.route(prefix, App)) + + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(0) + route.set("/b") + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(0) + + await Promise.resolve() + G.rafMock.fire() + + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(1) + o(G.rafMock.queueLength()).equals(0) + }) + + o("sets route via pushState/onpopstate", async () => { + G.window.location.href = `${prefix}/test` + + var route + var App = function () { + route = this.route + } + + m.mount(G.root, () => m.route(prefix, App)) + + await Promise.resolve() + G.rafMock.fire() + + G.window.history.pushState(null, null, `${prefix}/other/x/y/z?c=d#e=f`) + G.window.onpopstate() + + await Promise.resolve() + G.rafMock.fire() + + // Yep, before even the throttle mechanism takes hold. + o(route.current).equals("/other/x/y/z?c=d#e=f") + + await Promise.resolve() + G.rafMock.fire() + + o(G.rafMock.queueLength()).equals(0) + }) + + o("`replace: true` works", async () => { + G.window.location.href = `${prefix}/test` + + var route + var App = function () { + route = this.route + } + + m.mount(G.root, () => m.route(prefix, App)) + + route.set("/other", {replace: true}) + + await Promise.resolve() + G.rafMock.fire() + + G.window.history.back() + o(G.window.location.href).equals(`${fullHost}/`) + + await Promise.resolve() + G.rafMock.fire() + + o(G.window.location.href).equals(`${fullHost}/`) + o(G.rafMock.queueLength()).equals(0) + }) + + o("`replace: true` works in links", async () => { + G.window.location.href = `${prefix}/test` + + var e = G.window.document.createEvent("MouseEvents") + + e.initEvent("click", true, true) + e.button = 0 + + var App = function () { + if (this.route.path === "/test") { + return m("a", m.link("/other", {replace: true})) + } else if (this.route.path === "/other") { + return m("div") + } else if (this.route.path === "/") { + return m("span") + } else { + throw new Error(`Unknown route: ${this.route.path}`) + } + } + + m.mount(G.root, () => m.route(prefix, App)) + + G.root.firstChild.dispatchEvent(e) + + await Promise.resolve() + G.rafMock.fire() + + G.window.history.back() + o(G.window.location.href).equals(`${fullHost}/`) + + await Promise.resolve() + G.rafMock.fire() + + o(G.window.location.href).equals(`${fullHost}/`) + o(G.rafMock.queueLength()).equals(0) + }) + + o("`replace: false` works", async () => { + G.window.location.href = `${prefix}/test` + + var route + var App = function () { + route = this.route + } + + m.mount(G.root, () => m.route(prefix, App)) + + route.set("/other", {replace: false}) + + await Promise.resolve() + G.rafMock.fire() + + G.window.history.back() + o(G.window.location.href).equals(`${fullPrefix}test`) + + await Promise.resolve() + G.rafMock.fire() + + o(G.window.location.href).equals(`${fullPrefix}test`) + o(G.rafMock.queueLength()).equals(0) + }) + + o("`replace: false` works in links", async () => { + G.window.location.href = `${prefix}/test` + + var e = G.window.document.createEvent("MouseEvents") + + e.initEvent("click", true, true) + e.button = 0 + + var App = function () { + if (this.route.path === "/test") { + return m("a", m.link("/other", {replace: false})) + } else if (this.route.path === "/other") { + return m("div") + } else { + throw new Error(`Unknown route: ${this.route.path}`) + } + } + + m.mount(G.root, () => m.route(prefix, App)) + + G.root.firstChild.dispatchEvent(e) + + await Promise.resolve() + G.rafMock.fire() + + G.window.history.back() + o(G.window.location.href).equals(`${fullPrefix}test`) + + await Promise.resolve() + G.rafMock.fire() + + o(G.window.location.href).equals(`${fullPrefix}test`) + o(G.rafMock.queueLength()).equals(0) + }) + + o("state works", async () => { + G.window.location.href = `${prefix}/test` + + var route + var App = function () { + route = this.route + } + + m.mount(G.root, () => m.route(prefix, App)) + + route.set("/other", {state: {a: 1}}) + + await Promise.resolve() + G.rafMock.fire() + + o(G.window.history.state).deepEquals({a: 1}) + o(G.rafMock.queueLength()).equals(0) + }) + + o("adds trailing slash where needed", () => { + G.window.location.href = `${prefix}/test` + + var route + var App = function () { + route = this.route + } + + m.mount(G.root, () => m.route(`${prefix}/`, App)) + + o(route.path).equals("/test") + o([...route.params]).deepEquals([]) + o(G.rafMock.queueLength()).equals(0) + }) + + o("handles route with search", () => { + G.window.location.href = `${prefix}/test?a=b&c=d` + + var route + var App = function () { + route = this.route + } + + m.mount(G.root, () => m.route(prefix, App)) + + o(route.path).equals("/test") + o([...route.params]).deepEquals([["a", "b"], ["c", "d"]]) + o(G.rafMock.queueLength()).equals(0) + }) + + o("reacts to back button", () => { + G.window.location.href = "http://old.com" + G.window.location.href = "http://new.com" + + var App = () => {} + + m.mount(G.root, () => m.route(prefix, App)) + + G.window.history.back() + + o(G.window.location.pathname).equals("/") + o(G.window.location.hostname).equals("old.com") + o(G.rafMock.queueLength()).equals(0) + }) + + o("changes location on route.Link", async () => { + var e = G.window.document.createEvent("MouseEvents") + + e.initEvent("click", true, true) + e.button = 0 + + G.window.location.href = `${prefix}/` + + var App = function () { + if (this.route.path === "/") { + return m("a", m.link("/test")) + } else if (this.route.path === "/test") { + return m("div") + } else { + throw new Error(`Unknown route: ${this.route.path}`) + } + } + + m.mount(G.root, () => m.route(prefix, App)) + + o(G.window.location.href).equals(fullPrefix) + + G.root.firstChild.dispatchEvent(e) + + await Promise.resolve() + G.rafMock.fire() + + o(G.window.location.href).equals(`${fullPrefix}test`) + o(G.rafMock.queueLength()).equals(0) + }) + + o("passes state on route.Link", async () => { + var e = G.window.document.createEvent("MouseEvents") + + e.initEvent("click", true, true) + e.button = 0 + G.window.location.href = `${prefix}/` + + var App = function () { + if (this.route.path === "/") { + return m("a", m.link("/test", {state: {a: 1}})) + } else if (this.route.path === "/test") { + return m("div") + } else { + throw new Error(`Unknown route: ${this.route.path}`) + } + } + + m.mount(G.root, () => m.route(prefix, App)) + + G.root.firstChild.dispatchEvent(e) + + await Promise.resolve() + G.rafMock.fire() + + o(G.window.history.state).deepEquals({a: 1}) + o(G.rafMock.queueLength()).equals(0) + }) + + o("route.Link doesn't redraw on wrong button", async () => { + var e = G.window.document.createEvent("MouseEvents") + + e.initEvent("click", true, true) + e.button = 10 + + G.window.location.href = `${prefix}/` + + var App = function () { + if (this.route.path === "/") { + return m("a", m.link("/test")) + } else if (this.route.path === "/test") { + return m("div") + } else { + throw new Error(`Unknown route: ${this.route.path}`) + } + } + + m.mount(G.root, () => m.route(prefix, App)) + + o(G.window.location.href).equals(fullPrefix) + + G.root.firstChild.dispatchEvent(e) + + await Promise.resolve() + G.rafMock.fire() + + o(G.window.location.href).equals(fullPrefix) + o(G.rafMock.queueLength()).equals(0) + }) + + o("route.Link doesn't redraw on preventDefault", async () => { + var e = G.window.document.createEvent("MouseEvents") + + e.initEvent("click", true, true) + e.button = 0 + + G.window.location.href = `${prefix}/` + + var App = function () { + if (this.route.path === "/") { + return m("a", {onclick(e) { e.preventDefault() }}, m.link("/test")) + } else if (this.route.path === "/test") { + return m("div") + } else { + throw new Error(`Unknown route: ${this.route.path}`) + } + } + + m.mount(G.root, () => m.route(prefix, App)) + + o(G.window.location.href).equals(fullPrefix) + + G.root.firstChild.dispatchEvent(e) + + await Promise.resolve() + G.rafMock.fire() + + o(G.window.location.href).equals(fullPrefix) + o(G.rafMock.queueLength()).equals(0) + }) + + o("`route.set(m.route.current)` re-runs the resolution logic (#1180)", async () => { + G.window.location.href = `${prefix}/` + + var route + var App = o.spy(function () { + route = this.route + return m("div") + }) + + m.mount(G.root, () => m.route(prefix, App)) + + o(App.callCount).equals(1) + + await Promise.resolve() + G.rafMock.fire() + + o(App.callCount).equals(1) + + route.set(route.current) + + await Promise.resolve() + G.rafMock.fire() + await Promise.resolve() + G.rafMock.fire() + + o(App.callCount).equals(2) + o(G.rafMock.queueLength()).equals(0) + }) + }) + }) + }) +}) diff --git a/stream/tests/test-scan.js b/tests/std/stream/scan.js similarity index 94% rename from stream/tests/test-scan.js rename to tests/std/stream/scan.js index 5940c3d17..b48678276 100644 --- a/stream/tests/test-scan.js +++ b/tests/std/stream/scan.js @@ -1,7 +1,6 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var stream = require("../stream") +import stream from "../../../src/entry/stream.esm.js" o.spec("scan", function() { o("defaults to seed", function() { diff --git a/stream/tests/test-scanMerge.js b/tests/std/stream/scanMerge.js similarity index 89% rename from stream/tests/test-scanMerge.js rename to tests/std/stream/scanMerge.js index 0a441f73d..652e492e5 100644 --- a/stream/tests/test-scanMerge.js +++ b/tests/std/stream/scanMerge.js @@ -1,7 +1,6 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var stream = require("../stream") +import stream from "../../../src/entry/stream.esm.js" o.spec("scanMerge", function() { o("defaults to seed", function() { diff --git a/stream/tests/test-stream.js b/tests/std/stream/stream.js similarity index 87% rename from stream/tests/test-stream.js rename to tests/std/stream/stream.js index 1b28c2127..360e59e2e 100644 --- a/stream/tests/test-stream.js +++ b/tests/std/stream/stream.js @@ -1,7 +1,6 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var Stream = require("../stream") +import Stream from "../../../src/entry/stream.esm.js" o.spec("stream", function() { o.spec("stream", function() { @@ -42,31 +41,11 @@ o.spec("stream", function() { o(b()).equals(2) }) - // NOTE: this *must* be the *only* uses of `Stream.HALT` in the entire - // test suite. - o("HALT is a deprecated alias of SKIP and warns once", function() { - var log = console.log - var warnings = [] - console.log = function(a) { - warnings.push(a) - } - - try { - o(Stream.HALT).equals(Stream.SKIP) - o(warnings).deepEquals(["HALT is deprecated and has been renamed to SKIP"]) - o(Stream.HALT).equals(Stream.SKIP) - o(warnings).deepEquals(["HALT is deprecated and has been renamed to SKIP"]) - o(Stream.HALT).equals(Stream.SKIP) - o(warnings).deepEquals(["HALT is deprecated and has been renamed to SKIP"]) - } finally { - console.log = log - } - }) }) o.spec("combine", function() { o("transforms value", function() { var stream = Stream() - var doubled = Stream.combine(function(s) {return s() * 2}, [stream]) + var doubled = Stream.combine(function() {return stream() * 2}, [stream]) stream(2) @@ -74,14 +53,14 @@ o.spec("stream", function() { }) o("transforms default value", function() { var stream = Stream(2) - var doubled = Stream.combine(function(s) {return s() * 2}, [stream]) + var doubled = Stream.combine(function() {return stream() * 2}, [stream]) o(doubled()).equals(4) }) o("transforms multiple values", function() { var s1 = Stream() var s2 = Stream() - var added = Stream.combine(function(s1, s2) {return s1() + s2()}, [s1, s2]) + var added = Stream.combine(function() {return s1() + s2()}, [s1, s2]) s1(2) s2(3) @@ -91,14 +70,14 @@ o.spec("stream", function() { o("transforms multiple default values", function() { var s1 = Stream(2) var s2 = Stream(3) - var added = Stream.combine(function(s1, s2) {return s1() + s2()}, [s1, s2]) + var added = Stream.combine(function() {return s1() + s2()}, [s1, s2]) o(added()).equals(5) }) o("transforms mixed default and late-bound values", function() { var s1 = Stream(2) var s2 = Stream() - var added = Stream.combine(function(s1, s2) {return s1() + s2()}, [s1, s2]) + var added = Stream.combine(function() {return s1() + s2()}, [s1, s2]) s2(3) @@ -107,9 +86,9 @@ o.spec("stream", function() { o("combines atomically", function() { var count = 0 var a = Stream() - var b = Stream.combine(function(a) {return a() * 2}, [a]) - var c = Stream.combine(function(a) {return a() * a()}, [a]) - var d = Stream.combine(function(b, c) { + var b = Stream.combine(function() {return a() * 2}, [a]) + var c = Stream.combine(function() {return a() * a()}, [a]) + var d = Stream.combine(function() { count++ return b() + c() }, [b, c]) @@ -123,9 +102,9 @@ o.spec("stream", function() { o("combines default value atomically", function() { var count = 0 var a = Stream(3) - var b = Stream.combine(function(a) {return a() * 2}, [a]) - var c = Stream.combine(function(a) {return a() * a()}, [a]) - var d = Stream.combine(function(b, c) { + var b = Stream.combine(function() {return a() * 2}, [a]) + var c = Stream.combine(function() {return a() * a()}, [a]) + var d = Stream.combine(function() { count++ return b() + c() }, [b, c]) @@ -136,11 +115,11 @@ o.spec("stream", function() { o("combines and maps nested streams atomically", function() { var count = 0 var a = Stream(3) - var b = Stream.combine(function(a) {return a() * 2}, [a]) - var c = Stream.combine(function(a) {return a() * a()}, [a]) + var b = Stream.combine(function() {return a() * 2}, [a]) + var c = Stream.combine(function() {return a() * a()}, [a]) var d = c.map(function(x){return x}) - var e = Stream.combine(function(x) {return x()}, [d]) - var f = Stream.combine(function(b, e) { + var e = Stream.combine(function() {return d()}, [d]) + var f = Stream.combine(function() { count++ return b() + e() }, [b, e]) @@ -152,7 +131,7 @@ o.spec("stream", function() { var streams = [] var a = Stream() var b = Stream() - Stream.combine(function(a, b, changed) { + Stream.combine(function(changed) { streams = changed }, [a, b]) @@ -166,7 +145,7 @@ o.spec("stream", function() { o("combine continues with ended streams", function() { var a = Stream() var b = Stream() - var combined = Stream.combine(function(a, b) { + var combined = Stream.combine(function() { return a() + b() }, [a, b]) @@ -180,7 +159,7 @@ o.spec("stream", function() { var streams = [] var a = Stream(3) var b = Stream(5) - Stream.combine(function(a, b, changed) { + Stream.combine(function(changed) { streams = changed }, [a, b]) @@ -230,7 +209,7 @@ o.spec("stream", function() { var count = 0 var skip = false var a = Stream(1) - var b = Stream.combine(function(a) { + var b = Stream.combine(function() { if (skip) { return Stream.SKIP } @@ -439,7 +418,7 @@ o.spec("stream", function() { o.spec("end", function() { o("end stream works", function() { var stream = Stream() - var doubled = Stream.combine(function(stream) {return stream() * 2}, [stream]) + var doubled = Stream.combine(function() {return stream() * 2}, [stream]) stream.end(true) @@ -449,7 +428,7 @@ o.spec("stream", function() { }) o("end stream works with default value", function() { var stream = Stream(2) - var doubled = Stream.combine(function(stream) {return stream() * 2}, [stream]) + var doubled = Stream.combine(function() {return stream() * 2}, [stream]) stream.end(true) @@ -461,14 +440,14 @@ o.spec("stream", function() { var stream = Stream(2) stream.end(true) - var doubled = Stream.combine(function(stream) {return stream() * 2}, [stream]) + var doubled = Stream.combine(function() {return stream() * 2}, [stream]) stream(3) o(doubled()).equals(undefined) }) o("upstream does not affect ended stream", function() { var stream = Stream(2) - var doubled = Stream.combine(function(stream) {return stream() * 2}, [stream]) + var doubled = Stream.combine(function() {return stream() * 2}, [stream]) doubled.end(true) diff --git a/tests/std/throttler.js b/tests/std/throttler.js new file mode 100644 index 000000000..c85402472 --- /dev/null +++ b/tests/std/throttler.js @@ -0,0 +1,170 @@ +import o from "ospec" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("throttler", () => { + function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)) + } + + var throttled + + o.afterEach(() => { + if (throttled) throttled.dispose() + }) + + o("validates create input", () => { + o(() => m.throttler(NaN)).throws(RangeError) + o(() => m.throttler(+1/0)).throws(RangeError) + o(() => m.throttler(-1/0)).throws(RangeError) + o(() => m.throttler("")).throws(RangeError) + o(() => m.throttler("123")).throws(RangeError) + o(() => m.throttler(true)).throws(RangeError) + o(() => m.throttler(false)).throws(RangeError) + o(() => m.throttler(null)).throws(RangeError) + o(() => m.throttler([])).throws(RangeError) + o(() => m.throttler({})).throws(RangeError) + o(() => m.throttler(Symbol("wat"))).throws(RangeError) + m.throttler() + m.throttler(100) + }) + + o("validates update input", () => { + throttled = m.throttler() + + o(() => throttled.update(NaN)).throws(RangeError) + o(() => throttled.update(+1/0)).throws(RangeError) + o(() => throttled.update(-1/0)).throws(RangeError) + o(() => throttled.update("")).throws(RangeError) + o(() => throttled.update("123")).throws(RangeError) + o(() => throttled.update(true)).throws(RangeError) + o(() => throttled.update(false)).throws(RangeError) + o(() => throttled.update(null)).throws(RangeError) + o(() => throttled.update([])).throws(RangeError) + o(() => throttled.update({})).throws(RangeError) + o(() => throttled.update(Symbol("wat"))).throws(RangeError) + o(() => throttled.update()).throws(RangeError) + throttled.update(100) + }) + + o("detects edges correctly", async () => { + o.timeout(1000) + + throttled = m.throttler(100) + + var p1 = throttled() + var p2 = throttled() + await sleep(10) + var p3 = throttled() + await sleep(140) + var p4 = throttled() + o(await p1).equals(undefined) + o(await p2).equals(true) + o(await p3).equals(false) + o(await p4).equals(undefined) + + var p5 = throttled() + await sleep(150) + var p6 = throttled() + o(await p5).equals(false) + o(await p6).equals(undefined) + }) + + o("retains the timer on early hit", async () => { + o.timeout(1000) + + throttled = m.throttler(100) + + var slept = false + setTimeout(() => { slept = true }, 125) + void throttled() + await sleep(50) + await throttled() + o(slept).equals(false) + }) + + o("handles dynamic changes to higher delays", async () => { + o.timeout(1000) + + throttled = m.throttler(100) + + var p1 = throttled() + var p2 = throttled() + await sleep(10) + var p3 = throttled() + throttled.update(200) + await sleep(140) + var p4 = throttled() + o(await p1).equals(undefined) + o(await p2).equals(true) + o(await p3).equals(true) + o(await p4).equals(false) + + var p5 = throttled() + await sleep(250) + var p6 = throttled() + o(await p5).equals(undefined) + o(await p6).equals(undefined) + }) + + o("handles dynamic changes to lower delays", async () => { + o.timeout(1000) + + throttled = m.throttler(100) + + var p1 = throttled() + var p2 = throttled() + await sleep(10) + var p3 = throttled() + throttled.update(50) + await sleep(100) + var p4 = throttled() + o(await p1).equals(undefined) + o(await p2).equals(true) + o(await p3).equals(false) + o(await p4).equals(undefined) + + var p5 = throttled() + await sleep(100) + var p6 = throttled() + o(await p5).equals(false) + o(await p6).equals(undefined) + }) + + o("handles same-duration changes", async () => { + o.timeout(1000) + + throttled = m.throttler(100) + + var p1 = throttled() + throttled.update(100) + var p2 = throttled() + throttled.update(100) + await sleep(10) + throttled.update(100) + var p3 = throttled() + throttled.update(100) + await sleep(140) + throttled.update(100) + var p4 = throttled() + throttled.update(100) + o(await p1).equals(undefined) + throttled.update(100) + o(await p2).equals(true) + throttled.update(100) + o(await p3).equals(false) + throttled.update(100) + o(await p4).equals(undefined) + throttled.update(100) + + var p5 = throttled() + throttled.update(100) + await sleep(150) + throttled.update(100) + var p6 = throttled() + throttled.update(100) + o(await p5).equals(false) + throttled.update(100) + o(await p6).equals(undefined) + }) +}) diff --git a/tests/std/tracked.js b/tests/std/tracked.js new file mode 100644 index 000000000..d30e61408 --- /dev/null +++ b/tests/std/tracked.js @@ -0,0 +1,395 @@ +import o from "ospec" + +import m from "../../src/entry/mithril.esm.js" + +const readState = (list) => list.map((h) => [h.key, h.value, h.signal.aborted]) + +o.spec("trackedList", () => { + o("initializes values correctly", () => { + let calls = 0 + const t = m.trackedList(() => calls++, [[1, "one"], [2, "two"]]) + + o(readState(t.live())).deepEquals([[1, "one", false], [2, "two", false]]) + o(t.list()).deepEquals([[1, "one"], [2, "two"]]) + + o(t.has(1)).equals(true) + o(t.get(1)).equals("one") + o(t.has(2)).equals(true) + o(t.get(2)).equals("two") + + o(calls).equals(0) + }) + + o("tracks values correctly", () => { + let calls = 0 + const t = m.trackedList(() => calls++) + + t.set(1, "one") + o(calls).equals(1) + o(readState(t.live())).deepEquals([[1, "one", false]]) + o(t.list()).deepEquals([[1, "one"]]) + o(t.has(1)).equals(true) + o(t.get(1)).equals("one") + const live1 = t.live()[0] + + t.set(2, "two") + o(calls).equals(2) + o(readState(t.live())).deepEquals([[1, "one", false], [2, "two", false]]) + o(t.live()[0]).equals(live1) + o(t.list()).deepEquals([[1, "one"], [2, "two"]]) + o(t.has(1)).equals(true) + o(t.get(1)).equals("one") + o(t.has(2)).equals(true) + o(t.get(2)).equals("two") + const live2 = t.live()[1] + + t.delete(1) + o(calls).equals(3) + o(readState(t.live())).deepEquals([[1, "one", true], [2, "two", false]]) + o(t.live()[0]).equals(live1) + o(t.live()[1]).equals(live2) + o(t.list()).deepEquals([[2, "two"]]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + o(t.has(2)).equals(true) + o(t.get(2)).equals("two") + + live1.release() + o(calls).equals(4) + o(readState(t.live())).deepEquals([[2, "two", false]]) + o(t.live()[0]).equals(live2) + o(t.list()).deepEquals([[2, "two"]]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + o(t.has(2)).equals(true) + o(t.get(2)).equals("two") + + t.replace(2, "dos") + o(calls).equals(5) + o(readState(t.live())).deepEquals([[2, "two", true], [2, "dos", false]]) + o(t.live()[0]).equals(live2) + o(t.list()).deepEquals([[2, "dos"]]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + o(t.has(2)).equals(true) + o(t.get(2)).equals("dos") + const live3 = t.live()[1] + + live2.release() + o(calls).equals(6) + o(readState(t.live())).deepEquals([[2, "dos", false]]) + o(t.live()[0]).equals(live3) + o(t.list()).deepEquals([[2, "dos"]]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + o(t.has(2)).equals(true) + o(t.get(2)).equals("dos") + }) + + o("invokes `redraw()` after the update is fully completed, including any and all signal aborts", () => { + let live1, live2, live3 + let live1Aborted = false + let live2Aborted = false + let call = 0 + const t = m.trackedList(() => { + switch (++call) { + case 1: + o(readState(t.live())).deepEquals([[1, "one", false]]) + o(t.list()).deepEquals([[1, "one"]]) + o(t.has(1)).equals(true) + o(t.get(1)).equals("one") + live1 = t.live()[0] + break + + case 2: + o(readState(t.live())).deepEquals([[1, "one", false], [2, "two", false]]) + o(t.live()[0]).equals(live1) + o(t.list()).deepEquals([[1, "one"], [2, "two"]]) + o(t.has(1)).equals(true) + o(t.get(1)).equals("one") + o(t.has(2)).equals(true) + o(t.get(2)).equals("two") + live2 = t.live()[1] + break + + case 3: + o(readState(t.live())).deepEquals([[1, "one", true], [2, "two", false]]) + o(t.live()[0]).equals(live1) + o(t.live()[1]).equals(live2) + o(t.list()).deepEquals([[2, "two"]]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + o(t.has(2)).equals(true) + o(t.get(2)).equals("two") + break + + case 4: + o(readState(t.live())).deepEquals([[2, "two", false]]) + o(t.live()[0]).equals(live2) + o(t.list()).deepEquals([[2, "two"]]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + o(t.has(2)).equals(true) + o(t.get(2)).equals("two") + break + + case 5: + o(readState(t.live())).deepEquals([[2, "two", true], [2, "dos", false]]) + o(t.live()[0]).equals(live2) + o(t.list()).deepEquals([[2, "dos"]]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + o(t.has(2)).equals(true) + o(t.get(2)).equals("dos") + live3 = t.live()[1] + break + + case 6: + o(readState(t.live())).deepEquals([[2, "dos", false]]) + o(t.live()[0]).equals(live3) + o(t.list()).deepEquals([[2, "dos"]]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + o(t.has(2)).equals(true) + o(t.get(2)).equals("dos") + break + + default: + throw new Error("Too many calls") + } + }) + + t.set(1, "one") + o(call).equals(1) + o(live1Aborted).equals(false) + o(live2Aborted).equals(false) + let deleteOneStarted = false + live1.signal.onabort = () => { + live1Aborted = true + o(call).equals(2) + o(deleteOneStarted).equals(true) + } + + t.set(2, "two") + o(call).equals(2) + o(live1Aborted).equals(false) + o(live2Aborted).equals(false) + let deleteTwoStarted = false + live2.signal.onabort = () => { + live2Aborted = true + o(call).equals(4) + o(deleteTwoStarted).equals(true) + } + + deleteOneStarted = true + t.delete(1) + o(call).equals(3) + o(live1Aborted).equals(true) + o(live2Aborted).equals(false) + + live1.release() + o(call).equals(4) + o(live1Aborted).equals(true) + o(live2Aborted).equals(false) + + deleteTwoStarted = true + t.replace(2, "dos") + o(call).equals(5) + o(live1Aborted).equals(true) + o(live2Aborted).equals(true) + + live2.release() + o(call).equals(6) + o(live1Aborted).equals(true) + o(live2Aborted).equals(true) + }) + + o("tracks parallel removes correctly", () => { + let calls = 0 + const t = m.trackedList(() => calls++) + + t.set(1, "one") + const live1 = t.live()[0] + + t.set(2, "two") + const live2 = t.live()[1] + + t.delete(1) + o(calls).equals(3) + o(readState(t.live())).deepEquals([[1, "one", true], [2, "two", false]]) + o(t.live()[0]).equals(live1) + o(t.live()[1]).equals(live2) + o(t.list()).deepEquals([[2, "two"]]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + o(t.has(2)).equals(true) + o(t.get(2)).equals("two") + + t.replace(2, "dos") + o(calls).equals(4) + o(readState(t.live())).deepEquals([[1, "one", true], [2, "two", true], [2, "dos", false]]) + o(t.live()[0]).equals(live1) + o(t.live()[1]).equals(live2) + o(t.list()).deepEquals([[2, "dos"]]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + o(t.has(2)).equals(true) + o(t.get(2)).equals("dos") + const live3 = t.live()[2] + + live1.release() + o(calls).equals(5) + o(readState(t.live())).deepEquals([[2, "two", true], [2, "dos", false]]) + o(t.live()[0]).equals(live2) + o(t.list()).deepEquals([[2, "dos"]]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + o(t.has(2)).equals(true) + o(t.get(2)).equals("dos") + + live2.release() + o(calls).equals(6) + o(readState(t.live())).deepEquals([[2, "dos", false]]) + o(t.live()[0]).equals(live3) + o(t.list()).deepEquals([[2, "dos"]]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + o(t.has(2)).equals(true) + o(t.get(2)).equals("dos") + }) + + o("tolerates release before abort", () => { + let calls = 0 + const t = m.trackedList(() => calls++) + + t.set(1, "one") + o(calls).equals(1) + o(readState(t.live())).deepEquals([[1, "one", false]]) + o(t.list()).deepEquals([[1, "one"]]) + o(t.has(1)).equals(true) + o(t.get(1)).equals("one") + const live1 = t.live()[0] + + live1.release() + o(calls).equals(1) + o(readState(t.live())).deepEquals([[1, "one", false]]) + o(t.list()).deepEquals([[1, "one"]]) + o(t.has(1)).equals(true) + o(t.get(1)).equals("one") + + t.delete(1) + o(calls).equals(2) + o(readState(t.live())).deepEquals([]) + o(t.list()).deepEquals([]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + }) + + o("tolerates double release before abort", () => { + let calls = 0 + const t = m.trackedList(() => calls++) + + t.set(1, "one") + const live1 = t.live()[0] + + live1.release() + live1.release() + o(calls).equals(1) + o(readState(t.live())).deepEquals([[1, "one", false]]) + o(t.list()).deepEquals([[1, "one"]]) + o(t.has(1)).equals(true) + o(t.get(1)).equals("one") + + t.delete(1) + o(calls).equals(2) + o(readState(t.live())).deepEquals([]) + o(t.list()).deepEquals([]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + }) + + o("tolerates double release spanning delete", () => { + let calls = 0 + const t = m.trackedList(() => calls++) + + t.set(1, "one") + const live1 = t.live()[0] + live1.release() + t.delete(1) + live1.release() + + o(calls).equals(2) + o(readState(t.live())).deepEquals([]) + o(t.list()).deepEquals([]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + }) + + o("tracks double release after delete", () => { + let calls = 0 + const t = m.trackedList(() => calls++) + + t.set(1, "one") + const live1 = t.live()[0] + t.delete(1) + o(calls).equals(2) + o(readState(t.live())).deepEquals([[1, "one", true]]) + o(t.list()).deepEquals([]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + + live1.release() + o(calls).equals(3) + o(readState(t.live())).deepEquals([]) + o(t.list()).deepEquals([]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + + live1.release() + o(calls).equals(3) + o(readState(t.live())).deepEquals([]) + o(t.list()).deepEquals([]) + o(t.has(1)).equals(false) + o(t.get(1)).equals(undefined) + }) +}) + +o.spec("tracked", () => { + o("tracks values correctly", () => { + let calls = 0 + const trackHit = m.tracked(() => calls++) + + o(readState(trackHit("a"))).deepEquals([[0, "a", false]]) + o(readState(trackHit("a"))).deepEquals([[0, "a", false]]) + o(calls).equals(0) + + const list1 = trackHit("b") + o(readState(list1)).deepEquals([[0, "a", true], [1, "b", false]]) + o(calls).equals(0) + list1[0].release() + o(calls).equals(1) + + o(readState(trackHit("b"))).deepEquals([[1, "b", false]]) + o(calls).equals(1) + o(readState(trackHit("c"))).deepEquals([[1, "b", true], [2, "c", false]]) + o(calls).equals(1) + o(readState(trackHit("d"))).deepEquals([[1, "b", true], [2, "c", true], [3, "d", false]]) + o(calls).equals(1) + + const list2 = trackHit("d") + o(readState(list2)).deepEquals([[1, "b", true], [2, "c", true], [3, "d", false]]) + o(calls).equals(1) + list2[2].remove() + o(calls).equals(1) + + o(readState(trackHit("d"))).deepEquals([[1, "b", true], [2, "c", true], [3, "d", true]]) + o(calls).equals(1) + + list2[0].release() + o(calls).equals(2) + list2[1].release() + o(calls).equals(3) + list2[2].release() + o(calls).equals(4) + }) +}) diff --git a/tests/std/use.js b/tests/std/use.js new file mode 100644 index 000000000..2a9ac734c --- /dev/null +++ b/tests/std/use.js @@ -0,0 +1,60 @@ +import o from "ospec" + +import {setupGlobals} from "../../test-utils/global.js" + +import m from "../../src/entry/mithril.esm.js" + +o.spec("m.use", () => { + var G = setupGlobals() + + o("works with empty arrays", () => { + var layout = o.spy() + var remove = o.spy() + + m.render(G.root, m.use([], m.layout(layout), m.remove(remove))) + o(layout.callCount).equals(1) + o(remove.callCount).equals(0) + + m.render(G.root, m.use([], m.layout(layout), m.remove(remove))) + o(layout.callCount).equals(2) + o(remove.callCount).equals(0) + + m.render(G.root, null) + o(layout.callCount).equals(2) + o(remove.callCount).equals(1) + }) + + o("works with equal non-empty arrays", () => { + var layout = o.spy() + var remove = o.spy() + + m.render(G.root, m.use([1], m.layout(layout), m.remove(remove))) + o(layout.callCount).equals(1) + o(remove.callCount).equals(0) + + m.render(G.root, m.use([1], m.layout(layout), m.remove(remove))) + o(layout.callCount).equals(2) + o(remove.callCount).equals(0) + + m.render(G.root, null) + o(layout.callCount).equals(2) + o(remove.callCount).equals(1) + }) + + o("works with non-equal same-length non-empty arrays", () => { + var remove = o.spy() + var layout = o.spy() + + m.render(G.root, m.use([1], m.layout(layout), m.remove(remove))) + o(layout.callCount).equals(1) + o(remove.callCount).equals(0) + + m.render(G.root, m.use([2], m.layout(layout), m.remove(remove))) + o(layout.callCount).equals(2) + o(remove.callCount).equals(1) + + m.render(G.root, null) + o(layout.callCount).equals(2) + o(remove.callCount).equals(2) + }) +}) diff --git a/tests/test-api.js b/tests/test-api.js deleted file mode 100644 index dc61228ee..000000000 --- a/tests/test-api.js +++ /dev/null @@ -1,170 +0,0 @@ -"use strict" - -var o = require("ospec") -var browserMock = require("../test-utils/browserMock") -var components = require("../test-utils/components") - -o.spec("api", function() { - var FRAME_BUDGET = Math.floor(1000 / 60) - var mock = browserMock(), root - mock.setTimeout = setTimeout - if (typeof global !== "undefined") { - global.window = mock - global.requestAnimationFrame = mock.requestAnimationFrame - } - var m = require("..") // eslint-disable-line global-require - - o.afterEach(function() { - if (root) m.mount(root, null) - }) - - o.spec("m", function() { - o("works", function() { - var vnode = m("div") - - o(vnode.tag).equals("div") - }) - }) - o.spec("m.trust", function() { - o("works", function() { - var vnode = m.trust("
") - - o(vnode.tag).equals("<") - o(vnode.children).equals("
") - }) - }) - o.spec("m.fragment", function() { - o("works", function() { - var vnode = m.fragment({key: 123}, [m("div")]) - - o(vnode.tag).equals("[") - o(vnode.key).equals(123) - o(vnode.children.length).equals(1) - o(vnode.children[0].tag).equals("div") - }) - }) - o.spec("m.parseQueryString", function() { - o("works", function() { - var query = m.parseQueryString("?a=1&b=2") - - o(query).deepEquals({a: "1", b: "2"}) - }) - }) - o.spec("m.buildQueryString", function() { - o("works", function() { - var query = m.buildQueryString({a: 1, b: 2}) - - o(query).equals("a=1&b=2") - }) - }) - o.spec("m.request", function() { - o("works", function() { - o(typeof m.request).equals("function") // TODO improve - }) - }) - o.spec("m.render", function() { - o("works", function() { - root = window.document.createElement("div") - m.render(root, m("div")) - - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("DIV") - }) - }) - components.forEach(function(cmp){ - o.spec(cmp.kind, function(){ - var createComponent = cmp.create - - o.spec("m.mount", function() { - o("works", function() { - root = window.document.createElement("div") - m.mount(root, createComponent({view: function() {return m("div")}})) - - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("DIV") - }) - }) - o.spec("m.route", function() { - o("works", function(done) { - root = window.document.createElement("div") - m.route(root, "/a", { - "/a": createComponent({view: function() {return m("div")}}) - }) - - setTimeout(function() { - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("DIV") - - done() - }, FRAME_BUDGET) - }) - o("m.route.prefix", function(done) { - root = window.document.createElement("div") - m.route.prefix = "#" - m.route(root, "/a", { - "/a": createComponent({view: function() {return m("div")}}) - }) - - setTimeout(function() { - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("DIV") - - done() - }, FRAME_BUDGET) - }) - o("m.route.get", function(done) { - root = window.document.createElement("div") - m.route(root, "/a", { - "/a": createComponent({view: function() {return m("div")}}) - }) - - setTimeout(function() { - o(m.route.get()).equals("/a") - - done() - }, FRAME_BUDGET) - }) - o("m.route.set", function(done) { - o.timeout(100) - root = window.document.createElement("div") - m.route(root, "/a", { - "/:id": createComponent({view: function() {return m("div")}}) - }) - - setTimeout(function() { - m.route.set("/b") - setTimeout(function() { - o(m.route.get()).equals("/b") - - done() - }, FRAME_BUDGET) - }, FRAME_BUDGET) - }) - }) - o.spec("m.redraw", function() { - o("works", function(done) { - var count = 0 - root = window.document.createElement("div") - m.mount(root, createComponent({view: function() {count++}})) - o(count).equals(1) - m.redraw() - o(count).equals(1) - setTimeout(function() { - - o(count).equals(2) - - done() - }, FRAME_BUDGET) - }) - o("sync", function() { - 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) - }) - }) - }) - }) -}) diff --git a/tests/test-utils/browserMock.js b/tests/test-utils/browserMock.js new file mode 100644 index 000000000..b27104f83 --- /dev/null +++ b/tests/test-utils/browserMock.js @@ -0,0 +1,35 @@ +import o from "ospec" + +import {callAsync} from "../../test-utils/callAsync.js" +import {setupGlobals} from "../../test-utils/global.js" + +o.spec("browserMock", function() { + var G = setupGlobals() + + o("Mocks DOM and pushState", function() { + o(G.window.location).notEquals(undefined) + o(G.window.document).notEquals(undefined) + }) + o("G.window.onhashchange can be reached from the pushStateMock functions", function(done) { + G.window.onhashchange = o.spy() + G.window.location.hash = "#a" + + callAsync(function(){ + o(G.window.onhashchange.callCount).equals(1) + done() + }) + }) + o("G.window.onpopstate can be reached from the pushStateMock functions", function() { + G.window.onpopstate = o.spy() + G.window.history.pushState(null, null, "#a") + G.window.history.back() + + o(G.window.onpopstate.callCount).equals(1) + }) + o("G.window.onunload can be reached from the pushStateMock functions", function() { + G.window.onunload = o.spy() + G.window.location.href = "/a" + + o(G.window.onunload.callCount).equals(1) + }) +}) diff --git a/tests/test-utils/callAsync.js b/tests/test-utils/callAsync.js new file mode 100644 index 000000000..ded82203d --- /dev/null +++ b/tests/test-utils/callAsync.js @@ -0,0 +1,45 @@ +/* global setTimeout, clearTimeout */ + +import o from "ospec" + +import {callAsync, clearPending, waitAsync} from "../../test-utils/callAsync.js" + +o.spec("callAsync", function() { + o("gets called before setTimeout", function(done) { + var timeout + callAsync(function() { + clearTimeout(timeout) + done() + }) + timeout = setTimeout(function() { + throw new Error("callAsync was called too slow") + }, 5) + }) + o("gets cleared", function(done) { + callAsync(function() { + clearTimeout(timeout) + done(new Error("should never have been called")) + }) + const timeout = setTimeout(done, 5) + clearPending() + }) +}) + +o.spec("waitAsync", function() { + function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)) + } + + o("gets called before setTimeout", () => Promise.race([ + waitAsync(), + sleep(5).then(() => { throw new Error("callAsync was called too slow") }) + ])) + o("gets cleared", () => { + const promise = waitAsync() + clearPending() + return Promise.race([ + promise.then(() => { throw new Error("should never have been called") }), + sleep(5), + ]) + }) +}) diff --git a/test-utils/tests/test-domMock.js b/tests/test-utils/domMock.js similarity index 64% rename from test-utils/tests/test-domMock.js rename to tests/test-utils/domMock.js index 36d3364cb..c43f288f4 100644 --- a/test-utils/tests/test-domMock.js +++ b/tests/test-utils/domMock.js @@ -1,18 +1,13 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var domMock = require("../../test-utils/domMock") +import {setupGlobals} from "../../test-utils/global.js" o.spec("domMock", function() { - var $document, $window - o.beforeEach(function() { - $window = domMock() - $document = $window.document - }) + var G = setupGlobals() o.spec("createElement", function() { o("works", function() { - var node = $document.createElement("div") + var node = G.window.document.createElement("div") o(node.nodeType).equals(1) o(node.nodeName).equals("DIV") @@ -26,7 +21,7 @@ o.spec("domMock", function() { o.spec("createElementNS", function() { o("works", function() { - var node = $document.createElementNS("http://www.w3.org/2000/svg", "svg") + var node = G.window.document.createElementNS("http://www.w3.org/2000/svg", "svg") o(node.nodeType).equals(1) o(node.nodeName).equals("svg") @@ -40,7 +35,7 @@ o.spec("domMock", function() { o.spec("createTextNode", function() { o("works", function() { - var node = $document.createTextNode("abc") + var node = G.window.document.createTextNode("abc") o(node.nodeType).equals(3) o(node.nodeName).equals("#text") @@ -48,32 +43,32 @@ o.spec("domMock", function() { o(node.nodeValue).equals("abc") }) o("works w/ number", function() { - var node = $document.createTextNode(123) + var node = G.window.document.createTextNode(123) o(node.nodeValue).equals("123") }) o("works w/ null", function() { - var node = $document.createTextNode(null) + var node = G.window.document.createTextNode(null) o(node.nodeValue).equals("null") }) o("works w/ undefined", function() { - var node = $document.createTextNode(undefined) + var node = G.window.document.createTextNode(undefined) o(node.nodeValue).equals("undefined") }) o("works w/ object", function() { - var node = $document.createTextNode({}) + var node = G.window.document.createTextNode({}) o(node.nodeValue).equals("[object Object]") }) o("does not unescape HTML", function() { - var node = $document.createTextNode("&") + var node = G.window.document.createTextNode("&") o(node.nodeValue).equals("&") }) o("nodeValue casts to string", function() { - var node = $document.createTextNode("a") + var node = G.window.document.createTextNode("a") node.nodeValue = true o(node.nodeValue).equals("true") @@ -82,7 +77,7 @@ o.spec("domMock", function() { o("doesn't work with symbols", function(){ var threw = false try { - $document.createTextNode(Symbol("nono")) + G.window.document.createTextNode(Symbol("nono")) } catch(e) { threw = true } @@ -91,7 +86,7 @@ o.spec("domMock", function() { o("symbols can't be used as nodeValue", function(){ var threw = false try { - var node = $document.createTextNode("a") + var node = G.window.document.createTextNode("a") node.nodeValue = Symbol("nono") } catch(e) { threw = true @@ -101,22 +96,10 @@ o.spec("domMock", function() { } }) - o.spec("createDocumentFragment", function() { - o("works", function() { - var node = $document.createDocumentFragment() - - o(node.nodeType).equals(11) - o(node.nodeName).equals("#document-fragment") - o(node.parentNode).equals(null) - o(node.childNodes.length).equals(0) - o(node.firstChild).equals(null) - }) - }) - o.spec("appendChild", function() { o("works", function() { - var parent = $document.createElement("div") - var child = $document.createElement("a") + var parent = G.window.document.createElement("div") + var child = G.window.document.createElement("a") parent.appendChild(child) o(parent.childNodes.length).equals(1) @@ -125,9 +108,9 @@ o.spec("domMock", function() { o(child.parentNode).equals(parent) }) o("moves existing", function() { - var parent = $document.createElement("div") - var a = $document.createElement("a") - var b = $document.createElement("b") + var parent = G.window.document.createElement("div") + var a = G.window.document.createElement("a") + var b = G.window.document.createElement("b") parent.appendChild(a) parent.appendChild(b) parent.appendChild(a) @@ -141,50 +124,30 @@ o.spec("domMock", function() { o(b.parentNode).equals(parent) }) o("removes from old parent", function() { - var parent = $document.createElement("div") - var source = $document.createElement("span") - var a = $document.createElement("a") - var b = $document.createElement("b") + var parent = G.window.document.createElement("div") + var source = G.window.document.createElement("span") + var a = G.window.document.createElement("a") + var b = G.window.document.createElement("b") parent.appendChild(a) source.appendChild(b) parent.appendChild(b) o(source.childNodes.length).equals(0) }) - o("transfers from fragment", function() { - var parent = $document.createElement("div") - var a = $document.createDocumentFragment("a") - var b = $document.createElement("b") - var c = $document.createElement("c") - a.appendChild(b) - a.appendChild(c) - parent.appendChild(a) - - o(parent.childNodes.length).equals(2) - o(parent.childNodes[0]).equals(b) - o(parent.childNodes[1]).equals(c) - o(parent.firstChild).equals(b) - o(parent.firstChild.nextSibling).equals(c) - o(a.childNodes.length).equals(0) - o(a.firstChild).equals(null) - o(a.parentNode).equals(null) - o(b.parentNode).equals(parent) - o(c.parentNode).equals(parent) - }) o("throws if appended to self", function(done) { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") try {div.appendChild(div)} catch (e) {done()} }) o("throws if appended to child", function(done) { - var parent = $document.createElement("div") - var child = $document.createElement("a") + var parent = G.window.document.createElement("div") + var child = G.window.document.createElement("a") parent.appendChild(child) try {child.appendChild(parent)} catch (e) {done()} }) o("throws if child is not element", function(done) { - var parent = $document.createElement("div") + var parent = G.window.document.createElement("div") var child = 1 try {parent.appendChild(child)} catch (e) {done()} @@ -193,8 +156,8 @@ o.spec("domMock", function() { o.spec("removeChild", function() { o("works", function() { - var parent = $document.createElement("div") - var child = $document.createElement("a") + var parent = G.window.document.createElement("div") + var child = G.window.document.createElement("a") parent.appendChild(child) parent.removeChild(child) @@ -203,8 +166,8 @@ o.spec("domMock", function() { o(child.parentNode).equals(null) }) o("throws if not a child", function(done) { - var parent = $document.createElement("div") - var child = $document.createElement("a") + var parent = G.window.document.createElement("div") + var child = G.window.document.createElement("a") try {parent.removeChild(child)} catch (e) {done()} }) @@ -212,9 +175,9 @@ o.spec("domMock", function() { o.spec("insertBefore", function() { o("works", function() { - var parent = $document.createElement("div") - var a = $document.createElement("a") - var b = $document.createElement("b") + var parent = G.window.document.createElement("div") + var a = G.window.document.createElement("a") + var b = G.window.document.createElement("b") parent.appendChild(a) parent.insertBefore(b, a) @@ -227,9 +190,9 @@ o.spec("domMock", function() { o(b.parentNode).equals(parent) }) o("moves existing", function() { - var parent = $document.createElement("div") - var a = $document.createElement("a") - var b = $document.createElement("b") + var parent = G.window.document.createElement("div") + var a = G.window.document.createElement("a") + var b = G.window.document.createElement("b") parent.appendChild(a) parent.appendChild(b) parent.insertBefore(b, a) @@ -243,10 +206,10 @@ o.spec("domMock", function() { 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") + var parent = G.window.document.createElement("div") + var a = G.window.document.createElement("a") + var b = G.window.document.createElement("b") + var c = G.window.document.createElement("c") parent.appendChild(a) parent.appendChild(b) parent.appendChild(c) @@ -265,44 +228,20 @@ o.spec("domMock", function() { }) o("removes from old parent", function() { - var parent = $document.createElement("div") - var source = $document.createElement("span") - var a = $document.createElement("a") - var b = $document.createElement("b") + var parent = G.window.document.createElement("div") + var source = G.window.document.createElement("span") + var a = G.window.document.createElement("a") + var b = G.window.document.createElement("b") parent.appendChild(a) source.appendChild(b) parent.insertBefore(b, a) o(source.childNodes.length).equals(0) }) - o("transfers from fragment", function() { - var parent = $document.createElement("div") - var ref = $document.createElement("span") - var a = $document.createDocumentFragment("a") - var b = $document.createElement("b") - var c = $document.createElement("c") - parent.appendChild(ref) - a.appendChild(b) - a.appendChild(c) - parent.insertBefore(a, ref) - - o(parent.childNodes.length).equals(3) - o(parent.childNodes[0]).equals(b) - o(parent.childNodes[1]).equals(c) - o(parent.childNodes[2]).equals(ref) - o(parent.firstChild).equals(b) - o(parent.firstChild.nextSibling).equals(c) - o(parent.firstChild.nextSibling.nextSibling).equals(ref) - o(a.childNodes.length).equals(0) - o(a.firstChild).equals(null) - o(a.parentNode).equals(null) - o(b.parentNode).equals(parent) - o(c.parentNode).equals(parent) - }) o("appends if second arg is null", function() { - var parent = $document.createElement("div") - var a = $document.createElement("a") - var b = $document.createElement("b") + var parent = G.window.document.createElement("div") + var a = G.window.document.createElement("a") + var b = G.window.document.createElement("b") parent.appendChild(a) parent.insertBefore(b, null) @@ -314,44 +253,44 @@ o.spec("domMock", function() { o(a.parentNode).equals(parent) }) o("throws if appended to self", function(done) { - var div = $document.createElement("div") - var a = $document.createElement("a") + var div = G.window.document.createElement("div") + var a = G.window.document.createElement("a") div.appendChild(a) try {div.isnertBefore(div, a)} catch (e) {done()} }) o("throws if appended to child", function(done) { - var parent = $document.createElement("div") - var a = $document.createElement("a") - var b = $document.createElement("b") + var parent = G.window.document.createElement("div") + var a = G.window.document.createElement("a") + var b = G.window.document.createElement("b") parent.appendChild(a) a.appendChild(b) try {a.insertBefore(parent, b)} catch (e) {done()} }) o("throws if child is not element", function(done) { - var parent = $document.createElement("div") - var a = $document.createElement("a") + var parent = G.window.document.createElement("div") + var a = G.window.document.createElement("a") parent.appendChild(a) try {parent.insertBefore(1, a)} catch (e) {done()} }) o("throws if inserted before itself", function(done) { - var parent = $document.createElement("div") - var a = $document.createElement("a") + var parent = G.window.document.createElement("div") + var a = G.window.document.createElement("a") try {parent.insertBefore(a, a)} catch (e) {done()} }) o("throws if second arg is undefined", function(done) { - var parent = $document.createElement("div") - var a = $document.createElement("a") + var parent = G.window.document.createElement("div") + var a = G.window.document.createElement("a") try {parent.insertBefore(a)} catch (e) {done()} }) o("throws if reference is not child", function(done) { - var parent = $document.createElement("div") - var a = $document.createElement("a") - var b = $document.createElement("b") + var parent = G.window.document.createElement("div") + var a = G.window.document.createElement("a") + var b = G.window.document.createElement("b") try {parent.insertBefore(a, b)} catch (e) {done()} }) @@ -359,13 +298,13 @@ o.spec("domMock", function() { o.spec("getAttribute", function() { o("works", function() { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.setAttribute("id", "aaa") o(div.getAttribute("id")).equals("aaa") }) o("works for attributes with a namespace", function() { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.setAttributeNS("http://www.w3.org/1999/xlink", "href", "aaa") o(div.getAttribute("href")).equals("aaa") @@ -374,7 +313,7 @@ o.spec("domMock", function() { o.spec("setAttribute", function() { o("works", function() { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.setAttribute("id", "aaa") o(div.attributes["id"].value).equals("aaa") @@ -382,31 +321,31 @@ o.spec("domMock", function() { o(div.attributes["id"].namespaceURI).equals(null) }) o("works w/ number", function() { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.setAttribute("id", 123) o(div.attributes["id"].value).equals("123") }) o("works w/ null", function() { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.setAttribute("id", null) o(div.attributes["id"].value).equals("null") }) o("works w/ undefined", function() { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.setAttribute("id", undefined) o(div.attributes["id"].value).equals("undefined") }) o("works w/ object", function() { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.setAttribute("id", {}) o(div.attributes["id"].value).equals("[object Object]") }) o("setting via attributes map stringifies", function() { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.setAttribute("id", "a") div.attributes["id"].value = 123 @@ -419,7 +358,7 @@ o.spec("domMock", function() { }) o.spec("hasAttribute", function() { o("works", function() { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") o(div.hasAttribute("id")).equals(false) @@ -435,7 +374,7 @@ o.spec("domMock", function() { o.spec("setAttributeNS", function() { o("works", function() { - var a = $document.createElementNS("http://www.w3.org/2000/svg", "a") + var a = G.window.document.createElementNS("http://www.w3.org/2000/svg", "a") a.setAttributeNS("http://www.w3.org/1999/xlink", "href", "/aaa") o(a.href).deepEquals({baseVal: "/aaa", animVal: "/aaa"}) @@ -443,7 +382,7 @@ o.spec("domMock", function() { o(a.attributes["href"].namespaceURI).equals("http://www.w3.org/1999/xlink") }) o("works w/ number", function() { - var a = $document.createElementNS("http://www.w3.org/2000/svg", "a") + var a = G.window.document.createElementNS("http://www.w3.org/2000/svg", "a") a.setAttributeNS("http://www.w3.org/1999/xlink", "href", 123) o(a.href).deepEquals({baseVal: "123", animVal: "123"}) @@ -451,7 +390,7 @@ o.spec("domMock", function() { 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") + var a = G.window.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) @@ -474,7 +413,7 @@ o.spec("domMock", function() { o.spec("removeAttribute", function() { o("works", function() { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.setAttribute("id", "aaa") div.removeAttribute("id") @@ -484,7 +423,7 @@ o.spec("domMock", function() { o.spec("textContent", function() { o("works", function() { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.textContent = "aaa" o(div.childNodes.length).equals(1) @@ -492,128 +431,39 @@ o.spec("domMock", function() { o(div.firstChild.nodeValue).equals("aaa") }) o("works with empty string", function() { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.textContent = "" o(div.childNodes.length).equals(0) }) }) - o.spec("innerHTML", function() { - o("works", function() { - var div = $document.createElement("div") - div.innerHTML = "
123234
345
" - o(div.childNodes.length).equals(2) - o(div.childNodes[0].nodeType).equals(1) - o(div.childNodes[0].nodeName).equals("BR") - o(div.childNodes[1].nodeType).equals(1) - o(div.childNodes[1].nodeName).equals("A") - o(div.childNodes[1].attributes["class"].value).equals("aaa") - o(div.childNodes[1].attributes["id"].value).equals("xyz") - o(div.childNodes[1].childNodes[0].nodeType).equals(3) - o(div.childNodes[1].childNodes[0].nodeValue).equals("123") - o(div.childNodes[1].childNodes[1].nodeType).equals(1) - o(div.childNodes[1].childNodes[1].nodeName).equals("B") - o(div.childNodes[1].childNodes[1].attributes["class"].value).equals("bbb") - o(div.childNodes[1].childNodes[2].nodeType).equals(3) - o(div.childNodes[1].childNodes[2].nodeValue).equals("234") - o(div.childNodes[1].childNodes[3].nodeType).equals(1) - o(div.childNodes[1].childNodes[3].nodeName).equals("BR") - o(div.childNodes[1].childNodes[3].attributes["class"].value).equals("ccc") - o(div.childNodes[1].childNodes[4].nodeType).equals(3) - o(div.childNodes[1].childNodes[4].nodeValue).equals("345") - }) - o("headers work", function() { - var div = $document.createElement("div") - div.innerHTML = "

" - o(div.childNodes.length).equals(6) - o(div.childNodes[0].nodeType).equals(1) - o(div.childNodes[0].nodeName).equals("H1") - o(div.childNodes[1].nodeType).equals(1) - o(div.childNodes[1].nodeName).equals("H2") - o(div.childNodes[2].nodeType).equals(1) - o(div.childNodes[2].nodeName).equals("H3") - o(div.childNodes[3].nodeType).equals(1) - o(div.childNodes[3].nodeName).equals("H4") - o(div.childNodes[4].nodeType).equals(1) - o(div.childNodes[4].nodeName).equals("H5") - o(div.childNodes[5].nodeType).equals(1) - o(div.childNodes[5].nodeName).equals("H6") - }) - o("detaches old elements", function() { - var div = $document.createElement("div") - var a = $document.createElement("a") - div.appendChild(a) - div.innerHTML = "" - - o(a.parentNode).equals(null) - }) - o("empty SVG document", function() { - var div = $document.createElement("div") - div.innerHTML = "" - - 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 = - "" - + "hello" - + " " - + "world" - + "" - - 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() { - o($document.documentElement.nodeName).equals("HTML") - o($document.body.nodeName).equals("BODY") - o($document.documentElement.firstChild.nodeName).equals("HEAD") - o($document.documentElement).equals($document.body.parentNode) - o($document.activeElement).equals($document.body) + o(G.window.document.documentElement.nodeName).equals("HTML") + o(G.window.document.body.nodeName).equals("BODY") + o(G.window.document.documentElement.firstChild.nodeName).equals("HEAD") + o(G.window.document.documentElement).equals(G.window.document.body.parentNode) + o(G.window.document.activeElement).equals(null) }) o("focus changes activeElement", function() { - var input = $document.createElement("input") - $document.body.appendChild(input) + var input = G.window.document.createElement("input") + G.window.document.body.appendChild(input) input.focus() - o($document.activeElement).equals(input) + o(G.window.document.activeElement).equals(input) - $document.body.removeChild(input) + G.window.document.body.removeChild(input) }) }) o.spec("style", function() { o("has style property", function() { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") o(typeof div.style).equals("object") }) o("setting style.cssText string works", function() { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.style.cssText = "background-color: red; border-bottom: 1px solid red;" o(div.style.backgroundColor).equals("red") @@ -621,7 +471,7 @@ o.spec("domMock", function() { 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") + var div = G.window.document.createElement("div") div.style.cssText = "background: red;" div.style.cssText = "" @@ -629,7 +479,7 @@ o.spec("domMock", function() { o(div.attributes.style.value).equals("") }) o("the final semicolon is optional when setting style.cssText", function() { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.style.cssText = "background: red" o(div.style.background).equals("red") @@ -637,13 +487,13 @@ o.spec("domMock", function() { 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") + var div = G.window.document.createElement("div") div.style.cssText = "cssText: red;" o(div.style.cssText).equals("") }) o("setting style.cssText that has a semi-colon in a strings", function(){ - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.style.cssText = "background: url(';'); font-family: \";\"" o(div.style.background).equals("url(';')") @@ -651,7 +501,7 @@ o.spec("domMock", function() { o(div.style.cssText).equals("background: url(';'); font-family: \";\";") }) o("comments in style.cssText are stripped", function(){ - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.style.cssText = "/**/background/*:*/: /*>;)*/red/**/;/**/" o(div.style.background).equals("red") @@ -659,14 +509,14 @@ o.spec("domMock", function() { }) o("comments in strings in style.cssText are preserved", function(){ - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.style.cssText = "background: url('/*foo*/')" o(div.style.background).equals("url('/*foo*/')") }) o("setting style updates style.cssText", function () { - var div = $document.createElement("div") + var div = G.window.document.createElement("div") div.style = "background: red;" o(div.style.background).equals("red") @@ -679,14 +529,14 @@ o.spec("domMock", function() { var spy, div, e o.beforeEach(function() { spy = o.spy() - div = $document.createElement("div") - e = $document.createEvent("MouseEvents") + div = G.window.document.createElement("div") + e = G.window.document.createEvent("MouseEvents") e.initEvent("click", true, true) - $document.body.appendChild(div) + G.window.document.body.appendChild(div) }) o.afterEach(function() { - $document.body.removeChild(div) + G.window.document.body.removeChild(div) }) o("has onclick", function() { @@ -737,18 +587,18 @@ o.spec("domMock", function() { }) 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) + G.window.document.body.addEventListener("click", spy, true) + G.window.document.body.addEventListener("click", spy, false) + G.window.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) + G.window.document.body.addEventListener("click", spy, true) + G.window.document.body.addEventListener("click", spy, false) + G.window.document.body.removeEventListener("click", spy, false) div.dispatchEvent(e) o(spy.callCount).equals(1) @@ -771,14 +621,14 @@ o.spec("domMock", function() { var spy, div, e o.beforeEach(function() { spy = o.spy() - div = $document.createElement("div") - e = $document.createEvent("HTMLEvents") + div = G.window.document.createElement("div") + e = G.window.document.createEvent("HTMLEvents") e.initEvent("transitionend", true, true) - $document.body.appendChild(div) + G.window.document.body.appendChild(div) }) o.afterEach(function() { - $document.body.removeChild(div) + G.window.document.body.removeChild(div) }) o("ontransitionend does not fire", function(done) { @@ -792,14 +642,14 @@ o.spec("domMock", function() { o.spec("capture and bubbling phases", function() { var div, e o.beforeEach(function() { - div = $document.createElement("div") - e = $document.createEvent("MouseEvents") + div = G.window.document.createElement("div") + e = G.window.document.createEvent("MouseEvents") e.initEvent("click", true, true) - $document.body.appendChild(div) + G.window.document.body.appendChild(div) }) o.afterEach(function() { - $document.body.removeChild(div) + G.window.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 = [] @@ -836,7 +686,7 @@ o.spec("domMock", function() { o(ev).equals(e) o(ev.eventPhase).equals(1) o(ev.target).equals(div) - o(ev.currentTarget).equals($document.body) + o(ev.currentTarget).equals(G.window.document.body) }) var bubble = o.spy(function(ev){ sequence.push("bubble") @@ -844,11 +694,11 @@ o.spec("domMock", function() { o(ev).equals(e) o(ev.eventPhase).equals(3) o(ev.target).equals(div) - o(ev.currentTarget).equals($document.body) + o(ev.currentTarget).equals(G.window.document.body) }) - $document.body.addEventListener("click", bubble, false) - $document.body.addEventListener("click", capture, true) + G.window.document.body.addEventListener("click", bubble, false) + G.window.document.body.addEventListener("click", capture, true) div.dispatchEvent(e) o(capture.callCount).equals(1) @@ -863,7 +713,7 @@ o.spec("domMock", function() { o(ev).equals(e) o(ev.eventPhase).equals(3) o(ev.target).equals(div) - o(ev.currentTarget).equals($document.body) + o(ev.currentTarget).equals(G.window.document.body) }) var target = o.spy(function(ev){ sequence.push("target") @@ -874,7 +724,7 @@ o.spec("domMock", function() { o(ev.currentTarget).equals(div) }) - $document.body.addEventListener("click", parent) + G.window.document.body.addEventListener("click", parent) div.addEventListener("click", target) div.dispatchEvent(e) @@ -890,7 +740,7 @@ o.spec("domMock", function() { o(ev).equals(e) o(ev.eventPhase).equals(3) o(ev.target).equals(div) - o(ev.currentTarget).equals($document.body) + o(ev.currentTarget).equals(G.window.document.body) }) var target = o.spy(function(ev){ sequence.push("target") @@ -901,8 +751,8 @@ o.spec("domMock", function() { o(ev.currentTarget).equals(div) }) - $document.body.addEventListener("click", parent) - $document.body.onclick = parent + G.window.document.body.addEventListener("click", parent) + G.window.document.body.onclick = parent div.addEventListener("click", target) div.dispatchEvent(e) @@ -914,15 +764,15 @@ o.spec("domMock", 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) + o(ev.target).equals(G.window.document.body) + o(ev.currentTarget).equals(G.window.document.body) }) var child = o.spy(function(){ }) - $document.body.addEventListener("click", target) + G.window.document.body.addEventListener("click", target) div.addEventListener("click", child) - $document.body.dispatchEvent(e) + G.window.document.body.dispatchEvent(e) o(target.callCount).equals(1) o(child.callCount).equals(0) @@ -935,9 +785,9 @@ o.spec("domMock", function() { var bubParent = o.spy() var legacyParent = o.spy() - $document.body.addEventListener("click", capParent, true) - $document.body.addEventListener("click", bubParent, false) - $document.body.onclick = legacyParent + G.window.document.body.addEventListener("click", capParent, true) + G.window.document.body.addEventListener("click", bubParent, false) + G.window.document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) @@ -960,9 +810,9 @@ o.spec("domMock", function() { var bubParent = o.spy() var legacyParent = o.spy() - $document.body.addEventListener("click", capParent, true) - $document.body.addEventListener("click", bubParent, false) - $document.body.onclick = legacyParent + G.window.document.body.addEventListener("click", capParent, true) + G.window.document.body.addEventListener("click", bubParent, false) + G.window.document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) @@ -986,9 +836,9 @@ o.spec("domMock", function() { var bubParent = o.spy() var legacyParent = o.spy() - $document.body.addEventListener("click", capParent, true) - $document.body.addEventListener("click", bubParent, false) - $document.body.onclick = legacyParent + G.window.document.body.addEventListener("click", capParent, true) + G.window.document.body.addEventListener("click", bubParent, false) + G.window.document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) @@ -1011,9 +861,9 @@ o.spec("domMock", function() { var bubParent = o.spy() var legacyParent = o.spy() - $document.body.addEventListener("click", capParent, true) - $document.body.addEventListener("click", bubParent, false) - $document.body.onclick = legacyParent + G.window.document.body.addEventListener("click", capParent, true) + G.window.document.body.addEventListener("click", bubParent, false) + G.window.document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) @@ -1036,9 +886,9 @@ o.spec("domMock", function() { 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 + G.window.document.body.addEventListener("click", capParent, true) + G.window.document.body.addEventListener("click", bubParent, false) + G.window.document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) @@ -1061,9 +911,9 @@ o.spec("domMock", function() { 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 + G.window.document.body.addEventListener("click", capParent, true) + G.window.document.body.addEventListener("click", bubParent, false) + G.window.document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) @@ -1086,9 +936,9 @@ o.spec("domMock", function() { var bubParent = o.spy() var legacyParent = o.spy() - $document.body.addEventListener("click", capParent, true) - $document.body.addEventListener("click", bubParent, false) - $document.body.onclick = legacyParent + G.window.document.body.addEventListener("click", capParent, true) + G.window.document.body.addEventListener("click", bubParent, false) + G.window.document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) @@ -1111,9 +961,9 @@ o.spec("domMock", function() { var bubParent = o.spy() var legacyParent = o.spy() - $document.body.addEventListener("click", capParent, true) - $document.body.addEventListener("click", bubParent, false) - $document.body.onclick = legacyParent + G.window.document.body.addEventListener("click", capParent, true) + G.window.document.body.addEventListener("click", bubParent, false) + G.window.document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) @@ -1137,9 +987,9 @@ o.spec("domMock", function() { var bubParent = o.spy() var legacyParent = o.spy() - $document.body.addEventListener("click", capParent, true) - $document.body.addEventListener("click", bubParent, false) - $document.body.onclick = legacyParent + G.window.document.body.addEventListener("click", capParent, true) + G.window.document.body.addEventListener("click", bubParent, false) + G.window.document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) @@ -1162,9 +1012,9 @@ o.spec("domMock", function() { var bubParent = o.spy() var legacyParent = o.spy() - $document.body.addEventListener("click", capParent, true) - $document.body.addEventListener("click", bubParent, false) - $document.body.onclick = legacyParent + G.window.document.body.addEventListener("click", capParent, true) + G.window.document.body.addEventListener("click", bubParent, false) + G.window.document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) @@ -1187,9 +1037,9 @@ o.spec("domMock", function() { 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 + G.window.document.body.addEventListener("click", capParent, true) + G.window.document.body.addEventListener("click", bubParent, false) + G.window.document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) @@ -1212,9 +1062,9 @@ o.spec("domMock", function() { 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 + G.window.document.body.addEventListener("click", capParent, true) + G.window.document.body.addEventListener("click", bubParent, false) + G.window.document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) @@ -1229,13 +1079,14 @@ o.spec("domMock", function() { 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}) + o("errors thrown in handlers don't interrupt the chain", function() { + var handler = o.spy(() => {throw "fail"}) + + console.error = o.spy() - $document.body.addEventListener("click", handler, true) - $document.body.addEventListener("click", handler, false) - $document.body.onclick = handler + G.window.document.body.addEventListener("click", handler, true) + G.window.document.body.addEventListener("click", handler, false) + G.window.document.body.onclick = handler div.addEventListener("click", handler, true) div.addEventListener("click", handler, false) @@ -1245,57 +1096,37 @@ o.spec("domMock", function() { 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(console.error.calls.map((c) => c.args)).deepEquals([ + ["fail"], ["fail"], ["fail"], + ["fail"], ["fail"], ["fail"], + ]) }) }) }) o.spec("attributes", function() { o.spec("a[href]", function() { o("is empty string if no attribute", function() { - var a = $document.createElement("a") + var a = G.window.document.createElement("a") o(a.href).equals("") o(a.attributes["href"]).equals(undefined) }) o("is path if attribute is set", function() { - var a = $document.createElement("a") + var a = G.window.document.createElement("a") a.setAttribute("href", "") o(a.href).notEquals("") o(a.attributes["href"].value).equals("") }) o("is path if property is set", function() { - var a = $document.createElement("a") + var a = G.window.document.createElement("a") a.href = "" 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") + var a = G.window.document.createElementNS("http://www.w3.org/2000/svg", "a") a.href = "/foo" o(a.href).deepEquals({baseVal: "", animVal: ""}) @@ -1304,14 +1135,14 @@ o.spec("domMock", function() { }) o.spec("input[checked]", function() { o("only exists in input elements", function() { - var input = $document.createElement("input") - var a = $document.createElement("a") + var input = G.window.document.createElement("input") + var a = G.window.document.createElement("a") o("checked" in input).equals(true) o("checked" in a).equals(false) }) o("tracks attribute value when unset", function() { - var input = $document.createElement("input") + var input = G.window.document.createElement("input") input.setAttribute("type", "checkbox") o(input.checked).equals(false) @@ -1328,7 +1159,7 @@ o.spec("domMock", function() { o(input.attributes["checked"]).equals(undefined) }) o("does not track attribute value when set", function() { - var input = $document.createElement("input") + var input = G.window.document.createElement("input") input.setAttribute("type", "checkbox") input.checked = true @@ -1344,23 +1175,23 @@ o.spec("domMock", function() { o(input.checked).equals(true) }) o("toggles on click", function() { - var input = $document.createElement("input") + var input = G.window.document.createElement("input") input.setAttribute("type", "checkbox") input.checked = false - var e = $document.createEvent("MouseEvents") + var e = G.window.document.createEvent("MouseEvents") e.initEvent("click", true, true) input.dispatchEvent(e) o(input.checked).equals(true) }) o("doesn't toggle on click when preventDefault() is used", function() { - var input = $document.createElement("input") + var input = G.window.document.createElement("input") input.setAttribute("type", "checkbox") input.checked = false input.onclick = function(e) {e.preventDefault()} - var e = $document.createEvent("MouseEvents") + var e = G.window.document.createEvent("MouseEvents") e.initEvent("click", true, true) input.dispatchEvent(e) @@ -1369,14 +1200,14 @@ o.spec("domMock", function() { }) o.spec("input[value]", function() { o("only exists in input elements", function() { - var input = $document.createElement("input") - var a = $document.createElement("a") + var input = G.window.document.createElement("input") + var a = G.window.document.createElement("a") o("value" in input).equals(true) o("value" in a).equals(false) }) o("converts null to ''", function() { - var input = $document.createElement("input") + var input = G.window.document.createElement("input") input.value = "x" o(input.value).equals("x") @@ -1386,7 +1217,7 @@ o.spec("domMock", function() { o(input.value).equals("") }) o("converts values to strings", function() { - var input = $document.createElement("input") + var input = G.window.document.createElement("input") input.value = 5 o(input.value).equals("5") @@ -1401,7 +1232,7 @@ o.spec("domMock", function() { }) if (typeof Symbol === "function") o("throws when set to a symbol", function() { var threw = false - var input = $document.createElement("input") + var input = G.window.document.createElement("input") try { input.value = Symbol("") } catch (e) { @@ -1415,28 +1246,28 @@ o.spec("domMock", function() { }) o.spec("input[type]", function(){ o("only exists in input elements", function() { - var input = $document.createElement("input") - var a = $document.createElement("a") + var input = G.window.document.createElement("input") + var a = G.window.document.createElement("a") o("type" in input).equals(true) o("type" in a).equals(false) }) o("is 'text' by default", function() { - var input = $document.createElement("input") + var input = G.window.document.createElement("input") o(input.type).equals("text") }) "radio|button|checkbox|color|date|datetime|datetime-local|email|file|hidden|month|number|password|range|research|search|submit|tel|text|url|week|image" .split("|").forEach(function(type) { o("can be set to " + type, function(){ - var input = $document.createElement("input") + var input = G.window.document.createElement("input") input.type = type o(input.getAttribute("type")).equals(type) o(input.type).equals(type) }) o("bad values set the attribute, but the getter corrects to 'text', " + type, function(){ - var input = $document.createElement("input") + var input = G.window.document.createElement("input") input.type = "badbad" + type o(input.getAttribute("type")).equals("badbad" + type) @@ -1446,20 +1277,20 @@ o.spec("domMock", function() { }) o.spec("textarea[value]", function() { o("reads from child if no value was ever set", function() { - var textarea = $document.createElement("textarea") - textarea.appendChild($document.createTextNode("aaa")) + var textarea = G.window.document.createElement("textarea") + textarea.appendChild(G.window.document.createTextNode("aaa")) o(textarea.value).equals("aaa") }) o("ignores child if value set", function() { - var textarea = $document.createElement("textarea") + var textarea = G.window.document.createElement("textarea") textarea.value = null - textarea.appendChild($document.createTextNode("aaa")) + textarea.appendChild(G.window.document.createTextNode("aaa")) o(textarea.value).equals("") }) o("textarea[value] doesn't reflect `attributes.value`", function() { - var textarea = $document.createElement("textarea") + var textarea = G.window.document.createElement("textarea") textarea.value = "aaa" textarea.setAttribute("value", "bbb") @@ -1468,8 +1299,8 @@ o.spec("domMock", function() { }) o.spec("select[value] and select[selectedIndex]", function() { o("only exist in select elements", function() { - var select = $document.createElement("select") - var a = $document.createElement("a") + var select = G.window.document.createElement("select") + var a = G.window.document.createElement("a") o("value" in select).equals(true) o("value" in a).equals(false) @@ -1478,13 +1309,13 @@ o.spec("domMock", function() { o("selectedIndex" in a).equals(false) }) o("value defaults to value at first index", function() { - var select = $document.createElement("select") + var select = G.window.document.createElement("select") - var option1 = $document.createElement("option") + var option1 = G.window.document.createElement("option") option1.setAttribute("value", "a") select.appendChild(option1) - var option2 = $document.createElement("option") + var option2 = G.window.document.createElement("option") option2.setAttribute("value", "b") select.appendChild(option2) @@ -1492,12 +1323,12 @@ o.spec("domMock", function() { o(select.selectedIndex).equals(0) }) o("value falls back to child nodeValue if no attribute", function() { - var select = $document.createElement("select") + var select = G.window.document.createElement("select") - var option1 = $document.createElement("option") - option1.appendChild($document.createTextNode("a")) - var option2 = $document.createElement("option") - option2.appendChild($document.createTextNode("b")) + var option1 = G.window.document.createElement("option") + option1.appendChild(G.window.document.createTextNode("a")) + var option2 = G.window.document.createElement("option") + option2.appendChild(G.window.document.createTextNode("b")) select.appendChild(option1) select.appendChild(option2) @@ -1508,27 +1339,27 @@ o.spec("domMock", function() { o(select.childNodes[1].value).equals("b") }) o("value defaults to invalid if no options", function() { - var select = $document.createElement("select") + var select = G.window.document.createElement("select") o(select.value).equals("") o(select.selectedIndex).equals(-1) }) o("setting valid value works", function() { - var select = $document.createElement("select") + var select = G.window.document.createElement("select") - var option1 = $document.createElement("option") + var option1 = G.window.document.createElement("option") option1.setAttribute("value", "a") select.appendChild(option1) - var option2 = $document.createElement("option") + var option2 = G.window.document.createElement("option") option2.setAttribute("value", "b") select.appendChild(option2) - var option3 = $document.createElement("option") + var option3 = G.window.document.createElement("option") option3.setAttribute("value", "") select.appendChild(option3) - var option4 = $document.createElement("option") + var option4 = G.window.document.createElement("option") option4.setAttribute("value", "null") select.appendChild(option4) @@ -1553,17 +1384,17 @@ o.spec("domMock", function() { o(select.selectedIndex).equals(-1) }) o("setting valid value works with type conversion", function() { - var select = $document.createElement("select") + var select = G.window.document.createElement("select") - var option1 = $document.createElement("option") + var option1 = G.window.document.createElement("option") option1.setAttribute("value", "0") select.appendChild(option1) - var option2 = $document.createElement("option") + var option2 = G.window.document.createElement("option") option2.setAttribute("value", "undefined") select.appendChild(option2) - var option3 = $document.createElement("option") + var option3 = G.window.document.createElement("option") option3.setAttribute("value", "") select.appendChild(option3) @@ -1590,24 +1421,24 @@ o.spec("domMock", function() { } }) o("option.value = null is converted to 'null'", function() { - var option = $document.createElement("option") + var option = G.window.document.createElement("option") option.value = null o(option.value).equals("null") }) o("setting valid value works with optgroup", function() { - var select = $document.createElement("select") + var select = G.window.document.createElement("select") - var option1 = $document.createElement("option") + var option1 = G.window.document.createElement("option") option1.setAttribute("value", "a") - var option2 = $document.createElement("option") + var option2 = G.window.document.createElement("option") option2.setAttribute("value", "b") - var option3 = $document.createElement("option") + var option3 = G.window.document.createElement("option") option3.setAttribute("value", "c") - var optgroup = $document.createElement("optgroup") + var optgroup = G.window.document.createElement("optgroup") optgroup.appendChild(option1) optgroup.appendChild(option2) select.appendChild(optgroup) @@ -1619,13 +1450,13 @@ o.spec("domMock", function() { o(select.selectedIndex).equals(1) }) o("setting valid selectedIndex works", function() { - var select = $document.createElement("select") + var select = G.window.document.createElement("select") - var option1 = $document.createElement("option") + var option1 = G.window.document.createElement("option") option1.setAttribute("value", "a") select.appendChild(option1) - var option2 = $document.createElement("option") + var option2 = G.window.document.createElement("option") option2.setAttribute("value", "b") select.appendChild(option2) @@ -1635,13 +1466,13 @@ o.spec("domMock", function() { o(select.selectedIndex).equals(1) }) o("setting option[selected] works", function() { - var select = $document.createElement("select") + var select = G.window.document.createElement("select") - var option1 = $document.createElement("option") + var option1 = G.window.document.createElement("option") option1.setAttribute("value", "a") select.appendChild(option1) - var option2 = $document.createElement("option") + var option2 = G.window.document.createElement("option") option2.setAttribute("value", "b") select.appendChild(option2) @@ -1651,13 +1482,13 @@ o.spec("domMock", function() { o(select.selectedIndex).equals(1) }) o("unsetting option[selected] works", function() { - var select = $document.createElement("select") + var select = G.window.document.createElement("select") - var option1 = $document.createElement("option") + var option1 = G.window.document.createElement("option") option1.setAttribute("value", "a") select.appendChild(option1) - var option2 = $document.createElement("option") + var option2 = G.window.document.createElement("option") option2.setAttribute("value", "b") select.appendChild(option2) @@ -1668,13 +1499,13 @@ o.spec("domMock", function() { o(select.selectedIndex).equals(0) }) o("setting invalid value yields a selectedIndex of -1 and value of empty string", function() { - var select = $document.createElement("select") + var select = G.window.document.createElement("select") - var option1 = $document.createElement("option") + var option1 = G.window.document.createElement("option") option1.setAttribute("value", "a") select.appendChild(option1) - var option2 = $document.createElement("option") + var option2 = G.window.document.createElement("option") option2.setAttribute("value", "b") select.appendChild(option2) @@ -1684,13 +1515,13 @@ o.spec("domMock", function() { o(select.selectedIndex).equals(-1) }) o("setting invalid selectedIndex yields a selectedIndex of -1 and value of empty string", function() { - var select = $document.createElement("select") + var select = G.window.document.createElement("select") - var option1 = $document.createElement("option") + var option1 = G.window.document.createElement("option") option1.setAttribute("value", "a") select.appendChild(option1) - var option2 = $document.createElement("option") + var option2 = G.window.document.createElement("option") option2.setAttribute("value", "b") select.appendChild(option2) @@ -1700,13 +1531,13 @@ o.spec("domMock", function() { o(select.selectedIndex).equals(-1) }) o("setting invalid value yields a selectedIndex of -1 and value of empty string even when there's an option whose value is empty string", function() { - var select = $document.createElement("select") + var select = G.window.document.createElement("select") - var option1 = $document.createElement("option") + var option1 = G.window.document.createElement("option") option1.setAttribute("value", "a") select.appendChild(option1) - var option2 = $document.createElement("option") + var option2 = G.window.document.createElement("option") option2.setAttribute("value", "") select.appendChild(option2) @@ -1716,13 +1547,13 @@ o.spec("domMock", function() { o(select.selectedIndex).equals(-1) }) o("setting invalid selectedIndex yields a selectedIndex of -1 and value of empty string even when there's an option whose value is empty string", function() { - var select = $document.createElement("select") + var select = G.window.document.createElement("select") - var option1 = $document.createElement("option") + var option1 = G.window.document.createElement("option") option1.setAttribute("value", "a") select.appendChild(option1) - var option2 = $document.createElement("option") + var option2 = G.window.document.createElement("option") option2.setAttribute("value", "") select.appendChild(option2) @@ -1734,7 +1565,7 @@ o.spec("domMock", function() { }) o.spec("canvas width and height", function() { o("setting property works", function() { - var canvas = $document.createElement("canvas") + var canvas = G.window.document.createElement("canvas") canvas.width = 100 o(canvas.attributes["width"].value).equals("100") @@ -1745,7 +1576,7 @@ o.spec("domMock", function() { o(canvas.height).equals(100) }) o("setting string casts to number", function() { - var canvas = $document.createElement("canvas") + var canvas = G.window.document.createElement("canvas") canvas.width = "100" o(canvas.attributes["width"].value).equals("100") @@ -1756,7 +1587,7 @@ o.spec("domMock", function() { o(canvas.height).equals(100) }) o("setting float casts to int", function() { - var canvas = $document.createElement("canvas") + var canvas = G.window.document.createElement("canvas") canvas.width = 1.2 o(canvas.attributes["width"].value).equals("1") @@ -1767,7 +1598,7 @@ o.spec("domMock", function() { o(canvas.height).equals(1) }) o("setting percentage fails", function() { - var canvas = $document.createElement("canvas") + var canvas = G.window.document.createElement("canvas") canvas.width = "100%" o(canvas.attributes["width"].value).equals("0") @@ -1778,7 +1609,7 @@ o.spec("domMock", function() { o(canvas.height).equals(0) }) o("setting attribute works", function() { - var canvas = $document.createElement("canvas") + var canvas = G.window.document.createElement("canvas") canvas.setAttribute("width", "100%") o(canvas.attributes["width"].value).equals("100%") @@ -1792,14 +1623,14 @@ o.spec("domMock", function() { }) o.spec("className", function() { o("works", function() { - var el = $document.createElement("div") + var el = G.window.document.createElement("div") el.className = "a" o(el.className).equals("a") o(el.attributes["class"].value).equals("a") }) o("setter throws in svg", function(done) { - var el = $document.createElementNS("http://www.w3.org/2000/svg", "svg") + var el = G.window.document.createElementNS("http://www.w3.org/2000/svg", "svg") try { el.className = "a" } @@ -1809,41 +1640,34 @@ o.spec("domMock", function() { }) }) o.spec("spies", function() { - var $window o.beforeEach(function() { - $window = domMock({spy: o.spy}) + G.initialize({spy: o.spy}) }) o("basics", function() { - o(typeof $window.__getSpies).equals("function") - o("__getSpies" in domMock()).equals(false) + o(typeof G.window.__getSpies).equals("function") + G.initialize() + o("__getSpies" in G.window).equals(false) }) - o("input elements have spies on value and type setters", function() { - var input = $window.document.createElement("input") + o("input elements have spies on value setters", function() { + var input = G.window.document.createElement("input") - var spies = $window.__getSpies(input) + var spies = G.window.__getSpies(input) o(typeof spies).equals("object") o(spies).notEquals(null) o(typeof spies.valueSetter).equals("function") - o(typeof spies.typeSetter).equals("function") o(spies.valueSetter.callCount).equals(0) - o(spies.typeSetter.callCount).equals(0) input.value = "aaa" - input.type = "radio" o(spies.valueSetter.callCount).equals(1) o(spies.valueSetter.this).equals(input) o(spies.valueSetter.args[0]).equals("aaa") - - o(spies.typeSetter.callCount).equals(1) - o(spies.typeSetter.this).equals(input) - o(spies.typeSetter.args[0]).equals("radio") }) o("select elements have spies on value setters", function() { - var select = $window.document.createElement("select") + var select = G.window.document.createElement("select") - var spies = $window.__getSpies(select) + var spies = G.window.__getSpies(select) o(typeof spies).equals("object") o(spies).notEquals(null) @@ -1857,9 +1681,9 @@ o.spec("domMock", function() { o(spies.valueSetter.args[0]).equals("aaa") }) o("option elements have spies on value setters", function() { - var option = $window.document.createElement("option") + var option = G.window.document.createElement("option") - var spies = $window.__getSpies(option) + var spies = G.window.__getSpies(option) o(typeof spies).equals("object") o(spies).notEquals(null) @@ -1873,9 +1697,9 @@ o.spec("domMock", function() { o(spies.valueSetter.args[0]).equals("aaa") }) o("textarea elements have spies on value setters", function() { - var textarea = $window.document.createElement("textarea") + var textarea = G.window.document.createElement("textarea") - var spies = $window.__getSpies(textarea) + var spies = G.window.__getSpies(textarea) o(typeof spies).equals("object") o(spies).notEquals(null) @@ -1889,62 +1713,4 @@ 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( - "", - "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( - "" - + "hello" - + " " - + "world" - + "", - "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") - }) - }) }) diff --git a/test-utils/tests/test-parseURL.js b/tests/test-utils/parseURL.js similarity index 98% rename from test-utils/tests/test-parseURL.js rename to tests/test-utils/parseURL.js index 5a1e3567d..53a0ac1e4 100644 --- a/test-utils/tests/test-parseURL.js +++ b/tests/test-utils/parseURL.js @@ -1,7 +1,6 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var parseURL = require("../../test-utils/parseURL") +import parseURL from "../../test-utils/parseURL.js" o.spec("parseURL", function() { var root = {protocol: "http:", hostname: "localhost", port: "", pathname: "/"} diff --git a/tests/test-utils/pushStateMock.js b/tests/test-utils/pushStateMock.js new file mode 100644 index 000000000..413dce4a2 --- /dev/null +++ b/tests/test-utils/pushStateMock.js @@ -0,0 +1,697 @@ +import o from "ospec" + +import {callAsync, waitAsync} from "../../test-utils/callAsync.js" +import {setupGlobals} from "../../test-utils/global.js" + +o.spec("pushStateMock", function() { + var G = setupGlobals() + + o.spec("initial state", function() { + o("has url on page load", function() { + o(G.window.location.href).equals("http://localhost/") + }) + }) + + o.spec("set href", function() { + o("changes url on location.href change", function() { + var old = G.window.location.href + G.window.location.href = "http://localhost/a" + + o(old).equals("http://localhost/") + o(G.window.location.href).equals("http://localhost/a") + }) + o("changes url on relative location.href change", function() { + var old = G.window.location.href + G.window.location.href = "a" + + o(old).equals("http://localhost/") + o(G.window.location.href).equals("http://localhost/a") + o(G.window.location.pathname).equals("/a") + }) + o("changes url on dotdot location.href change", function() { + G.window.location.href = "a" + var old = G.window.location.href + G.window.location.href = ".." + + o(old).equals("http://localhost/a") + o(G.window.location.href).equals("http://localhost/") + o(G.window.location.pathname).equals("/") + }) + o("changes url on deep dotdot location.href change", function() { + G.window.location.href = "a/b/c" + var old = G.window.location.href + G.window.location.href = ".." + + o(old).equals("http://localhost/a/b/c") + o(G.window.location.href).equals("http://localhost/a") + o(G.window.location.pathname).equals("/a") + }) + o("does not change url on dotdot location.href change from root", function() { + var old = G.window.location.href + G.window.location.href = ".." + + o(old).equals("http://localhost/") + o(G.window.location.href).equals("http://localhost/") + o(G.window.location.pathname).equals("/") + }) + o("changes url on dot relative location.href change", function() { + var old = G.window.location.href + G.window.location.href = "a" + G.window.location.href = "./b" + + o(old).equals("http://localhost/") + o(G.window.location.href).equals("http://localhost/b") + o(G.window.location.pathname).equals("/b") + }) + o("does not change url on dot location.href change", function() { + var old = G.window.location.href + G.window.location.href = "a" + G.window.location.href = "." + + o(old).equals("http://localhost/") + o(G.window.location.href).equals("http://localhost/a") + o(G.window.location.pathname).equals("/a") + }) + o("changes url on hash-only location.href change", function() { + var old = G.window.location.href + G.window.location.href = "#a" + + o(old).equals("http://localhost/") + o(G.window.location.href).equals("http://localhost/#a") + o(G.window.location.hash).equals("#a") + }) + o("changes url on search-only location.href change", function() { + var old = G.window.location.href + G.window.location.href = "?a" + + o(old).equals("http://localhost/") + o(G.window.location.href).equals("http://localhost/?a") + o(G.window.location.search).equals("?a") + }) + o("changes hash on location.href change", function() { + var old = G.window.location.href + G.window.location.href = "http://localhost/a#b" + + o(old).equals("http://localhost/") + o(G.window.location.href).equals("http://localhost/a#b") + o(G.window.location.hash).equals("#b") + }) + o("changes search on location.href change", function() { + var old = G.window.location.href + G.window.location.href = "http://localhost/a?b" + + o(old).equals("http://localhost/") + o(G.window.location.href).equals("http://localhost/a?b") + o(G.window.location.search).equals("?b") + }) + o("changes search and hash on location.href change", function() { + var old = G.window.location.href + G.window.location.href = "http://localhost/a?b#c" + + o(old).equals("http://localhost/") + o(G.window.location.href).equals("http://localhost/a?b#c") + o(G.window.location.search).equals("?b") + o(G.window.location.hash).equals("#c") + }) + o("handles search with search and hash", function() { + var old = G.window.location.href + G.window.location.href = "http://localhost/a?b?c#d" + + o(old).equals("http://localhost/") + o(G.window.location.href).equals("http://localhost/a?b?c#d") + o(G.window.location.search).equals("?b?c") + o(G.window.location.hash).equals("#d") + }) + o("handles hash with search and hash", function() { + var old = G.window.location.href + G.window.location.href = "http://localhost/a#b?c#d" + + o(old).equals("http://localhost/") + o(G.window.location.href).equals("http://localhost/a#b?c#d") + o(G.window.location.search).equals("") + o(G.window.location.hash).equals("#b?c#d") + }) + }) + o.spec("set search", function() { + o("changes url on location.search change", function() { + var old = G.window.location.href + G.window.location.search = "?b" + + o(old).equals("http://localhost/") + o(G.window.location.href).equals("http://localhost/?b") + o(G.window.location.search).equals("?b") + }) + }) + o.spec("set hash", function() { + o("changes url on location.hash change", function() { + var old = G.window.location.href + G.window.location.hash = "#b" + + o(old).equals("http://localhost/") + o(G.window.location.href).equals("http://localhost/#b") + o(G.window.location.hash).equals("#b") + }) + }) + o.spec("set pathname", function() { + o("changes url on location.pathname change", function() { + var old = G.window.location.href + G.window.location.pathname = "/a" + + o(old).equals("http://localhost/") + o(G.window.location.href).equals("http://localhost/a") + o(G.window.location.pathname).equals("/a") + }) + }) + o.spec("set protocol", function() { + o("setting protocol throws", function(done) { + try { + G.window.location.protocol = "https://" + } + catch (e) { + return done() + } + throw new Error("Expected an error") + }) + }) + o.spec("set port", function() { + o("setting origin changes href", function() { + var old = G.window.location.href + G.window.location.port = "81" + + o(old).equals("http://localhost/") + o(G.window.location.port).equals("81") + o(G.window.location.href).equals("http://localhost:81/") + }) + }) + o.spec("set hostname", function() { + o("setting hostname changes href", function() { + var old = G.window.location.href + G.window.location.hostname = "127.0.0.1" + + o(old).equals("http://localhost/") + o(G.window.location.hostname).equals("127.0.0.1") + o(G.window.location.href).equals("http://127.0.0.1/") + }) + }) + o.spec("set origin", function() { + o("setting origin is ignored", function() { + var old = G.window.location.href + G.window.location.origin = "http://127.0.0.1" + + o(old).equals("http://localhost/") + o(G.window.location.origin).equals("http://localhost") + }) + }) + o.spec("set host", function() { + o("setting host is ignored", function() { + var old = G.window.location.href + G.window.location.host = "http://127.0.0.1" + + o(old).equals("http://localhost/") + o(G.window.location.host).equals("localhost") + }) + }) + o.spec("pushState", function() { + o("changes url on pushstate", function() { + var old = G.window.location.href + G.window.history.pushState(null, null, "http://localhost/a") + + o(old).equals("http://localhost/") + o(G.window.location.href).equals("http://localhost/a") + }) + o("changes search on pushstate", function() { + var old = G.window.location.href + G.window.history.pushState(null, null, "http://localhost/?a") + + o(old).equals("http://localhost/") + o(G.window.location.href).equals("http://localhost/?a") + o(G.window.location.search).equals("?a") + }) + o("changes search on relative pushstate", function() { + var old = G.window.location.href + G.window.history.pushState(null, null, "?a") + + o(old).equals("http://localhost/") + o(G.window.location.href).equals("http://localhost/?a") + o(G.window.location.search).equals("?a") + }) + o("changes hash on pushstate", function() { + var old = G.window.location.href + G.window.history.pushState(null, null, "http://localhost/#a") + + o(old).equals("http://localhost/") + o(G.window.location.href).equals("http://localhost/#a") + o(G.window.location.hash).equals("#a") + }) + o("changes hash on relative pushstate", function() { + var old = G.window.location.href + G.window.history.pushState(null, null, "#a") + + o(old).equals("http://localhost/") + o(G.window.location.href).equals("http://localhost/#a") + o(G.window.location.hash).equals("#a") + }) + }) + o.spec("onpopstate", function() { + o("history.back() without history does not trigger onpopstate", function() { + G.window.onpopstate = o.spy() + G.window.history.back() + + o(G.window.onpopstate.callCount).equals(0) + }) + o("history.back() after pushstate triggers onpopstate", function() { + G.window.onpopstate = o.spy() + G.window.history.pushState(null, null, "http://localhost/a") + G.window.history.back() + + o(G.window.onpopstate.callCount).equals(1) + o(G.window.onpopstate.args[0].type).equals("popstate") + }) + o("history.back() after relative pushstate triggers onpopstate", function() { + G.window.onpopstate = o.spy() + G.window.history.pushState(null, null, "a") + G.window.history.back() + + o(G.window.onpopstate.callCount).equals(1) + }) + o("history.back() after search pushstate triggers onpopstate", function() { + G.window.onpopstate = o.spy() + G.window.history.pushState(null, null, "http://localhost/?a") + G.window.history.back() + + o(G.window.onpopstate.callCount).equals(1) + }) + o("history.back() after relative search pushstate triggers onpopstate", function() { + G.window.onpopstate = o.spy() + G.window.history.pushState(null, null, "?a") + G.window.history.back() + + o(G.window.onpopstate.callCount).equals(1) + }) + o("history.back() after hash pushstate triggers onpopstate", function() { + G.window.onpopstate = o.spy() + G.window.history.pushState(null, null, "http://localhost/#a") + G.window.history.back() + + o(G.window.onpopstate.callCount).equals(1) + }) + o("history.back() after relative hash pushstate triggers onpopstate", function() { + G.window.onpopstate = o.spy() + G.window.history.pushState(null, null, "#a") + G.window.history.back() + + o(G.window.onpopstate.callCount).equals(1) + }) + o("history.back() after replacestate does not trigger onpopstate", function() { + G.window.onpopstate = o.spy() + G.window.history.replaceState(null, null, "http://localhost/a") + G.window.history.back() + + o(G.window.onpopstate.callCount).equals(0) + }) + o("history.back() after relative replacestate does not trigger onpopstate", function() { + G.window.onpopstate = o.spy() + G.window.history.replaceState(null, null, "a") + G.window.history.back() + + o(G.window.onpopstate.callCount).equals(0) + }) + o("history.back() after relative search replacestate does not trigger onpopstate", function() { + G.window.onpopstate = o.spy() + G.window.history.replaceState(null, null, "?a") + G.window.history.back() + + o(G.window.onpopstate.callCount).equals(0) + }) + o("history.back() after relative hash replacestate does not trigger onpopstate", function() { + G.window.onpopstate = o.spy() + G.window.history.replaceState(null, null, "#a") + G.window.history.back() + + o(G.window.onpopstate.callCount).equals(0) + }) + o("history.forward() after pushstate triggers onpopstate", function() { + G.window.onpopstate = o.spy() + G.window.history.pushState(null, null, "http://localhost/a") + G.window.history.back() + G.window.history.forward() + + o(G.window.onpopstate.callCount).equals(2) + }) + o("history.forward() after relative pushstate triggers onpopstate", function() { + G.window.onpopstate = o.spy() + G.window.history.pushState(null, null, "a") + G.window.history.back() + G.window.history.forward() + + o(G.window.onpopstate.callCount).equals(2) + }) + o("history.forward() after search pushstate triggers onpopstate", function() { + G.window.onpopstate = o.spy() + G.window.history.pushState(null, null, "http://localhost/?a") + G.window.history.back() + G.window.history.forward() + + o(G.window.onpopstate.callCount).equals(2) + }) + o("history.forward() after relative search pushstate triggers onpopstate", function() { + G.window.onpopstate = o.spy() + G.window.history.pushState(null, null, "?a") + G.window.history.back() + G.window.history.forward() + + o(G.window.onpopstate.callCount).equals(2) + }) + o("history.forward() after hash pushstate triggers onpopstate", function() { + G.window.onpopstate = o.spy() + G.window.history.pushState(null, null, "http://localhost/#a") + G.window.history.back() + G.window.history.forward() + + o(G.window.onpopstate.callCount).equals(2) + }) + o("history.forward() after relative hash pushstate triggers onpopstate", function() { + G.window.onpopstate = o.spy() + G.window.history.pushState(null, null, "#a") + G.window.history.back() + G.window.history.forward() + + o(G.window.onpopstate.callCount).equals(2) + }) + o("history.forward() without history does not trigger onpopstate", function() { + G.window.onpopstate = o.spy() + G.window.history.forward() + + o(G.window.onpopstate.callCount).equals(0) + }) + o("history navigation without history does not trigger onpopstate", function() { + G.window.onpopstate = o.spy() + G.window.history.back() + G.window.history.forward() + + o(G.window.onpopstate.callCount).equals(0) + }) + o("reverse history navigation without history does not trigger onpopstate", function() { + G.window.onpopstate = o.spy() + G.window.history.forward() + G.window.history.back() + + o(G.window.onpopstate.callCount).equals(0) + }) + o("onpopstate has correct url during call", function(done) { + G.window.location.href = "a" + G.window.onpopstate = function() { + o(G.window.location.href).equals("http://localhost/a") + done() + } + G.window.history.pushState(null, null, "b") + G.window.history.back() + }) + o("replaceState does not break forward history", function() { + G.window.onpopstate = o.spy() + + G.window.history.pushState(null, null, "b") + G.window.history.back() + + o(G.window.onpopstate.callCount).equals(1) + o(G.window.location.href).equals("http://localhost/") + + G.window.history.replaceState(null, null, "a") + + o(G.window.location.href).equals("http://localhost/a") + + G.window.history.forward() + + o(G.window.onpopstate.callCount).equals(2) + o(G.window.location.href).equals("http://localhost/b") + }) + o("pushstate retains state", function() { + G.window.onpopstate = o.spy() + + G.window.history.pushState({a: 1}, null, "#a") + G.window.history.pushState({b: 2}, null, "#b") + + o(G.window.onpopstate.callCount).equals(0) + + G.window.history.back() + + o(G.window.onpopstate.callCount).equals(1) + o(G.window.onpopstate.args[0].type).equals("popstate") + o(G.window.onpopstate.args[0].state).deepEquals({a: 1}) + + G.window.history.back() + + o(G.window.onpopstate.callCount).equals(2) + o(G.window.onpopstate.args[0].type).equals("popstate") + o(G.window.onpopstate.args[0].state).equals(null) + + G.window.history.forward() + + o(G.window.onpopstate.callCount).equals(3) + o(G.window.onpopstate.args[0].type).equals("popstate") + o(G.window.onpopstate.args[0].state).deepEquals({a: 1}) + + G.window.history.forward() + + o(G.window.onpopstate.callCount).equals(4) + o(G.window.onpopstate.args[0].type).equals("popstate") + o(G.window.onpopstate.args[0].state).deepEquals({b: 2}) + }) + o("replacestate replaces state", function() { + G.window.onpopstate = o.spy(pop) + + G.window.history.replaceState({a: 1}, null, "a") + + o(G.window.history.state).deepEquals({a: 1}) + + G.window.history.pushState(null, null, "a") + G.window.history.back() + + function pop(e) { + o(e.state).deepEquals({a: 1}) + o(G.window.history.state).deepEquals({a: 1}) + } + }) + }) + o.spec("onhashchance", function() { + o("onhashchange triggers on location.href change", function(done) { + G.window.onhashchange = o.spy() + G.window.location.href = "http://localhost/#a" + + callAsync(function(){ + o(G.window.onhashchange.callCount).equals(1) + o(G.window.onhashchange.args[0].type).equals("hashchange") + done() + }) + }) + o("onhashchange triggers on relative location.href change", function(done) { + G.window.onhashchange = o.spy() + G.window.location.href = "#a" + + callAsync(function(){ + o(G.window.onhashchange.callCount).equals(1) + done() + }) + }) + o("onhashchange triggers on location.hash change", function(done) { + G.window.onhashchange = o.spy() + G.window.location.hash = "#a" + + callAsync(function(){ + o(G.window.onhashchange.callCount).equals(1) + done() + }) + }) + o("onhashchange does not trigger on page change", function(done) { + G.window.onhashchange = o.spy() + G.window.location.href = "http://localhost/a" + + callAsync(function(){ + o(G.window.onhashchange.callCount).equals(0) + done() + }) + }) + o("onhashchange does not trigger on page change with different hash", function(done) { + G.window.location.href = "http://localhost/#a" + callAsync(function(){ + G.window.onhashchange = o.spy() + G.window.location.href = "http://localhost/a#b" + + callAsync(function(){ + o(G.window.onhashchange.callCount).equals(0) + done() + }) + }) + }) + o("onhashchange does not trigger on page change with same hash", function(done) { + G.window.location.href = "http://localhost/#b" + callAsync(function(){ + G.window.onhashchange = o.spy() + G.window.location.href = "http://localhost/a#b" + + callAsync(function(){ + o(G.window.onhashchange.callCount).equals(0) + done() + }) + }) + }) + o("onhashchange triggers on history.back()", function(done) { + G.window.location.href = "#a" + callAsync(function(){ + G.window.onhashchange = o.spy() + G.window.history.back() + + callAsync(function(){ + o(G.window.onhashchange.callCount).equals(1) + done() + }) + }) + }) + o("onhashchange triggers on history.forward()", function(done) { + G.window.location.href = "#a" + callAsync(function(){ + G.window.onhashchange = o.spy() + G.window.history.back() + callAsync(function(){ + G.window.history.forward() + + callAsync(function(){ + o(G.window.onhashchange.callCount).equals(2) + done() + }) + }) + }) + }) + o("onhashchange triggers once when the hash changes twice in a single tick", async () => { + G.window.location.href = "#a" + await waitAsync() + G.window.onhashchange = o.spy() + G.window.history.back() + G.window.history.forward() + await waitAsync() + o(G.window.onhashchange.callCount).equals(1) + }) + o("onhashchange does not trigger on history.back() that causes page change with different hash", function(done) { + G.window.location.href = "#a" + G.window.location.href = "a#b" + callAsync(function(){ + G.window.onhashchange = o.spy() + G.window.history.back() + + callAsync(function(){ + o(G.window.onhashchange.callCount).equals(0) + done() + }) + }) + }) + o("onhashchange does not trigger on history.back() that causes page change with same hash", function(done) { + G.window.location.href = "#a" + G.window.location.href = "a#a" + callAsync(function(){ + G.window.onhashchange = o.spy() + G.window.history.back() + + callAsync(function(){ + o(G.window.onhashchange.callCount).equals(0) + done() + }) + }) + }) + o("onhashchange does not trigger on history.forward() that causes page change with different hash", function(done) { + G.window.location.href = "#a" + G.window.location.href = "a#b" + callAsync(function(){ + G.window.onhashchange = o.spy() + G.window.history.back() + G.window.history.forward() + + callAsync(function(){ + o(G.window.onhashchange.callCount).equals(0) + done() + }) + }) + }) + o("onhashchange does not trigger on history.forward() that causes page change with same hash", function(done) { + G.window.location.href = "#a" + G.window.location.href = "a#b" + callAsync(function(){ + G.window.onhashchange = o.spy() + G.window.history.back() + G.window.history.forward() + + callAsync(function(){ + o(G.window.onhashchange.callCount).equals(0) + done() + }) + }) + }) + }) + o.spec("onunload", function() { + o("onunload triggers on location.href change", function() { + G.window.onunload = o.spy() + G.window.location.href = "http://localhost/a" + + o(G.window.onunload.callCount).equals(1) + o(G.window.onunload.args[0].type).equals("unload") + }) + o("onunload triggers on relative location.href change", function() { + G.window.onunload = o.spy() + G.window.location.href = "a" + + o(G.window.onunload.callCount).equals(1) + }) + o("onunload triggers on search change via location.href", function() { + G.window.onunload = o.spy() + G.window.location.href = "http://localhost/?a" + + o(G.window.onunload.callCount).equals(1) + }) + o("onunload triggers on relative search change via location.href", function() { + G.window.onunload = o.spy() + G.window.location.href = "?a" + + o(G.window.onunload.callCount).equals(1) + }) + o("onunload does not trigger on hash change via location.href", function() { + G.window.onunload = o.spy() + G.window.location.href = "http://localhost/#a" + + o(G.window.onunload.callCount).equals(0) + }) + o("onunload does not trigger on relative hash change via location.href", function() { + G.window.onunload = o.spy() + G.window.location.href = "#a" + + o(G.window.onunload.callCount).equals(0) + }) + o("onunload does not trigger on hash-only history.back()", function() { + G.window.location.href = "#a" + G.window.onunload = o.spy() + G.window.history.back() + + o(G.window.onunload.callCount).equals(0) + }) + o("onunload does not trigger on hash-only history.forward()", function() { + G.window.location.href = "#a" + G.window.history.back() + G.window.onunload = o.spy() + G.window.history.forward() + + o(G.window.onunload.callCount).equals(0) + }) + o("onunload has correct url during call via location.href change", function(done) { + G.window.onunload = function() { + o(G.window.location.href).equals("http://localhost/") + done() + } + G.window.location.href = "a" + }) + o("onunload has correct url during call via location.search change", function(done) { + G.window.onunload = function() { + o(G.window.location.href).equals("http://localhost/") + done() + } + G.window.location.search = "?a" + }) + }) +}) diff --git a/test-utils/tests/test-throttleMock.js b/tests/test-utils/throttleMock.js similarity index 90% rename from test-utils/tests/test-throttleMock.js rename to tests/test-utils/throttleMock.js index b537b43af..b9bde1f33 100644 --- a/test-utils/tests/test-throttleMock.js +++ b/tests/test-utils/throttleMock.js @@ -1,7 +1,6 @@ -"use strict" +import o from "ospec" -var o = require("ospec") -var throttleMocker = require("../../test-utils/throttleMock") +import throttleMocker from "../../test-utils/throttleMock.js" o.spec("throttleMock", function() { o("schedules one callback", function() { diff --git a/util/censor.js b/util/censor.js deleted file mode 100644 index f21ce0d0f..000000000 --- a/util/censor.js +++ /dev/null @@ -1,48 +0,0 @@ -"use strict" - -// Note: this is mildly perf-sensitive. -// -// It does *not* use `delete` - dynamic `delete`s usually cause objects to bail -// out into dictionary mode and just generally cause a bunch of optimization -// issues within engines. -// -// Ideally, I would've preferred to do this, if it weren't for the optimization -// issues: -// -// ```js -// const hasOwn = require("./hasOwn") -// const magic = [ -// "key", "oninit", "oncreate", "onbeforeupdate", "onupdate", -// "onbeforeremove", "onremove", -// ] -// module.exports = (attrs, extras) => { -// const result = Object.assign(Object.create(null), attrs) -// for (const key of magic) delete result[key] -// if (extras != null) for (const key of extras) delete result[key] -// return result -// } -// ``` - -var hasOwn = require("./hasOwn") -// Words in RegExp literals are sometimes mangled incorrectly by the internal bundler, so use RegExp(). -var magic = new RegExp("^(?:key|oninit|oncreate|onbeforeupdate|onupdate|onbeforeremove|onremove)$") - -module.exports = function(attrs, extras) { - var result = {} - - if (extras != null) { - for (var key in attrs) { - if (hasOwn.call(attrs, key) && !magic.test(key) && extras.indexOf(key) < 0) { - result[key] = attrs[key] - } - } - } else { - for (var key in attrs) { - if (hasOwn.call(attrs, key) && !magic.test(key)) { - result[key] = attrs[key] - } - } - } - - return result -} diff --git a/util/hasOwn.js b/util/hasOwn.js deleted file mode 100644 index c7bd0576f..000000000 --- a/util/hasOwn.js +++ /dev/null @@ -1,4 +0,0 @@ -// This exists so I'm only saving it once. -"use strict" - -module.exports = {}.hasOwnProperty diff --git a/util/tests/test-censor.js b/util/tests/test-censor.js deleted file mode 100644 index 18500e202..000000000 --- a/util/tests/test-censor.js +++ /dev/null @@ -1,238 +0,0 @@ -"use strict" - -var o = require("ospec") -var censor = require("../../util/censor") - -o.spec("censor", function() { - o.spec("magic missing, no extras", function() { - o("returns new object", function() { - var original = {one: "two"} - var censored = censor(original) - o(censored).notEquals(original) - o(censored).deepEquals({one: "two"}) - }) - o("does not modify original object", function() { - var original = {one: "two"} - censor(original) - o(original).deepEquals({one: "two"}) - }) - }) - - o.spec("magic present, no extras", function() { - o("returns new object", function() { - var original = { - one: "two", - key: "test", - oninit: "test", - oncreate: "test", - onbeforeupdate: "test", - onupdate: "test", - onbeforeremove: "test", - onremove: "test", - } - var censored = censor(original) - o(censored).notEquals(original) - o(censored).deepEquals({one: "two"}) - }) - o("does not modify original object", function() { - var original = { - one: "two", - key: "test", - oninit: "test", - oncreate: "test", - onbeforeupdate: "test", - onupdate: "test", - onbeforeremove: "test", - onremove: "test", - } - censor(original) - o(original).deepEquals({ - one: "two", - key: "test", - oninit: "test", - oncreate: "test", - onbeforeupdate: "test", - onupdate: "test", - onbeforeremove: "test", - onremove: "test", - }) - }) - }) - - o.spec("magic missing, null extras", function() { - o("returns new object", function() { - var original = {one: "two"} - var censored = censor(original, null) - o(censored).notEquals(original) - o(censored).deepEquals({one: "two"}) - }) - o("does not modify original object", function() { - var original = {one: "two"} - censor(original, null) - o(original).deepEquals({one: "two"}) - }) - }) - - o.spec("magic present, null extras", function() { - o("returns new object", function() { - var original = { - one: "two", - key: "test", - oninit: "test", - oncreate: "test", - onbeforeupdate: "test", - onupdate: "test", - onbeforeremove: "test", - onremove: "test", - } - var censored = censor(original, null) - o(censored).notEquals(original) - o(censored).deepEquals({one: "two"}) - }) - o("does not modify original object", function() { - var original = { - one: "two", - key: "test", - oninit: "test", - oncreate: "test", - onbeforeupdate: "test", - onupdate: "test", - onbeforeremove: "test", - onremove: "test", - } - censor(original, null) - o(original).deepEquals({ - one: "two", - key: "test", - oninit: "test", - oncreate: "test", - onbeforeupdate: "test", - onupdate: "test", - onbeforeremove: "test", - onremove: "test", - }) - }) - }) - - o.spec("magic missing, extras missing", function() { - o("returns new object", function() { - var original = {one: "two"} - var censored = censor(original, ["extra"]) - o(censored).notEquals(original) - o(censored).deepEquals({one: "two"}) - }) - o("does not modify original object", function() { - var original = {one: "two"} - censor(original, ["extra"]) - o(original).deepEquals({one: "two"}) - }) - }) - - o.spec("magic present, extras missing", function() { - o("returns new object", function() { - var original = { - one: "two", - key: "test", - oninit: "test", - oncreate: "test", - onbeforeupdate: "test", - onupdate: "test", - onbeforeremove: "test", - onremove: "test", - } - var censored = censor(original, ["extra"]) - o(censored).notEquals(original) - o(censored).deepEquals({one: "two"}) - }) - o("does not modify original object", function() { - var original = { - one: "two", - key: "test", - oninit: "test", - oncreate: "test", - onbeforeupdate: "test", - onupdate: "test", - onbeforeremove: "test", - onremove: "test", - } - censor(original, ["extra"]) - o(original).deepEquals({ - one: "two", - key: "test", - oninit: "test", - oncreate: "test", - onbeforeupdate: "test", - onupdate: "test", - onbeforeremove: "test", - onremove: "test", - }) - }) - }) - - o.spec("magic missing, extras present", function() { - o("returns new object", function() { - var original = { - one: "two", - extra: "test", - } - var censored = censor(original, ["extra"]) - o(censored).notEquals(original) - o(censored).deepEquals({one: "two"}) - }) - o("does not modify original object", function() { - var original = { - one: "two", - extra: "test", - } - censor(original, ["extra"]) - o(original).deepEquals({ - one: "two", - extra: "test", - }) - }) - }) - - o.spec("magic present, extras present", function() { - o("returns new object", function() { - var original = { - one: "two", - extra: "test", - key: "test", - oninit: "test", - oncreate: "test", - onbeforeupdate: "test", - onupdate: "test", - onbeforeremove: "test", - onremove: "test", - } - var censored = censor(original, ["extra"]) - o(censored).notEquals(original) - o(censored).deepEquals({one: "two"}) - }) - o("does not modify original object", function() { - var original = { - one: "two", - extra: "test", - key: "test", - oninit: "test", - oncreate: "test", - onbeforeupdate: "test", - onupdate: "test", - onbeforeremove: "test", - onremove: "test", - } - censor(original, ["extra"]) - o(original).deepEquals({ - one: "two", - extra: "test", - key: "test", - oninit: "test", - oncreate: "test", - onbeforeupdate: "test", - onupdate: "test", - onbeforeremove: "test", - onremove: "test", - }) - }) - }) -})