Repository: MithrilJS/mithril.js Branch: main Commit: bd0ee3fde383 Files: 138 Total size: 869.2 KB Directory structure: gitextract_7dh5okin/ ├── .deploy.enc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github/ │ ├── CODEOWNERS │ ├── ISSUE_TEMPLATE/ │ │ ├── 1-core.yml │ │ ├── 2-stream.yml │ │ └── config.yml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── dependabot.yml │ └── workflows/ │ ├── issue-create.yml │ ├── notify-release.yml │ ├── pr-create-release.yml │ ├── publish-prerelease.yml │ ├── push-release.yml │ ├── rollback.yml │ └── test.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── api/ │ ├── mount-redraw.js │ ├── router.js │ └── tests/ │ ├── test-mountRedraw.js │ ├── test-router.js │ └── test-routerGetSet.js ├── browser.js ├── docs/ │ ├── code-of-conduct.md │ ├── contributing.md │ ├── credits.md │ ├── recent-changes.md │ └── releasing.md ├── hyperscript.js ├── index.js ├── mithril.js ├── mount-redraw.js ├── mount.js ├── package.json ├── pathname/ │ ├── build.js │ ├── compileTemplate.js │ ├── parse.js │ └── tests/ │ ├── test-buildPathname.js │ ├── test-compileTemplate.js │ └── test-parsePathname.js ├── performance/ │ ├── index.html │ └── test-perf.js ├── querystring/ │ ├── build.js │ ├── parse.js │ └── tests/ │ ├── test-buildQueryString.js │ └── test-parseQueryString.js ├── redraw.js ├── render/ │ ├── cachedAttrsIsStaticMap.js │ ├── delayedRemoval.js │ ├── domFor.js │ ├── emptyAttrs.js │ ├── fragment.js │ ├── hyperscript.js │ ├── hyperscriptVnode.js │ ├── render.js │ ├── tests/ │ │ ├── .eslintrc.js │ │ ├── manual/ │ │ │ ├── case-handling.html │ │ │ ├── iframe.html │ │ │ ├── index.html │ │ │ ├── minlength-input.html │ │ │ └── minlength-textarea.html │ │ ├── test-attributes.js │ │ ├── test-component.js │ │ ├── test-createElement.js │ │ ├── test-createFragment.js │ │ ├── test-createHTML.js │ │ ├── test-createNodes.js │ │ ├── test-createText.js │ │ ├── test-domFor.js │ │ ├── test-event.js │ │ ├── test-fragment.js │ │ ├── test-hyperscript.js │ │ ├── test-input.js │ │ ├── test-normalize.js │ │ ├── test-normalizeChildren.js │ │ ├── test-normalizeComponentChildren.js │ │ ├── test-onbeforeremove.js │ │ ├── test-onbeforeupdate.js │ │ ├── test-oncreate.js │ │ ├── test-oninit.js │ │ ├── test-onremove.js │ │ ├── test-onupdate.js │ │ ├── test-render-hyperscript-integration.js │ │ ├── test-render.js │ │ ├── test-textContent.js │ │ ├── test-trust.js │ │ ├── test-updateElement.js │ │ ├── test-updateFragment.js │ │ ├── test-updateHTML.js │ │ ├── test-updateNodes.js │ │ ├── test-updateNodesFuzzer.js │ │ └── test-updateText.js │ ├── trust.js │ └── vnode.js ├── render.js ├── request/ │ ├── request.js │ └── tests/ │ └── test-request.js ├── request.js ├── route.js ├── scripts/ │ ├── .eslintrc.js │ ├── _bundler-impl.js │ ├── bundler-readme.md │ ├── bundler.js │ ├── minify-stream.js │ ├── set-versioned-branch.sh │ └── tests/ │ └── test-bundler.js ├── stream/ │ ├── stream.js │ └── tests/ │ ├── test-scan.js │ ├── test-scanMerge.js │ └── test-stream.js ├── stream.js ├── test-utils/ │ ├── browserMock.js │ ├── callAsync.js │ ├── components.js │ ├── domMock.js │ ├── parseURL.js │ ├── pushStateMock.js │ ├── tests/ │ │ ├── test-browserMock.js │ │ ├── test-callAsync.js │ │ ├── test-components.js │ │ ├── test-domMock.js │ │ ├── test-parseURL.js │ │ ├── test-pushStateMock.js │ │ ├── test-throttleMock.js │ │ └── test-xhrMock.js │ ├── throttleMock.js │ └── xhrMock.js ├── tests/ │ └── test-api.js └── util/ ├── censor.js ├── decodeURIComponentSafe.js ├── hasOwn.js └── tests/ ├── test-censor.js └── test-decodeURIComponentSafe.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] charset = utf-8 [*.{js,json,yml,html,md}] indent_style = tab tab_width = 4 trim_trailing_whitespace = true insert_final_newline = true end_of_line = lf [{package.json,.travis.yml,.github/**/*.yml}] indent_style = space indent_size = 2 ================================================ FILE: .eslintignore ================================================ /coverage /node_modules /jsconfig.json /npm-debug.log /.vscode /.DS_Store /.eslintcache # These are artifacts from various scripts /mithril.js /mithril.min.js /stream/stream.min.js ================================================ FILE: .eslintrc.js ================================================ "use strict" module.exports = { "env": { "browser": true, "commonjs": true, "es6": true, "node": true }, "extends": "eslint:recommended", "rules": { "accessor-pairs": "error", "array-bracket-spacing": [ "error", "never" ], "array-callback-return": "error", "arrow-body-style": "error", "arrow-parens": "error", "arrow-spacing": "error", "block-scoped-var": "off", "block-spacing": "off", "brace-style": "off", "callback-return": "off", "camelcase": [ "error", { "properties": "never" } ], "comma-dangle": [ "error", "only-multiline" ], "comma-spacing": "off", "comma-style": [ "error", "last" ], "complexity": "off", "computed-property-spacing": [ "error", "never" ], "consistent-return": "off", "consistent-this": "off", "curly": "off", "default-case": "off", "dot-location": [ "error", "property" ], "dot-notation": "off", "eol-last": "off", "eqeqeq": "off", "func-names": "off", "func-style": "off", "generator-star-spacing": "error", "global-require": "error", "guard-for-in": "off", "handle-callback-err": "error", "id-blacklist": "error", "id-length": "off", "id-match": "error", "indent": [ "warn", "tab", { "outerIIFEBody": 0, "SwitchCase": 1 } ], "init-declarations": "off", "jsx-quotes": "error", "key-spacing": "off", "keyword-spacing": "off", "linebreak-style": "off", "lines-around-comment": "off", "max-depth": "off", "max-len": "off", "max-nested-callbacks": "error", "max-params": "off", "max-statements": "off", "max-statements-per-line": "off", "new-parens": "off", "newline-after-var": "off", "newline-before-return": "off", "newline-per-chained-call": "off", "no-alert": "error", "no-array-constructor": "error", "no-bitwise": "error", "no-caller": "error", "no-catch-shadow": "off", "no-cond-assign": "off", "no-confusing-arrow": "error", "no-console": "off", "no-continue": "off", "no-div-regex": "error", "no-duplicate-imports": "error", "no-else-return": "off", "no-empty-function": "off", "no-eq-null": "off", "no-eval": "error", "no-extend-native": "off", "no-extra-bind": "error", "no-extra-label": "error", "no-extra-parens": "off", "no-floating-decimal": "error", "no-implicit-coercion": "error", "no-implicit-globals": "error", "no-implied-eval": "error", "no-inline-comments": "off", "no-invalid-this": "off", "no-iterator": "error", "no-label-var": "off", "no-labels": "off", "no-lone-blocks": "error", "no-lonely-if": "off", "no-loop-func": "off", "no-magic-numbers": "off", "no-mixed-requires": "error", "no-multi-spaces": "error", "no-multi-str": "error", "no-multiple-empty-lines": "error", "no-native-reassign": "error", "no-negated-condition": "off", "no-nested-ternary": "off", "no-new": "off", "no-new-func": "off", "no-new-object": "error", "no-new-require": "error", "no-new-wrappers": "error", "no-octal-escape": "error", "no-param-reassign": "off", "no-path-concat": "off", "no-plusplus": "off", "no-process-env": "error", "no-process-exit": "error", "no-proto": "error", "no-redeclare": "off", "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", "no-sequences": "off", "no-shadow": "off", "no-shadow-restricted-names": "error", "no-spaced-func": "error", "no-sync": "off", "no-ternary": "off", "no-throw-literal": "off", "no-trailing-spaces": [ "error", { "skipBlankLines": true } ], "no-undef-init": "error", "no-undefined": "off", "no-underscore-dangle": "off", "no-unmodified-loop-condition": "error", "no-unneeded-ternary": "error", "no-unused-expressions": "off", "no-use-before-define": "off", "no-useless-call": "error", "no-useless-concat": "error", "no-useless-constructor": "error", "no-useless-escape": "off", "no-var": "off", "no-void": "off", "no-warning-comments": "off", "no-whitespace-before-property": "error", "no-with": "error", "object-curly-spacing": [ "error", "never" ], "object-shorthand": "off", "one-var": "off", "one-var-declaration-per-line": "off", "operator-assignment": [ "error", "always" ], "operator-linebreak": "off", "padded-blocks": "off", "prefer-arrow-callback": "off", "prefer-const": "error", "prefer-reflect": "off", "prefer-rest-params": "off", "prefer-spread": "off", "prefer-template": "off", "quote-props": "off", "quotes": [ "error", "double", {"avoidEscape": true} ], "radix": [ "error", "always" ], "require-jsdoc": "off", "require-yield": "error", "semi": "off", "semi-spacing": "off", "sort-imports": "error", "sort-vars": "off", "space-before-blocks": "off", "space-before-function-paren": "off", "space-in-parens": [ "error", "never" ], "space-infix-ops": "off", "space-unary-ops": "error", "spaced-comment": "off", "strict": ["error", "global"], "template-curly-spacing": "error", "valid-jsdoc": "off", "vars-on-top": "off", "wrap-iife": "off", "wrap-regex": "error", "yield-star-spacing": "error", "yoda": "off" }, "root": true }; ================================================ FILE: .gitattributes ================================================ * text=auto /mithril.js binary /mithril.min.js binary # Assets *.png binary *.svg binary *.ico binary ================================================ FILE: .github/CODEOWNERS ================================================ * @MithrilJS/Committers /.github/ @MithrilJS/Admins ================================================ FILE: .github/ISSUE_TEMPLATE/1-core.yml ================================================ name: '🐛 Framework Bug' description: Report a bug in Mithril.js core assignees: dead-claudia labels: - 'Area: Core' body: - type: checkboxes attributes: label: Is there an existing issue for this? description: Please search to see if an issue already exists for the bug you encountered. options: - label: I have searched the existing issues required: true - type: input attributes: label: Mithril.js Version description: | Provide the exact version of Mithril.js you're experiencing these issues with. This matters, even if it's really old like version 0.1.0. Do note that bugs in older versions are commonly fixed in newer versions, so you should try to test it against the latest version if you can. validations: required: true - type: textarea attributes: label: Browser and OS description: | Provide the name and version of both the browser and operating system you're experiencing these issues with. If it's multiple, feel free to list multiple. This matters, even if it's super ancient. validations: required: true - type: textarea attributes: label: Project description: | (Optional) Provide a link to your project, if it happens to be open source or if you created a repo somewhere that we can look into further. If it's spread across multiple repos or projects, feel free to list them all. - type: textarea attributes: label: Code description: | What did you try? What code is causing the unexpected behavior? Make sure to try to reduce your code as best as you can while still reproducing the bug, so we can more accurately determine the cause. Ideally, it should just be a bunch of Mithril.js calls with virtually no logic at all, but it's sufficient to just remove unrelated network calls, attributes, and the like. In addition, make sure the bug still persists with the latest version of Mithril. If it's an older version, the bug may have already been fixed. If you'd prefer, replace this code block with a link to a code playground like any of these: - Flems (stores everything in URL hash) - JSFiddle - CodePen - JSBin - Plunker - Glitch (supports backend) - CodeSandbox (supports backend) Or if it's a remote development project on your own server, feel free to provide that if it's serving unminified code we can look at. If it's a closed-source repo, it's okay to censor names and pull out irrelevant logic - we'd rather not sign NDAs just to see the code you're having trouble with. We do still need code of some kind that triggers the bug you're running into. render: javascript validations: required: true - type: textarea attributes: label: Steps to Reproduce description: | What steps need to be taken to reproduce this behavior? Please include things like specific data that need typed in, specific buttons that need clicked, and so on. placeholder: | 1. 2. 3. 4. validations: required: true - type: textarea attributes: label: Expected Behavior description: | What did you expect to happen? - An alert to pop up? - A specific thing to be logged? Please be very specific here. validations: required: true - type: textarea attributes: label: Observed Behavior description: | What actually happened? - The alert never showed? - The wrong thing was logged? Please be very specific here. validations: required: true - type: textarea attributes: label: Context description: | (Optional) How is this issue affecting you? What are you trying to do? Providing us context helps us reach a solution that best fits your particular needs. ================================================ FILE: .github/ISSUE_TEMPLATE/2-stream.yml ================================================ name: '🌊 Mithril.js Streams bug' description: Report an issue with Mithril.js's Streams module assignees: dead-claudia labels: - 'Area: Stream' body: - type: checkboxes attributes: label: Is there an existing issue for this? description: Please search to see if an issue already exists for the bug you encountered. options: - label: I have searched the existing issues required: true - type: input attributes: label: Mithril.js Version description: | Provide the exact version of Mithril.js you're experiencing these issues with. This matters, even if it's really old like version 0.1.0. Do note that bugs in older versions are commonly fixed in newer versions, so you should try to test it against the latest version if you can. validations: required: true - type: textarea attributes: label: Browser and OS description: | Provide the name and version of both the browser and operating system you're experiencing these issues with. If it's multiple, feel free to list multiple. This matters, even if it's super ancient. validations: required: true - type: textarea attributes: label: Project description: | (Optional) Provide a link to your project, if it happens to be open source or if you created a repo somewhere that we can look into further. If it's spread across multiple repos or projects, feel free to list them all. - type: textarea attributes: label: Code description: | What did you try? What code is causing the unexpected behavior? Make sure to try to reduce your code as best as you can while still reproducing the bug, so we can more accurately determine the cause. Ideally, it should just be a bunch of Mithril.js calls with virtually no logic at all, but it's sufficient to just remove unrelated network calls, attributes, and the like. In addition, make sure the bug still persists with the latest version of Mithril. If it's an older version, the bug may have already been fixed. If you'd prefer, replace this code block with a link to a code playground like any of these: - Flems (stores everything in URL hash) - JSFiddle - CodePen - JSBin - Plunker - Glitch (supports backend) - CodeSandbox (supports backend) Or if it's a remote development project on your own server, feel free to provide that if it's serving unminified code we can look at. If it's a closed-source repo, it's okay to censor names and pull out irrelevant logic - we'd rather not sign NDAs just to see the code you're having trouble with. We do still need code of some kind that triggers the bug you're running into. render: javascript validations: required: true - type: textarea attributes: label: Steps to Reproduce description: | What steps need to be taken to reproduce this behavior? Please include things like specific data that need typed in, specific buttons that need clicked, and so on. placeholder: | 1. 2. 3. 4. validations: required: true - type: textarea attributes: label: Expected Behavior description: | What did you expect to happen? - An alert to pop up? - A specific thing to be logged? Please be very specific here. validations: required: true - type: textarea attributes: label: Observed Behavior description: | What actually happened? - The alert never showed? - The wrong thing was logged? Please be very specific here. validations: required: true - type: textarea attributes: label: Context description: | (Optional) How is this issue affecting you? What are you trying to do? Providing us context helps us reach a solution that best fits your particular needs. ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: true contact_links: - name: ℹ Questions, Ideas, and Discussions url: https://github.com/MithrilJS/mithril.js/discussions about: | Got a question on how to use Mithril.js? Have a fancy idea of how we could do better? Check out our discussions forum! - name: ℹ Show and Tell url: https://github.com/MithrilJS/mithril.js/discussions/new?category=show-and-tell about: | Got something to show off? Made something so cool, you just have to tell the world? Let us know here in our discussions forum! - name: 🚀 Feature Request or Enhancement url: https://github.com/MithrilJS/mithril.js/discussions/new?category=ideas about: Got a feature request? Let us know here! We do those in our discussion forum. - name: 📄 Documentation issue url: https://github.com/MithrilJS/docs about: Found an issue with our documentation? File that in our docs repo instead. - name: 💻 Zulip Chat url: https://mithril.zulipchat.com/ about: Not sure about something? Just want to hang out? Come over to our Zulip chat! ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ## Description ## Motivation and Context ## How Has This Been Tested? ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) ## Checklist - [ ] My code follows the code style of this project. - [ ] I have added tests to cover my changes. - [ ] All new and existing tests passed. - [ ] My change requires a documentation update, and I've opened a pull request to update it already: - [ ] I have read https://mithril.js.org/contributing.html. ================================================ FILE: .github/dependabot.yml ================================================ # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 updates: - package-ecosystem: "npm" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: daily groups: security: applies-to: security-updates patterns: ['*'] normal: applies-to: version-updates patterns: ['*'] - package-ecosystem: github-actions directory: / schedule: interval: daily groups: security: applies-to: security-updates patterns: ['*'] normal: applies-to: version-updates patterns: ['*'] ================================================ FILE: .github/workflows/issue-create.yml ================================================ name: Ping triage on issue create on: issues: types: [opened] pull_request_target: types: [opened] jobs: notify_triage: uses: MithrilJS/infra/.github/workflows/notify-triage.yml@main secrets: inherit ================================================ FILE: .github/workflows/notify-release.yml ================================================ name: Notify release on: push: tags: ['v*'] jobs: update: runs-on: ubuntu-latest steps: - name: Send workflow dispatch to docs run: gh --repo MithrilJS/docs workflow run package-update.yml -f package=mithril env: GH_TOKEN: ${{ secrets.DOCS_UPDATE_TOKEN }} ================================================ FILE: .github/workflows/pr-create-release.yml ================================================ name: Warn on opening a PR to `release` on: pull_request_target: types: [opened] branches: [release] permissions: issues: write jobs: comment: # Don't auto-close actual release PRs if: ${{github.actor != 'JAForbes' || !contains(github.event.issue.title, 'Release - v')}} uses: MithrilJS/infra/.github/workflows/reject-pr.yml@main secrets: inherit ================================================ FILE: .github/workflows/publish-prerelease.yml ================================================ name: Publish prerelease and update PR on: workflow_call: workflow_dispatch: jobs: update-pr: concurrency: prr:pre-release runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v6 with: node-version: 20 - run: npm ci - run: npm run build - run: npx pr-release pr --verbose --target release --source main --compact --verbose --minimize-semver-change env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} # The following will publish a prerelease to npm - run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc name: Setup NPM Auth env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - run: npx pr-release infer-prerelease --preid=next --target release --source main --verbose --publish --minimize-semver-change name: Publish ================================================ FILE: .github/workflows/push-release.yml ================================================ name: Create release when pushing to `release` on: push: branches: [release] workflow_dispatch: concurrency: merge-release jobs: merge: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 with: ref: main - uses: actions/setup-node@v6 with: node-version: 20 - run: npm ci - run: npm run build - run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc name: Setup NPM Auth env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - run: npx pr-release merge --target release --source main --commit --force --clean --changelog ./docs/recent-changes.md --compact --minimize-semver-change --prerelease="npm publish" env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - run: bash scripts/set-versioned-branch.sh release ================================================ FILE: .github/workflows/rollback.yml ================================================ name: rollback on: workflow_dispatch: concurrency: prr:deploy jobs: pr: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v6 with: node-version: 20 - run: npm ci - run: npm run build - run: npx pr-release rollback --verbose --target release --source main --verbose --ignore 'package*' --ignore docs/changelog.md --ignore docs/recent-changes.md env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - run: bash scripts/set-versioned-branch.sh release ================================================ FILE: .github/workflows/test.yml ================================================ name: Test and maybe release on: pull_request_target: branches: [ main ] push: branches: [ main ] workflow_dispatch: jobs: run-tests: uses: MithrilJS/infra/.github/workflows/run-tests.yml@main with: test-node: true all-versions: true permissions: actions: write contents: read publish-prerelease: needs: run-tests if: ${{ github.event_name == 'push' }} concurrency: prr:pre-release runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v6 with: node-version: 20 - run: npm ci - run: npm run build - run: npx pr-release pr --verbose --target release --source main --compact --verbose --minimize-semver-change env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} # The following will publish a prerelease to npm - run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc name: Setup NPM Auth env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - run: npx pr-release infer-prerelease --preid=next --target release --source main --verbose --publish --minimize-semver-change name: Publish ================================================ FILE: .gitignore ================================================ /coverage /node_modules /jsconfig.json /npm-debug.log /.vscode /.DS_Store /.eslintcache ================================================ FILE: .npmignore ================================================ # 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/ ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2017 Leo Horie 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. ================================================ FILE: README.md ================================================ # Mithril.js [![npm Version](https://img.shields.io/npm/v/mithril.svg)](https://www.npmjs.com/package/mithril)   [![License](https://img.shields.io/npm/l/mithril.svg)](https://github.com/MithrilJS/mithril.js/blob/main/LICENSE)   [![npm Downloads](https://img.shields.io/npm/dm/mithril.svg)](https://www.npmjs.com/package/mithril)   [![Build Status](https://img.shields.io/github/actions/workflow/status/MithrilJS/mithril.js/.github%2Fworkflows%2Ftest.yml?branch=main&event=push)](https://github.com/MithrilJS/mithril.js/actions)   [![Donate at OpenCollective](https://img.shields.io/opencollective/all/mithriljs.svg?colorB=brightgreen)](https://opencollective.com/mithriljs)   [![Zulip, join chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://mithril.zulipchat.com/) - [What is Mithril.js?](#what-is-mithriljs) - [Installation](#installation) - [Documentation](#documentation) - [Getting Help](#getting-help) - [Contributing](#contributing) ## What is Mithril.js? A modern client-side JavaScript framework for building Single Page Applications. It's small (8.93 KB gzipped), fast and provides routing and XHR utilities out of the box. Mithril.js is used by companies like Vimeo and Nike, and open source platforms like Lichess 👍. Mithril.js supports Firefox ESR, and the last two versions of Firefox, Edge, Safari, and Chrome. No polyfills required. 👌 ## Installation ### CDN ```html ``` ### npm ```bash npm install mithril --save ``` The ["Getting started" guide](https://mithril.js.org/#getting-started) is a good place to start learning how to use Mithril.js. TypeScript type definitions are available from DefinitelyTyped. They can be installed with: ```bash $ npm install @types/mithril --save-dev ``` ## Documentation Documentation lives on [mithril.js.org](https://mithril.js.org). You may be interested in the [API Docs](https://mithril.js.org/api.html), a [Simple Application](https://mithril.js.org/simple-application.html), or perhaps some [Examples](https://mithril.js.org/examples.html). ## Getting Help Mithril.js has an active & welcoming community on [Zulip](https://mithril.zulipchat.com/), or feel free to ask questions on [Stack Overflow](https://stackoverflow.com/questions/tagged/mithril.js) using the `mithril.js` tag. ## Contributing There's a [Contributing FAQ](https://mithril.js.org/contributing.html) on the Mithril.js site that hopefully helps, but if not definitely hop into the [Zulip stream](https://mithril.zulipchat.com/) and ask away! --- Thanks for reading! 🎁 ================================================ FILE: api/mount-redraw.js ================================================ "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} } ================================================ FILE: api/router.js ================================================ "use strict" var Vnode = require("../render/vnode") var hyperscript = require("../render/hyperscript") var decodeURIComponentSafe = require("../util/decodeURIComponentSafe") var buildPathname = require("../pathname/build") var parsePathname = require("../pathname/parse") var compileTemplate = require("../pathname/compileTemplate") var censor = require("../util/censor") module.exports = function($window, mountRedraw) { var p = Promise.resolve() var scheduled = false var ready = false var hasBeenResolved = false var dom, compiled, fallbackRoute var currentResolver, component, attrs, currentPath, lastUpdate var RouterRoot = { onremove: function() { ready = hasBeenResolved = false $window.removeEventListener("popstate", fireAsync, false) }, view: function() { // The route has already been resolved. // Therefore, the following early return is not needed. // if (!hasBeenResolved) return var vnode = Vnode(component, attrs.key, attrs) if (currentResolver) return currentResolver.render(vnode) // Wrap in a fragment to preserve existing key semantics 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, decodeURIComponentSafe) .slice(route.prefix.length) var data = parsePathname(path) Object.assign(data.params, $window.history.state) function reject(e) { console.error(e) route.set(fallbackRoute, null, {replace: true}) } loop(0) function loop(i) { 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 (hasBeenResolved) mountRedraw.redraw() else { hasBeenResolved = true mountRedraw.mount(dom, RouterRoot) } } // 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 + ".") } route.set(fallbackRoute, null, {replace: true}) } } 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. setTimeout(resolveRoute) } } 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.") } } dom = root $window.addEventListener("popstate", fireAsync, false) ready = true // The RouterRoot component is mounted when the route is first resolved. resolveRoute() } route.set = function(path, data, options) { if (lastUpdate != null) { options = options || {} options.replace = true } lastUpdate = null 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 } } 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 = hyperscript( 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 } ================================================ FILE: api/tests/test-mountRedraw.js ================================================ "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() }) }) }) }) ================================================ FILE: api/tests/test-router.js ================================================ "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("remove trailing slash to match route if it is before rest operator match (...) ", function() { $window.location.href = prefix + "/test/d/" route(root, "/test/some/path", { "/test/:a" : { view: lock(function(vnode) { return JSON.stringify(route.param()) + " " + JSON.stringify(vnode.attrs) + " " + route.get() }) }, "/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"}) }) }) o("route component is mounted after the route is initially resolved (synchronous)", function() { var Component = { view: lock(function() { // the first rendered vnode is cleared o(root.childNodes.length).equals(0) return m("span") }) } // initial root node root.textContent = "foo" o(root.childNodes.length).equals(1) o(root.childNodes[0].nodeName).equals("#text") o(root.childNodes[0].nodeValue).equals("foo") // render another vnode first var render = coreRenderer($window) var vnode = m("a", "loading...") render(root, vnode) o(root.childNodes.length).equals(1) o(root.childNodes[0].nodeName).equals("A") o(root.childNodes[0].firstChild.nodeName).equals("#text") o(root.childNodes[0].firstChild.nodeValue).equals("loading...") // call route() (mount synchronously) $window.location.href = prefix + "/" route(root, "/", { "/" : Component }) // route component is mounted and the first rendered vnode is cleared o(root.childNodes.length).equals(1) o(root.childNodes[0]).notEquals(vnode.dom) o(root.childNodes[0].nodeName).equals("SPAN") o(root.childNodes[0].childNodes.length).equals(0) }) o("route component is mounted after the route is initially resolved (render, synchronous)", function() { var Component = { render: lock(function() { // the first rendered vnode is cleared o(root.childNodes.length).equals(0) return m("span") }) } // initial root node root.textContent = "foo" o(root.childNodes.length).equals(1) o(root.childNodes[0].nodeName).equals("#text") o(root.childNodes[0].nodeValue).equals("foo") // render another vnode first var render = coreRenderer($window) var vnode = m("a", "loading...") render(root, vnode) o(root.childNodes.length).equals(1) o(root.childNodes[0].nodeName).equals("A") o(root.childNodes[0].firstChild.nodeName).equals("#text") o(root.childNodes[0].firstChild.nodeValue).equals("loading...") // call route() (mount synchronously) $window.location.href = prefix + "/" route(root, "/", { "/" : Component }) // route component is mounted and the first rendered vnode is cleared o(root.childNodes.length).equals(1) o(root.childNodes[0]).notEquals(vnode.dom) o(root.childNodes[0].nodeName).equals("SPAN") o(root.childNodes[0].childNodes.length).equals(0) }) o("route component is mounted after the route is initially resolved (onmatch, asynchronous)", function() { var Component = { view: lock(function() { return m("span") }) } // check for the order of calling onmatch and render var count = 0 var resolver = { onmatch: lock(function() { count += 1 o(count).equals(1) // the first rendered vnode is not yet cleared o(root.childNodes.length).equals(1) o(root.childNodes[0]).equals(vnode.dom) o(root.childNodes[0].nodeName).equals("A") o(root.childNodes[0].firstChild.nodeName).equals("#text") o(root.childNodes[0].firstChild.nodeValue).equals("loading...") return Component }), render: lock(function(vnode) { count += 1 o(count).equals(2) // the first rendered vnode is cleared o(root.childNodes.length).equals(0) return vnode }) } // initial root node root.textContent = "foo" o(root.childNodes.length).equals(1) o(root.childNodes[0].nodeName).equals("#text") o(root.childNodes[0].nodeValue).equals("foo") // render another vnode first var render = coreRenderer($window) var vnode = m("a", "loading...") render(root, vnode) o(root.childNodes.length).equals(1) o(root.childNodes[0].nodeName).equals("A") o(root.childNodes[0].firstChild.nodeName).equals("#text") o(root.childNodes[0].firstChild.nodeValue).equals("loading...") // call route() (mount asynchronously) $window.location.href = prefix + "/" route(root, "/", { "/" : resolver }) // the first rendered vnode is not yet cleared o(root.childNodes.length).equals(1) o(root.childNodes[0]).equals(vnode.dom) o(root.childNodes[0].nodeName).equals("A") o(root.childNodes[0].firstChild.nodeName).equals("#text") o(root.childNodes[0].firstChild.nodeValue).equals("loading...") // The count of route resolver method calls is still 0 o(count).equals(0) return waitCycles(1).then(function() { // route component is mounted and the first rendered vnode is cleared o(root.childNodes.length).equals(1) o(root.childNodes[0]).notEquals(vnode.dom) o(root.childNodes[0].nodeName).equals("SPAN") o(root.childNodes[0].childNodes.length).equals(0) o(count).equals(2) }) }) o("error in the route component is thrown and not caught in the initial rendering (#2621)", function() { var Component = { view: lock(function() { throw Error("foo") }) } // Errors thrown during redrawing of mounted components are caught in m.mount() // and console.error is called. // Therefore, spy is used to confirm that console.error is not called // when it is first mounted. var spy = o.spy(console.error) console.error = spy $window.location.href = prefix + "/" o(function(){ route(root, "/", { "/" : Component }) }).throws("foo") o(spy.callCount).equals(0) }) }) }) }) }) ================================================ FILE: api/tests/test-routerGetSet.js ================================================ "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() }) }) }) }) }) }) ================================================ FILE: browser.js ================================================ "use strict" var m = require("./index") if (typeof module !== "undefined") module["exports"] = m else window.m = m ================================================ FILE: docs/code-of-conduct.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [contact@claudiameadows.dev](mailto:contact@claudiameadows.dev?subject=Mithril%20Code%20of%20Conduct). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. As a general policy, we generally do not disclose any particular action taken in accordance with this Code of Conduct beyond those who need to know. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version] [homepage]: https://contributor-covenant.org [version]: https://contributor-covenant.org/version/1/4/ ================================================ FILE: docs/contributing.md ================================================ # Contributing FAQs - [How do I go about contributing ideas or new features?](#how-do-i-go-about-contributing-ideas-or-new-features?) - [How should I report bugs?](#how-should-i-report-bugs?) - [How do I send a pull request?](#how-do-i-send-a-pull-request?) - [I'm submitting a PR. How do I run tests?](#i'm-submitting-a-pr-how-do-i-run-tests?) - [How do I build Mithril.js?](#how-do-i-build-mithril?) - [Is there a style guide?](#is-there-a-style-guide?) - [How do I embed live previews in docs?](#how-do-I-embed-live-previews-in-docs?) - [Why do tests mock the browser APIs?](#why-do-tests-mock-the-browser-apis?) - [Why does Mithril.js use its own testing framework and not Mocha/Jasmine/Tape?](#why-does-mithril-use-its-own-testing-framework-and-not-mochajasminetape?) - [Why doesn't the Mithril.js codebase use ES6 via Babel or Bublé? Would a PR to upgrade be welcome?](#why-doesn't-the-mithril-codebase-use-es6-via-babel-or-bublé?-would-a-pr-to-upgrade-be-welcome?) - [Why doesn't the Mithril.js codebase use trailing semi-colons? Would a PR to add them be welcome?](#why-doesn't-the-mithril-codebase-use-trailing-semi-colons?-would-a-pr-to-add-them-be-welcome?) - [Why does the Mithril.js codebase use a mix of `instanceof` and `typeof` checks instead of `Object.prototype.toString.call`, `Array.isArray`, etc? Would a PR to refactor those checks be welcome?](#why-does-the-mithril-codebase-use-a-mix-of-instanceof-and-typeof-checks-instead-of-objectprototypetostringcall,-arrayisarray,-etc?-would-a-pr-to-refactor-those-checks-be-welcome?) - [What should I know in advance when attempting a performance related contribution?](#What-should-I-know-in-advance-when-attempting-a-performance-related-contribution?) - [Do you all accept donations?](#do-you-all-accept-donations?) ## How do I go about contributing ideas or new features? Create an [issue thread on GitHub](https://github.com/MithrilJS/mithril.js/issues/new) to suggest your idea so the community can discuss it. If the consensus is that it's a good idea, the fastest way to get it into a release is to send a pull request. Without a PR, the time to implement the feature will depend on the bandwidth of the development team and its list of priorities. ## How should I report bugs? Ideally, the best way to report bugs is to provide a small snippet of code where the issue can be reproduced (via jsfiddle, jsbin, a gist, etc). Even better would be to submit a pull request with a fix and tests. If you don't know how to test your fix, or lint or whatever, submit anyways, and we can help you. ## How do I send a pull request? To send a pull request: - fork the repo (button at the top right in GitHub) - clone the forked repo to your computer (green button in GitHub) - Switch to the `main` branch (run `git checkout main`) - create a feature branch (run `git checkout -b the-feature-branch-name`) - make your changes - run the tests (run `npm test`) - push your changes to your fork - submit a pull request (go to the pull requests tab in GitHub, click the green button and select your feature branch) ## I'm submitting a PR. How do I run tests? After having run `npm install` (a one-time operation), run `npm run test` from the command line to run all tests. While testing, you can modify a test to use `o.only(description, test)` instead of `o(description, test)` if you wish to run only a specific test to speed up your debugging experience. Don't forget to remove the `.only` after you're done! ## How do I build Mithril.js? If all you're trying to do is run examples in the codebase, you don't need to build Mithril.js, you can just open the various html files and things should just work. To generate the bundled file for testing, run `npm run dev` from the command line. To generate the minified file, run `npm run build`. ## Is there a style guide? Yes, there's an `eslint` configuration, but it's not strict about formatting at all. If your contribution passes `npm run lint`, it's good enough for a PR (and it can still be accepted even if it doesn't pass). Spacing and formatting inconsistencies may be fixed after the fact, and we don't want that kind of stuff getting in the way of contributing. ## How do I embed live previews in docs? Any code tag marked as `js` and not `javascript` will automatically be wrapped in a live Flems preview. ## Why do tests mock the browser APIs? Most notoriously, because it's impossible to test the router and some side effects properly otherwise. Also, mocks allow the tests to run under Node.js without requiring heavy dependencies like PhantomJS/ChromeDriver/JSDOM. Another important reason is that it allows us to document browser API quirks via code, through the tests for the mocks. ## Why does Mithril.js use its own testing framework and not Mocha/Jasmine/Tape? Mainly to avoid requiring dependencies. `ospec` is customized to provide only essential information for common testing workflows (namely, no spamming ok's on pass, and accurate noiseless errors on failure) ## Why doesn't the Mithril.js codebase use ES6 via Babel or Bublé? Would a PR to upgrade be welcome? Being able to run Mithril.js' raw source code in all supported browsers is a requirement for all browser-related modules in this repo. In addition, transpiled code is generally much bulkier. ## Why doesn't the Mithril.js codebase use trailing semi-colons? Would a PR to add them be welcome? I don't use them. Adding them means the semi-colon usage in the codebase will eventually become inconsistent. Besides, [we aren't the only one who've decided to drop the semicolon](https://standardjs.com/#who-uses-javascript-standard-style). (We don't use Standard, though.) ## Why does the Mithril.js codebase use a mix of `instanceof` and `typeof` checks instead of `Object.prototype.toString.call`, `Array.isArray`, etc? Would a PR to refactor those checks be welcome? Mithril.js avoids peeking at objects' [[class]] string for performance considerations. Many type checks are seemingly inconsistent, weird or convoluted because those specific constructs demonstrated the best performance profile in benchmarks compared to alternatives. Type checks are generally already irreducible expressions and having micro-modules for type checking subroutines would add maintenance overhead. ## What should I know in advance when attempting a performance related contribution? You should be trying to reduce the number of DOM operations or reduce algorithmic complexity in a hot spot. Anything else is likely a waste of time. Specifically, micro-optimizations like caching array lengths, caching object property values and inlining functions won't have any positive impact in modern JavaScript engines. Keep object properties consistent (i.e. ensure the data objects always have the same properties and that properties are always in the same order) to allow the engine to keep using JIT'ed structs instead of hashmaps. Always place null checks first in compound type checking expressions to allow the JavaScript engine to optimize to type-specific code paths. Prefer for loops over Array methods and try to pull conditionals out of loops if possible. ## Do you all accept donations? Yes, we do, over at [our OpenCollective page](https://opencollective.com/mithriljs). We don't actively seek donations, but they are very much appreciated and are used to support development and related expenses. Both one-time and recurring donations are accepted. ================================================ FILE: docs/credits.md ================================================ # Credits Mithril.js was originally written by Leo Horie, but it is where it is today thanks to the hard work and great ideas of many people. Special thanks to: - Pat Cavit, who exposed most of the public API for Mithril.js 1.0, brought in test coverage and automated the publishing process - Claudia Meadows, who brought in linting, modernized the test suite and has been a strong voice in design discussions - Zoli Kahan, who replaced the original Promise implementation with one that actually worked properly - Alec Embke, who single-handedly wrote the JSON-P implementation - Barney Carroll, who suggested many great ideas and relentlessly pushed Mithril.js to the limit to uncover design issues prior to Mithril.js 1.0 - Dominic Gannaway, who offered insanely meticulous technical insight into rendering performance - Boris Letocha, whose search space reduction algorithm is the basis for Mithril.js' virtual DOM engine - Joel Richard, whose monomorphic virtual DOM structure is the basis for Mithril.js' vnode implementation - Simon Friis Vindum, whose open source work was an inspiration to many design decisions for Mithril.js 1.0 - Boris Kaul, for his awesome work on the benchmarking tools used to develop Mithril.js - Leon Sorokin, for writing a DOM instrumentation tool that helped improve performance in Mithril.js 1.0 - Jordan Walke, whose work on React was prior art to the implementation of keys in Mithril.js - Pierre-Yves Gérardy, who consistently makes high quality contributions - Gyandeep Singh, who contributed significant IE performance improvements Other people who also deserve recognition: - Arthur Clemens - creator of [Polythene](https://github.com/ArthurClemens/Polythene) and the [HTML-to-Mithril converter](https://arthurclemens.github.io/mithril-template-converter/index.html) - Stephan Hoyer - creator of [mithril-node-render](https://github.com/StephanHoyer/mithril-node-render), [mithril-query](https://github.com/StephanHoyer/mithril-query) and [mithril-source-hint](https://github.com/StephanHoyer/mithril-source-hint) - the countless people who have reported and fixed bugs, participated in discussions, and helped promote Mithril.js ================================================ FILE: docs/recent-changes.md ================================================ # Release v2.3.8 ### Patch Changes #### [refactor execSelector (@kfule)](https://github.com/MithrilJS/mithril.js/pull/3056) #### [Make `vnode.domSize` assignment consistent between create and update. (@kfule)](https://github.com/MithrilJS/mithril.js/pull/3055) This PR makes the code and behavior of create and update processes more consistent. #### [Bump rimraf from 6.0.1 to 6.1.0 in the normal group (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/3054) Bumps the normal group with 1 update: [rimraf](https://github.com/isaacs/rimraf). Updates `rimraf` from 6.0.1 to 6.1.0. Changelog. Sourced from rimraf's changelog. #### [Fix URI decoder bug and reduce bundle size through module tailoring and cleanup (@kfule)](https://github.com/MithrilJS/mithril.js/pull/3050) This fixes the URI decoder used in the Router to decode more strictly. #### [refactor `Vnode.normalizeChildren` (@kfule)](https://github.com/MithrilJS/mithril.js/pull/3052) `Vnode.normalizeChildren` now preallocates the array length and performs key-consistency checks after normalization. #### [Bump actions/setup-node from 5 to 6 in the normal group (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/3053) Bumps the normal group with 1 update: [actions/setup-node](https://github.com/actions/setup-node). Updates `actions/setup-node` from 5 to 6. Release notes. #### [Bump actions/setup-node from 4 to 5 in the normal group (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/3047) Bumps the normal group with 1 update: [actions/setup-node](https://github.com/actions/setup-node). Updates `actions/setup-node` from 4 to 5. Release notes. #### [docs: edited the link to the build badge (@Olexandr88)](https://github.com/MithrilJS/mithril.js/pull/3045) # Release v2.3.7 ### Patch Changes #### [Make the attrs of non-element vnodes always non-null. (@kfule)](https://github.com/MithrilJS/mithril.js/pull/3042) In #3041, it seemed that the case of non-element vnodes was not fully considered in terms of not breaking existing behavior. # Release v2.3.6 ### Patch Changes #### [Make the attrs of non-element vnodes always non-null. (@kfule)](https://github.com/MithrilJS/mithril.js/pull/3042) In #3041, it seemed that the case of non-element vnodes was not fully considered in terms of not breaking existing behavior. # Release v2.3.5 ### Patch Changes #### [Assorted Performance Improvements (@kfule)](https://github.com/MithrilJS/mithril.js/pull/3041) This PR improves performance through the following changes: Adoption of the spread syntax, which can be optimized in modern browsers. #### [Bump actions/checkout from 4 to 5 in the normal group (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/3039) Bumps the normal group with 1 update: [actions/checkout](https://github.com/actions/checkout). Updates `actions/checkout` from 4 to 5. Release notes. # Release v2.3.4 ### Patch Changes #### [Fix the error message selection condition (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/3037) The previous condition was basically "if this is non-nullish or a boolean". That "or a boolean" is very obviously redundant. #### [bundler: fix mangled comments and double suffixes (@kfule)](https://github.com/MithrilJS/mithril.js/pull/3032) This PR removes unnecessary suffixes from comments in the bundle file. It also fixes the strange double suffix (`mountRedraw00`). # Release v2.3.3 ### Patch Changes #### [router: delay mounting RouterRoot until the first route is resolved (fixes #2621) (@kfule)](https://github.com/MithrilJS/mithril.js/pull/3030) This PR delays the initial mounting of the router component until after the route has been resolved. #### [Bump glob from 11.0.2 to 11.0.3 in the normal group (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/3029) Bumps the normal group with 1 update: [glob](https://github.com/isaacs/node-glob). Updates `glob` from 11.0.2 to 11.0.3. Commits. af2e7ce 11.0.3. # Release v2.3.2 ### Patch Changes #### [Refactor router, fixes #2505 and #2778 (@kfule)](https://github.com/MithrilJS/mithril.js/pull/3027) This PR refactors the router code to fix two issues (#2505 and #2778). # Release v2.3.1 ### Patch Changes #### [set trailing slash optional in route matching (@touletan)](https://github.com/MithrilJS/mithril.js/pull/3025) Regexp has been updated to set trailing slash as optional in route matching. link to issue 3024. New test has been added. # Release v2.3.0 ### Minor Changes #### [feat: Make redraws when Promises returned by event handlers are completed (@kfule)](https://github.com/MithrilJS/mithril.js/pull/3020) This PR allows redraw on completion of the async event handler. This PR makes redraws when Promises returned by event handlers are completed. ### Patch Changes #### [Allow additional async redraw even if the first redraw is skipped (@kfule)](https://github.com/MithrilJS/mithril.js/pull/3021) This PR allows asynchronous redraw processing even if the first redraw is skipped by setting `event.redraw=false` before await in the async function. #### [Bump glob from 11.0.1 to 11.0.2 in the normal group (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/3019) Bumps the normal group with 1 update: [glob](https://github.com/isaacs/node-glob). Updates `glob` from 11.0.1 to 11.0.2. Commits. fd61f24 11.0.2. #### [Fix badge for build status (@kfule)](https://github.com/MithrilJS/mithril.js/pull/3015) The URL for the Shields.io badge for build status has been corrected. # Release v2.2.15 ### Patch Changes #### [[refactor] Limit the condition of the option tag to `selected` attribute in isFormAttribute() (@kfule)](https://github.com/MithrilJS/mithril.js/pull/3011) This PR limits the evaluation of whether a tag is `option` to only when setting the `selected` attribute. #### [test-perf: Load Benckmark.js first in Node.js (@kfule)](https://github.com/MithrilJS/mithril.js/pull/3008) Since Node21, global.navigator has been implemented, and together with browserMock, Benchmark.js incorrectly identifies the execution environment as a browser. # Release v2.2.14 ### Patch Changes #### [Improve handling of is-elements and Fix tiny bugs of setAttr()/updateStyle() (@kfule)](https://github.com/MithrilJS/mithril.js/pull/2988) Fixes a few tiny bugs in attributes and style properties updates, and improves handling of is-elements in updateNode(). #### [domFor: always get generation from delayedRemoval instead of parameter (@kfule)](https://github.com/MithrilJS/mithril.js/pull/3007) The `generation` of domFor is no longer passed as a parameter. This allows domFor to work well in onbeforeremove and onremove and reduces the amount of code. #### [render: wrap stateResult and attrsResult in Promise.resolve(), fix #2592 (@kfule)](https://github.com/MithrilJS/mithril.js/pull/3005) This PR wraps the return value of onbeforeremove in Promise.resolve(). This ensures that thenable objects are also always processed asynchronously. fix #2592. # Release v2.2.13 ### Patch Changes #### [Fix form checkValidity(), remove vnode.dom === .activeElement from setAttr() (Continued from #2257) (@kfule)](https://github.com/MithrilJS/mithril.js/pull/3002) Remove vnode.dom === activeElement(vnode.dom) from setAttribute() to fix validityCheck(), to fix https://github.com/MithrilJS/mithril.js/issues/2256. #### [Bump glob from 11.0.0 to 11.0.1 in the normal group (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/3001) Bumps the normal group with 1 update: [glob](https://github.com/isaacs/node-glob). Updates `glob` from 11.0.0 to 11.0.1. Commits. 148ef61 11.0.1. # Release v2.2.12 ### Patch Changes #### [disable Terser's "reduce_funcs" option for performance (@kfule)](https://github.com/MithrilJS/mithril.js/pull/3000) Terser's “reduce_funcs” option seems to degrade performance. So, disable it. #### [Bump chokidar from 4.0.1 to 4.0.3 in the normal group across 1 directory (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2998) Bumps the normal group with 1 update in the / directory: [chokidar](https://github.com/paulmillr/chokidar). Updates `chokidar` from 4.0.1 to 4.0.3. Release notes. # Release v2.2.11 ### Patch Changes #### [Use new pr-release prerelease hook (Fixes #2987) (@JAForbes)](https://github.com/MithrilJS/mithril.js/pull/2996) Per @dead-claudia's suggestion, pr-release now allows you to invoke a custom command before creating the github release. #### [updateStyle(): use setProperty() when css vars and dashed-properties, fixes #2989 (@kfule)](https://github.com/MithrilJS/mithril.js/pull/2991) This PR changes updateStyle() to use setProperty() for dashed-properties. This PR maybe fixes #2989. #### [Delete .github/ISSUE_TEMPLATE/0-docs.yml (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2993) Do a much better job discouraging filing docs bugs here. # Release v2.2.10 ### Patch Changes #### [[refactor] Performance improvement of updateStyle() (@kfule)](https://github.com/MithrilJS/mithril.js/pull/2985) This is a refactoring to improve the performance of `updateStyle()`. # Release v2.2.9 ### Patch Changes #### [[refactor] Refactoring of hyperscript.js and render.js, including performance improvements (@kfule)](https://github.com/MithrilJS/mithril.js/pull/2983) Refactor hyperscript.js and render.js. In particular, the replacement of fix #2622 appears to have significantly improved the performance regression. # Release v2.2.8 ### Patch Changes #### [m.domFor(): workaround for unintentional mangling. Fix #2842 (@kfule)](https://github.com/MithrilJS/mithril.js/pull/2981) Refactoring of domFor() for the internal bundler. https://github.com/MithrilJS/mithril.js/blob/cfa890f68571df1ab8543097f7fa61c34ee93683/mithril.js#L157. #### [Drop Istanbul to kill install warnings (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2979) Title's pretty self-explanatory. Also, this isn't really used much in practice. From a local run: ```. $ npm ci. # Release v2.2.7 ### Patch Changes #### [m.domFor(): workaround for unintentional mangling. Fix #2842 (@kfule)](https://github.com/MithrilJS/mithril.js/pull/2981) Refactoring of domFor() for the internal bundler. https://github.com/MithrilJS/mithril.js/blob/cfa890f68571df1ab8543097f7fa61c34ee93683/mithril.js#L157. #### [Drop Istanbul to kill install warnings (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2979) Title's pretty self-explanatory. Also, this isn't really used much in practice. From a local run: ```. $ npm ci. # Release v2.2.6 ### Patch Changes #### [m.domFor(): workaround for unintentional mangling. Fix #2842 (@kfule)](https://github.com/MithrilJS/mithril.js/pull/2981) Refactoring of domFor() for the internal bundler. https://github.com/MithrilJS/mithril.js/blob/cfa890f68571df1ab8543097f7fa61c34ee93683/mithril.js#L157. #### [Drop Istanbul to kill install warnings (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2979) Title's pretty self-explanatory. Also, this isn't really used much in practice. From a local run: ```. $ npm ci. # Release v2.2.5 ### Patch Changes #### [Bump the normal group across 1 directory with 2 updates (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2976) Bumps the normal group with 2 updates in the / directory: [chokidar](https://github.com/paulmillr/chokidar) and [eslint](https://github.com/eslint/eslint). #### [Cleaning up code by making vnode.attrs always non-null (@kfule)](https://github.com/MithrilJS/mithril.js/pull/2977) Commit f9e5163 made vnode.attrs always non-null, so there is no need for code to make vnode.attrs null or assume vnode.attrs is null. # Release v2.2.4 ### Patch Changes #### [Bump gh-pages from 2.1.1 to 5.0.0 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2890) Bumps [gh-pages](https://github.com/tschaub/gh-pages) from 2.1.1 to 5.0.0. Release notes. Sourced from gh-pages's releases. v5.0.0. #### [Bump @babel/parser from 7.7.5 to 7.25.6 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2959) Bumps [@babel/parser](https://github.com/babel/babel/tree/HEAD/packages/babel-parser) from 7.7.5 to 7.25.6. Release notes. Sourced from @​babel/parser's releases. #### [Bump minimatch from 3.0.4 to 3.1.2 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2816) Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.0.4 to 3.1.2. Commits. 699c459 3.1.2. 2f2b5ff fix: trim pattern. 25d7c0d 3.1.1. #### [Bump yaml and lint-staged (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2830) Bumps [yaml](https://github.com/eemeli/yaml) to 2.2.2 and updates ancestor dependency [lint-staged](https://github.com/okonet/lint-staged). #### [Bump gh-pages from 5.0.0 to 6.1.1 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2958) Bumps [gh-pages](https://github.com/tschaub/gh-pages) from 5.0.0 to 6.1.1. Release notes. Sourced from gh-pages's releases. v6.1.1. Fixes. #### [Bump glob from 7.1.4 to 11.0.0 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2957) Bumps [glob](https://github.com/isaacs/node-glob) from 7.1.4 to 11.0.0. Changelog. Sourced from glob's changelog. changeglob. 11.0. Drop support for node before v20. #### [Bump rimraf from 3.0.2 to 6.0.1 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2960) Bumps [rimraf](https://github.com/isaacs/rimraf) from 3.0.2 to 6.0.1. Changelog. Sourced from rimraf's changelog. 6.0. Drop support for nodes before v20. #### [Bump lint-staged from 13.2.1 to 15.2.10 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2961) [//]: # (dependabot-start). ⚠️ **Dependabot is rebasing this PR** ⚠️. Rebasing might not happen immediately, so don't worry if this takes some time. #### [Revise issue templates (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2955) #### [Update ospec and a few other dependencies (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2907) 1. Update ospec to the version I just published. 2. #### [Fix some outstanding bugs in the docs linter. (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2906) Missed an edge case in the task queue, and I also wanted to fully dedupe network requests. Locally it passes. #### [Rewrite docs linter, ease JSFiddle request debugging (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2904) I'll defer to the commit descriptions. They're self-descriptive. The first diff is quite large. #### [Update vnodes.md (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2903) Fix a broken link. Did some further digging (it's been a while since I've played with the scripts) and found that the JSFiddle errors are just warnings. #### [Migrate to Node 20, clean up workflows (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2902) **Note: ignore the commits. It's a mess. Just read the combined diff - the PR itself is the standalone unit. I plan to squash this as I merge anyways.**. #### [Remove dependance on global window and document (@KoryNunn)](https://github.com/MithrilJS/mithril.js/pull/2897) Use window and document from render target instead of using globals. This makes unit and intergration testing much easier. #### [Bump braces from 3.0.2 to 3.0.3 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2896) Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3. Commits. 74b2db2 3.0.3. 88f1429 update eslint. lint, fix unit tests. #### [Tweak docs with warning to fix #2508 (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2895) [z] Documentation change. [z] My change requires a change to the documentation. [z] I have updated the documentation accordingly. #### [Bump qs from 6.5.2 to 6.5.3 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2807) Bumps [qs](https://github.com/ljharb/qs) from 6.5.2 to 6.5.3. Changelog. Sourced from qs's changelog. 6.5.3. [Fix] parse: ignore __proto__ keys (#428). #### [Temporarily host REM on fly to fix the docs (@JAForbes)](https://github.com/MithrilJS/mithril.js/pull/2893) Fixes REM examples in the docs. The documentation currently has a dead link as REM is no longer hosted on heroku. #### [Move from individual code owners to just pinging all collaborators (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2844) Most collaborators have commit access, and it'd make it a little easier (and more likely) for pull requests to get reviewed. #### [docs: absolute url in version selector to avoid 404 errors (2 of 2) (@tbreuss)](https://github.com/MithrilJS/mithril.js/pull/2839) Fixes #2832 (2 of 2 pull requests). See my comment at https://github.com/MithrilJS/mithril.js/pull/2835#issuecomment-1535657892. #### [fix markdown editor example, bump marked.js version up (@tbreuss)](https://github.com/MithrilJS/mithril.js/pull/2848) Fixes the strange behavior of markdown editor example. Using newest version of marked.js, fixed strange behavior of markdown editor example. See #2845. #### [Bump word-wrap from 1.2.3 to 1.2.4 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2856) Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4. Release notes. Sourced from word-wrap's releases. 1.2.4. What's Changed. #### [Add missing `m.censor` to API navigation (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2843) Not sure how I forgot about this when I added the method. #### [docs: fix regex for parsing page title (@tbreuss)](https://github.com/MithrilJS/mithril.js/pull/2837) Fixes https://github.com/MithrilJS/mithril.js/issues/2833. I tested the generated documentation on my dev machine successfully. #### [docs: fix broken anchor link on github/npm (@tbreuss)](https://github.com/MithrilJS/mithril.js/pull/2838) Fixed a not working anchor link on github and npm by removing the question mark. #### [hyperscript: handles shared empty attrs, fixes #2821 (@kfule)](https://github.com/MithrilJS/mithril.js/pull/2822) Whenever there are selector-derived attrs, the attrs object will be regenerated and not shared. #### [Fix typos in `stream()` docs (@mtsknn)](https://github.com/MithrilJS/mithril.js/pull/2825) Noticed these typos while reading through the page. #### [Bump async from 2.6.3 to 2.6.4 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2815) Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4. Changelog. Sourced from async's changelog. v2.6.4. # Release v2.2.3 ### Patch Changes #### [Use markdown for the README badges (@pygy)](https://github.com/MithrilJS/mithril.js/pull/2773) Use markdown for the README badges. # Release v2.2.2 ### Patch Changes #### [Use markdown for the README badges (@pygy)](https://github.com/MithrilJS/mithril.js/pull/2773) Use markdown for the README badges. # Release v2.2.1 ### Patch Changes #### [Move the chat to Zulip (@pygy)](https://github.com/MithrilJS/mithril.js/pull/2771) This updates the documentation to link to the new Zulip chat room. # Release v2.2.0 ### Minor Changes #### [m.censor: work around a bunder bug (@kfule)](https://github.com/MithrilJS/mithril.js/pull/2752) The internal bundler sometimes mangles the words in RegExp literals incorrectly. Please see below. #### [Warn about reusing mutated attrs object - fixes #2719 (@StephanHoyer)](https://github.com/MithrilJS/mithril.js/pull/2722) #### [Send URLSearchParams as request body without extra configuration (@Coteh)](https://github.com/MithrilJS/mithril.js/pull/2695) This PR fixes an oddity I noticed in the way `m.request` handles `URLSearchParams` object. It now handles it in the same sort of way XHR and Fetch do it. #### [Add `params:` to `m.route.Link`, fix docs (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2537) Add `params:` to `m.route.Link`. Minor fix to docs to reflect reality with `m.route.Link`'s `disabled:` attribute. #### [Allow Mithril to be loaded in non-browser environments without modification (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2633) Recast the global reads to all be guarded with `typeof`, so that if they aren't defined, they're just `null`. #### [Add a `m.Fragment = "["` utility for JSX users. (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2744) The title says it all, and the diff's obvious. Resolves https://github.com/MithrilJS/mithril.js/issues/2640 and probably others. ### Patch Changes #### [Enable --minimize-semver-change for pr-release (@JAForbes)](https://github.com/MithrilJS/mithril.js/pull/2769) Minimizes semver changes on release to the minimum required version bump to satisfy major/minor/patch semver ranges. Minimizes the semver change so that. #### [Clean up m.route.Link (@barneycarroll)](https://github.com/MithrilJS/mithril.js/pull/2768) An attempt at better demonstrating `m.route.Link` with less text. Fixes #2767. #### [Runtime-deprecate ospec, change `change-log` to `changelog`, fix a few assorted bugs (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2578) This PR is in two parts: 1. Revise the build system and some of the local dev setup. Fully split ospec from the repo, and add it as a dependency. #### [Add meta description to docs (@StephanHoyer)](https://github.com/MithrilJS/mithril.js/pull/2316) rework of #2149. added a meta description parser and meta descriptions to all docs pages. because google. built the docs, inspected the output manually. #### [Fixed badges, consistent naming of Mithril.js (@tbreuss)](https://github.com/MithrilJS/mithril.js/pull/2750) use consistent naming of Mithril.js. fix badges in README. Fixes issue #2749. #### [Catch malformed URI Components (@jdiderik)](https://github.com/MithrilJS/mithril.js/pull/2711) Fix for error thrown when a value contains non-valid / malformed URI Component. Example: test=%c5%a1%e8ZM%80%82H. will throw "URI malformed". #### [Correctly handle invalid escapes in routes based on 0a5ead31c9fbd7b153c521c7f9d3df7bf826ce6c (@StephanHoyer)](https://github.com/MithrilJS/mithril.js/pull/2743) fixes #2061. @dead-claudia I just redid your change but slightly different in order to handle a mix of wrong and right encodings properly. #### [Standardise vnode text representation (@barneycarroll)](https://github.com/MithrilJS/mithril.js/pull/2670) This addresses the crucial feature of #2669: text is always represented as virtual text nodes, never as a `vnode.text`. #### [Issue 2624 no content 204 parse (@Evoke-PHP)](https://github.com/MithrilJS/mithril.js/pull/2641) Added guard so that JSON.parse does not fail on IE11 with no content empty string being parsed. Fixes https://github.com/MithrilJS/mithril.js/issues/2624. #### [[m.request] work around a bundler bug, fix #2647 (@pygy)](https://github.com/MithrilJS/mithril.js/pull/2655) The bundler mangles identifier-like strings within RegExps, this works around the problem by not using such RegExps. #### [Reject request on XHR timeout (@kevinfiol)](https://github.com/MithrilJS/mithril.js/pull/2646) Derived from PR #2581. Allows requests to properly reject on event of a timeout. #### [Remove extra isLifecycleMethod call from removeAttr (@ZeikJT)](https://github.com/MithrilJS/mithril.js/pull/2594) Removing an extra isLifecycleMethod in the removeAttr method, it isn't needed since it's already checked on the previous line. #### [Fix #2601 (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2603) Fix issue where ending a stream in the middle of a stream callback would result in erroneous parent stream state for the rest of that emit. Fixes #2601. #### [Add streams to releases again, include minified bundle, drop internal stuff from npm (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2539) Add `stream/stream.js` to releases again. Add `stream/stream.min.js` now that the process is remotely sane now. #### [Make errors and their messages more accurate and helpful (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2536) I updated error messages to be much more helpful. #### [Fix assertion descriptions (@soulofmischief)](https://github.com/MithrilJS/mithril.js/pull/2405) I moved the return statement to the end of define() so that it returns even if the comparison fails. #### [Fix branch target (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2765) https://github.com/MithrilJS/mithril.js/runs/6199543939?check_suite_focus=true. #### [Automate mithril's release workflow (@JAForbes)](https://github.com/MithrilJS/mithril.js/pull/2760) Automated releases, pre-releases, (code) rollbacks and recovery, npm publishing, change log management just by using normal github flow. #### [rework jsx docs (@StephanHoyer)](https://github.com/MithrilJS/mithril.js/pull/2758) #### [Add Simple Application Flems Supporting v2.0.4 and up (@tbreuss)](https://github.com/MithrilJS/mithril.js/pull/2751) Added Flems for Simple Application supporting v2.0.4 of Mithril.js. Fixes Issue #2710. #### [Make example work with webpack v5.69.1 (@StephanHoyer)](https://github.com/MithrilJS/mithril.js/pull/2757) fixes #2634. #### [2604: correct and move text about statements in view method (@kevinfiol)](https://github.com/MithrilJS/mithril.js/pull/2748) Addresses #2604. #### [Fix lint errors (@StephanHoyer)](https://github.com/MithrilJS/mithril.js/pull/2745) #### [WIP: Update modularisation details in Installation docs (@orbitbot)](https://github.com/MithrilJS/mithril.js/pull/2620) added link to flems.io as an easier way to just try out the framework. -. Documentation has grown a bit stale. #### [Added power support for the travis.yml file with ppc64le (@sreekanth370)](https://github.com/MithrilJS/mithril.js/pull/2644) Added power support for the travis.yml file with ppc64le. This is part of the Ubuntu distribution for ppc64le. #### [Updated babel/webpack docs to work with latest versions (@pereriksson)](https://github.com/MithrilJS/mithril.js/pull/2649) As a developer I tried setting up Mithril with Babel and Webpack but failed because of a variety of errors. #### [[docs] route redirection using the history API (@pygy)](https://github.com/MithrilJS/mithril.js/pull/1767) This is an attempt at fixing #1759, but there may be more to be added. Feedback welcome. ping @dontwork. #### [Bump path-parse from 1.0.6 to 1.0.7 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2718) Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7. Commits. See full diff in compare view. #### [Bump glob-parent from 5.1.0 to 5.1.2 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2693) Bumps [glob-parent](https://github.com/gulpjs/glob-parent) from 5.1.0 to 5.1.2. Release notes. Sourced from glob-parent's releases. v5.1.2. Bug Fixes. #### [Bump ajv from 6.10.2 to 6.12.6 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2741) Bumps [ajv](https://github.com/ajv-validator/ajv) from 6.10.2 to 6.12.6. Release notes. Sourced from ajv's releases. v6.12.6. Fix performance issue of "url" format. #### [Update standalone usage (@ghost)](https://github.com/MithrilJS/mithril.js/pull/2651) #### [Avoid double encoding of function signatures - fixes #2720 (@StephanHoyer)](https://github.com/MithrilJS/mithril.js/pull/2721) #### [Show previous versions (@mike-ward)](https://github.com/MithrilJS/mithril.js/pull/2353) Add Dropdown that shows links to archived versions of the documentation. #### [docs: improve m.request return value description (@GAumala)](https://github.com/MithrilJS/mithril.js/pull/2206) In the m.request return value description, add a line informing that error status codes cause the promise to reject. #### [A note on JSX events (@pereriksson)](https://github.com/MithrilJS/mithril.js/pull/2648) Naming JSX events according to their documentation produces unexpected results with incorrectly named events when using JSX with Mithril. #### [Document route resolution cancellation, fixes #1759 (@barneycarroll)](https://github.com/MithrilJS/mithril.js/pull/2672) Also fixes a broken internal link. #### [Bump marked from 0.7.0 to 4.0.10 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2717) Bumps [marked](https://github.com/markedjs/marked) from 0.7.0 to 4.0.10. Release notes. Sourced from marked's releases. v4.0.10. 4.0.10 (2022-01-13). Bug Fixes. #### [Flems in docs (#2348) [skip ci] (@porsager)](https://github.com/MithrilJS/mithril.js/pull/2348) Added flems instead of the current codepen samples. #### [Remove old TOC link (@ArthurClemens)](https://github.com/MithrilJS/mithril.js/pull/2698) Content was moved some time ago and linked section no longer exists. #### [Cavemansspa patch 1 (@cavemansspa)](https://github.com/MithrilJS/mithril.js/pull/2696) Documentation update. #### [Bump hosted-git-info from 2.8.4 to 2.8.9 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2684) Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.4 to 2.8.9. Changelog. Sourced from hosted-git-info's changelog. 2.8.9 (2021-04-07). #### [Bump lodash from 4.17.20 to 4.17.21 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2680) Bumps [lodash](https://github.com/lodash/lodash) from 4.17.20 to 4.17.21. Commits. f299b52 Bump to v4.17.21. #### [Bump handlebars from 4.7.6 to 4.7.7 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2679) Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.7.6 to 4.7.7. Changelog. Sourced from handlebars's changelog. v4.7.7 - February 15th, 2021. #### [Remove unreachable keyed node logic, fixes #2597 (@barneycarroll)](https://github.com/MithrilJS/mithril.js/pull/2673) #### [Delete test-utils/README.md (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2674) We don't expose this publicly anymore, so there's literally no justification for this file's existence. #### [simple-application.md: consistent use of type=submit (@danbst)](https://github.com/MithrilJS/mithril.js/pull/2657) When following tutorial and typing everything in, I was confused that Save button didn't work. #### [Fix inconsistent capitalizations of "JavaScript" (@mtsknn)](https://github.com/MithrilJS/mithril.js/pull/2639) "Javascript"/"javascript" → "JavaScript". Fixes #2398, or at least I can't find any more incorrect capitalizations. #### [fix some typos (@osban)](https://github.com/MithrilJS/mithril.js/pull/2487) Found some typos. Mainly unescaped `|` in tables, but also a few other irregularities. Not all problems are visible in the website docs. #### [Replace mocha by ospec in testing page (@gamtiq)](https://github.com/MithrilJS/mithril.js/pull/2585) Fixed a typo in testing doc page. Currently there is reference to `mocha` in the page whereas `opsec` is used. #### [Bump acorn from 7.1.0 to 7.4.0 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2630) Bumps [acorn](https://github.com/acornjs/acorn) from 7.1.0 to 7.4.0. Commits. 54efb62 Mark version 7.4.0. #### [Bump handlebars from 4.4.2 to 4.7.6 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2629) Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.4.2 to 4.7.6. Changelog. Sourced from handlebars's changelog. v4.7.6 - April 3rd, 2020. #### [Bump lodash from 4.17.15 to 4.17.20 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2628) Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.20. Commits. ded9bc6 Bump to v4.17.20. 63150ef Documentation fixes. #### [Bump minimist from 1.2.0 to 1.2.3 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2627) Bumps [minimist](https://github.com/substack/minimist) from 1.2.0 to 1.2.3. Commits. 6457d74 1.2.3. 38a4d1c even more aggressive checks for protocol pollution. #### [Update installation.md (@purefan)](https://github.com/MithrilJS/mithril.js/pull/2608) Offer to install mithril as a webpack plugin. Just makes my life easier by not having to include mithril in every one of my js files. #### [replace slave label with replica (@stephanos)](https://github.com/MithrilJS/mithril.js/pull/2605) One of the example is using the antiquated word "slave" for a database replica. I updated the language and tested the change. #### [ES6 and m.trust docs patch (@kczx3)](https://github.com/MithrilJS/mithril.js/pull/2593) While reading through some of the documentation I saw some issues with both the ES6 and `m.trust` pages. #### [docs: Fix simple typo, subsequece -> subsequence (@timgates42)](https://github.com/MithrilJS/mithril.js/pull/2582) There is a small typo in mithril.js, render/render.js. Should read `subsequence` rather than `subsequece`. #### [change link to go to ospec instead of mocha (@akessner)](https://github.com/MithrilJS/mithril.js/pull/2576) Change the link to point to ospec docs in github. ospec link went to mochajs. [issue 2575](https://github.com/MithrilJS/mithril.js/issues/2575). N/A. N/A. N/A. #### [updated to the Vimeo showcase (@CreaturesInUnitards)](https://github.com/MithrilJS/mithril.js/pull/2573) The scrimba version of Mithril 0-60 was built on their beta platform, and doesn't really even work anymore. #### [adding more community examples (@boazblake)](https://github.com/MithrilJS/mithril.js/pull/2567) #### [Exclude archive of previous docs (@cztomsik)](https://github.com/MithrilJS/mithril.js/pull/2561) update .npmignore so that archives are not included in the resulting package. space/bandwidth savings. fix #2552. #### [Pimp the docs linter (and assorted changes) (@pygy)](https://github.com/MithrilJS/mithril.js/pull/2553) Add an optional cache for faster runs. Add a final report. Don't return anything from `exec()`. Cover more files. Look for a "--cache" option. #### [Recast key docs to be much clearer and more accurate (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2540) Recast key docs to be much clearer and more accurate, including a few Flems examples to help intuitively explain things. #### [Add `m.censor`, adjust `m.route.Link` to use it (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2538) Add `m.censor`. Adjust `m.route.Link` to use it. Restructure a few things for better code reuse. Fixes #2472. #### [Update fetch() browser support in docs (@qgustavor)](https://github.com/MithrilJS/mithril.js/pull/2522) As [Can I use](https://caniuse.com/#feat=fetch) shows `fetch()` supported since Safari 10.1 and iOS Safari 10.3. #### [docs: Add release dates to all change-log files (@maranomynet)](https://github.com/MithrilJS/mithril.js/pull/2513) I'd like to introduce release dates to the change log files. Release dates are human-friendly and add a bit of historical perspective to change-log files. # Release v2.1.0 ### Minor Changes #### [m.censor: work around a bunder bug (@kfule)](https://github.com/MithrilJS/mithril.js/pull/2752) The internal bundler sometimes mangles the words in RegExp literals incorrectly. Please see below. #### [Warn about reusing mutated attrs object - fixes #2719 (@StephanHoyer)](https://github.com/MithrilJS/mithril.js/pull/2722) #### [Send URLSearchParams as request body without extra configuration (@Coteh)](https://github.com/MithrilJS/mithril.js/pull/2695) This PR fixes an oddity I noticed in the way `m.request` handles `URLSearchParams` object. It now handles it in the same sort of way XHR and Fetch do it. #### [Add `params:` to `m.route.Link`, fix docs (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2537) Add `params:` to `m.route.Link`. Minor fix to docs to reflect reality with `m.route.Link`'s `disabled:` attribute. #### [Allow Mithril to be loaded in non-browser environments without modification (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2633) Recast the global reads to all be guarded with `typeof`, so that if they aren't defined, they're just `null`. #### [Add a `m.Fragment = "["` utility for JSX users. (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2744) The title says it all, and the diff's obvious. Resolves https://github.com/MithrilJS/mithril.js/issues/2640 and probably others. ### Patch Changes #### [Enable --minimize-semver-change for pr-release (@JAForbes)](https://github.com/MithrilJS/mithril.js/pull/2769) Minimizes semver changes on release to the minimum required version bump to satisfy major/minor/patch semver ranges. Minimizes the semver change so that. #### [Clean up m.route.Link (@barneycarroll)](https://github.com/MithrilJS/mithril.js/pull/2768) An attempt at better demonstrating `m.route.Link` with less text. Fixes #2767. #### [Runtime-deprecate ospec, change `change-log` to `changelog`, fix a few assorted bugs (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2578) This PR is in two parts: 1. Revise the build system and some of the local dev setup. Fully split ospec from the repo, and add it as a dependency. #### [Add meta description to docs (@StephanHoyer)](https://github.com/MithrilJS/mithril.js/pull/2316) rework of #2149. added a meta description parser and meta descriptions to all docs pages. because google. built the docs, inspected the output manually. #### [Fixed badges, consistent naming of Mithril.js (@tbreuss)](https://github.com/MithrilJS/mithril.js/pull/2750) use consistent naming of Mithril.js. fix badges in README. Fixes issue #2749. #### [Catch malformed URI Components (@jdiderik)](https://github.com/MithrilJS/mithril.js/pull/2711) Fix for error thrown when a value contains non-valid / malformed URI Component. Example: test=%c5%a1%e8ZM%80%82H. will throw "URI malformed". #### [Correctly handle invalid escapes in routes based on 0a5ead31c9fbd7b153c521c7f9d3df7bf826ce6c (@StephanHoyer)](https://github.com/MithrilJS/mithril.js/pull/2743) fixes #2061. @dead-claudia I just redid your change but slightly different in order to handle a mix of wrong and right encodings properly. #### [Standardise vnode text representation (@barneycarroll)](https://github.com/MithrilJS/mithril.js/pull/2670) This addresses the crucial feature of #2669: text is always represented as virtual text nodes, never as a `vnode.text`. #### [Issue 2624 no content 204 parse (@Evoke-PHP)](https://github.com/MithrilJS/mithril.js/pull/2641) Added guard so that JSON.parse does not fail on IE11 with no content empty string being parsed. Fixes https://github.com/MithrilJS/mithril.js/issues/2624. #### [[m.request] work around a bundler bug, fix #2647 (@pygy)](https://github.com/MithrilJS/mithril.js/pull/2655) The bundler mangles identifier-like strings within RegExps, this works around the problem by not using such RegExps. #### [Reject request on XHR timeout (@kevinfiol)](https://github.com/MithrilJS/mithril.js/pull/2646) Derived from PR #2581. Allows requests to properly reject on event of a timeout. #### [Remove extra isLifecycleMethod call from removeAttr (@ZeikJT)](https://github.com/MithrilJS/mithril.js/pull/2594) Removing an extra isLifecycleMethod in the removeAttr method, it isn't needed since it's already checked on the previous line. #### [Fix #2601 (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2603) Fix issue where ending a stream in the middle of a stream callback would result in erroneous parent stream state for the rest of that emit. Fixes #2601. #### [Add streams to releases again, include minified bundle, drop internal stuff from npm (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2539) Add `stream/stream.js` to releases again. Add `stream/stream.min.js` now that the process is remotely sane now. #### [Make errors and their messages more accurate and helpful (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2536) I updated error messages to be much more helpful. #### [Fix assertion descriptions (@soulofmischief)](https://github.com/MithrilJS/mithril.js/pull/2405) I moved the return statement to the end of define() so that it returns even if the comparison fails. #### [Fix branch target (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2765) https://github.com/MithrilJS/mithril.js/runs/6199543939?check_suite_focus=true. #### [Automate mithril's release workflow (@JAForbes)](https://github.com/MithrilJS/mithril.js/pull/2760) Automated releases, pre-releases, (code) rollbacks and recovery, npm publishing, change log management just by using normal github flow. #### [rework jsx docs (@StephanHoyer)](https://github.com/MithrilJS/mithril.js/pull/2758) #### [Add Simple Application Flems Supporting v2.0.4 and up (@tbreuss)](https://github.com/MithrilJS/mithril.js/pull/2751) Added Flems for Simple Application supporting v2.0.4 of Mithril.js. Fixes Issue #2710. #### [Make example work with webpack v5.69.1 (@StephanHoyer)](https://github.com/MithrilJS/mithril.js/pull/2757) fixes #2634. #### [2604: correct and move text about statements in view method (@kevinfiol)](https://github.com/MithrilJS/mithril.js/pull/2748) Addresses #2604. #### [Fix lint errors (@StephanHoyer)](https://github.com/MithrilJS/mithril.js/pull/2745) #### [WIP: Update modularisation details in Installation docs (@orbitbot)](https://github.com/MithrilJS/mithril.js/pull/2620) added link to flems.io as an easier way to just try out the framework. -. Documentation has grown a bit stale. #### [Added power support for the travis.yml file with ppc64le (@sreekanth370)](https://github.com/MithrilJS/mithril.js/pull/2644) Added power support for the travis.yml file with ppc64le. This is part of the Ubuntu distribution for ppc64le. #### [Updated babel/webpack docs to work with latest versions (@pereriksson)](https://github.com/MithrilJS/mithril.js/pull/2649) As a developer I tried setting up Mithril with Babel and Webpack but failed because of a variety of errors. #### [[docs] route redirection using the history API (@pygy)](https://github.com/MithrilJS/mithril.js/pull/1767) This is an attempt at fixing #1759, but there may be more to be added. Feedback welcome. ping @dontwork. #### [Bump path-parse from 1.0.6 to 1.0.7 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2718) Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7. Commits. See full diff in compare view. #### [Bump glob-parent from 5.1.0 to 5.1.2 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2693) Bumps [glob-parent](https://github.com/gulpjs/glob-parent) from 5.1.0 to 5.1.2. Release notes. Sourced from glob-parent's releases. v5.1.2. Bug Fixes. #### [Bump ajv from 6.10.2 to 6.12.6 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2741) Bumps [ajv](https://github.com/ajv-validator/ajv) from 6.10.2 to 6.12.6. Release notes. Sourced from ajv's releases. v6.12.6. Fix performance issue of "url" format. #### [Update standalone usage (@ghost)](https://github.com/MithrilJS/mithril.js/pull/2651) #### [Avoid double encoding of function signatures - fixes #2720 (@StephanHoyer)](https://github.com/MithrilJS/mithril.js/pull/2721) #### [Show previous versions (@mike-ward)](https://github.com/MithrilJS/mithril.js/pull/2353) Add Dropdown that shows links to archived versions of the documentation. #### [docs: improve m.request return value description (@GAumala)](https://github.com/MithrilJS/mithril.js/pull/2206) In the m.request return value description, add a line informing that error status codes cause the promise to reject. #### [A note on JSX events (@pereriksson)](https://github.com/MithrilJS/mithril.js/pull/2648) Naming JSX events according to their documentation produces unexpected results with incorrectly named events when using JSX with Mithril. #### [Document route resolution cancellation, fixes #1759 (@barneycarroll)](https://github.com/MithrilJS/mithril.js/pull/2672) Also fixes a broken internal link. #### [Bump marked from 0.7.0 to 4.0.10 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2717) Bumps [marked](https://github.com/markedjs/marked) from 0.7.0 to 4.0.10. Release notes. Sourced from marked's releases. v4.0.10. 4.0.10 (2022-01-13). Bug Fixes. #### [Flems in docs (#2348) [skip ci] (@porsager)](https://github.com/MithrilJS/mithril.js/pull/2348) Added flems instead of the current codepen samples. #### [Remove old TOC link (@ArthurClemens)](https://github.com/MithrilJS/mithril.js/pull/2698) Content was moved some time ago and linked section no longer exists. #### [Cavemansspa patch 1 (@cavemansspa)](https://github.com/MithrilJS/mithril.js/pull/2696) Documentation update. #### [Bump hosted-git-info from 2.8.4 to 2.8.9 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2684) Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.4 to 2.8.9. Changelog. Sourced from hosted-git-info's changelog. 2.8.9 (2021-04-07). #### [Bump lodash from 4.17.20 to 4.17.21 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2680) Bumps [lodash](https://github.com/lodash/lodash) from 4.17.20 to 4.17.21. Commits. f299b52 Bump to v4.17.21. #### [Bump handlebars from 4.7.6 to 4.7.7 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2679) Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.7.6 to 4.7.7. Changelog. Sourced from handlebars's changelog. v4.7.7 - February 15th, 2021. #### [Remove unreachable keyed node logic, fixes #2597 (@barneycarroll)](https://github.com/MithrilJS/mithril.js/pull/2673) #### [Delete test-utils/README.md (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2674) We don't expose this publicly anymore, so there's literally no justification for this file's existence. #### [simple-application.md: consistent use of type=submit (@danbst)](https://github.com/MithrilJS/mithril.js/pull/2657) When following tutorial and typing everything in, I was confused that Save button didn't work. #### [Fix inconsistent capitalizations of "JavaScript" (@mtsknn)](https://github.com/MithrilJS/mithril.js/pull/2639) "Javascript"/"javascript" → "JavaScript". Fixes #2398, or at least I can't find any more incorrect capitalizations. #### [fix some typos (@osban)](https://github.com/MithrilJS/mithril.js/pull/2487) Found some typos. Mainly unescaped `|` in tables, but also a few other irregularities. Not all problems are visible in the website docs. #### [Replace mocha by ospec in testing page (@gamtiq)](https://github.com/MithrilJS/mithril.js/pull/2585) Fixed a typo in testing doc page. Currently there is reference to `mocha` in the page whereas `opsec` is used. #### [Bump acorn from 7.1.0 to 7.4.0 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2630) Bumps [acorn](https://github.com/acornjs/acorn) from 7.1.0 to 7.4.0. Commits. 54efb62 Mark version 7.4.0. #### [Bump handlebars from 4.4.2 to 4.7.6 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2629) Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.4.2 to 4.7.6. Changelog. Sourced from handlebars's changelog. v4.7.6 - April 3rd, 2020. #### [Bump lodash from 4.17.15 to 4.17.20 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2628) Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.20. Commits. ded9bc6 Bump to v4.17.20. 63150ef Documentation fixes. #### [Bump minimist from 1.2.0 to 1.2.3 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2627) Bumps [minimist](https://github.com/substack/minimist) from 1.2.0 to 1.2.3. Commits. 6457d74 1.2.3. 38a4d1c even more aggressive checks for protocol pollution. #### [Update installation.md (@purefan)](https://github.com/MithrilJS/mithril.js/pull/2608) Offer to install mithril as a webpack plugin. Just makes my life easier by not having to include mithril in every one of my js files. #### [replace slave label with replica (@stephanos)](https://github.com/MithrilJS/mithril.js/pull/2605) One of the example is using the antiquated word "slave" for a database replica. I updated the language and tested the change. #### [ES6 and m.trust docs patch (@kczx3)](https://github.com/MithrilJS/mithril.js/pull/2593) While reading through some of the documentation I saw some issues with both the ES6 and `m.trust` pages. #### [docs: Fix simple typo, subsequece -> subsequence (@timgates42)](https://github.com/MithrilJS/mithril.js/pull/2582) There is a small typo in mithril.js, render/render.js. Should read `subsequence` rather than `subsequece`. #### [change link to go to ospec instead of mocha (@akessner)](https://github.com/MithrilJS/mithril.js/pull/2576) Change the link to point to ospec docs in github. ospec link went to mochajs. [issue 2575](https://github.com/MithrilJS/mithril.js/issues/2575). N/A. N/A. N/A. #### [updated to the Vimeo showcase (@CreaturesInUnitards)](https://github.com/MithrilJS/mithril.js/pull/2573) The scrimba version of Mithril 0-60 was built on their beta platform, and doesn't really even work anymore. #### [adding more community examples (@boazblake)](https://github.com/MithrilJS/mithril.js/pull/2567) #### [Exclude archive of previous docs (@cztomsik)](https://github.com/MithrilJS/mithril.js/pull/2561) update .npmignore so that archives are not included in the resulting package. space/bandwidth savings. fix #2552. #### [Pimp the docs linter (and assorted changes) (@pygy)](https://github.com/MithrilJS/mithril.js/pull/2553) Add an optional cache for faster runs. Add a final report. Don't return anything from `exec()`. Cover more files. Look for a "--cache" option. #### [Recast key docs to be much clearer and more accurate (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2540) Recast key docs to be much clearer and more accurate, including a few Flems examples to help intuitively explain things. #### [Add `m.censor`, adjust `m.route.Link` to use it (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2538) Add `m.censor`. Adjust `m.route.Link` to use it. Restructure a few things for better code reuse. Fixes #2472. #### [Update fetch() browser support in docs (@qgustavor)](https://github.com/MithrilJS/mithril.js/pull/2522) As [Can I use](https://caniuse.com/#feat=fetch) shows `fetch()` supported since Safari 10.1 and iOS Safari 10.3. #### [docs: Add release dates to all change-log files (@maranomynet)](https://github.com/MithrilJS/mithril.js/pull/2513) I'd like to introduce release dates to the change log files. Release dates are human-friendly and add a bit of historical perspective to change-log files. ================================================ FILE: docs/releasing.md ================================================ # Mithril.js Release Processes Mithril.js' release process is automated by [pr-release]. pr-release is maintained by a long time Mithril.js community member [@JAForbes](https://github.com/JAForbes). pr-release handles the following: - Generating changelog entries - Automating the semver version - Publishing releases and pre-releases to npm - Creating github releases - Rollbacks ## For contributors Contributors should create their feature branch targetting the default branch `main`. When this branch is merged `pr-release` will either generate or update a release PR from `main` to `release`. The description and title will be managed by [pr-release], including the semver version. Contributors who have permissions should add the correct semver label to their PR (`major` | `minor` | `patch`). If no label is set, `patch` is assumed. If you do not have permissions, the maintainer will set the label on your behalf. ## Changelog Currently, `docs/recent-changes.md` holds an automatically prepended log of changes, managed by pr-release. Ideally, I want to get rid of this and just have pr-release somehow push to https://github.com/MithrilJS/docs automatically, but that may take some work. ## For maintainers Whenever a new feature branch is opened, a reviewing maintainer should add the correct semver label to their PR (`major` | `minor` | `patch`). If no label is set, `patch` is assumed. If a `major` or `minor` feature branch is merged but no labels were set, you can still go back and edit the semver labels. On label change the release pr will automatically be regenerated and will recalculate the semver version. [pr-release]: https://pr-release.org/ ================================================ FILE: hyperscript.js ================================================ "use strict" var hyperscript = require("./render/hyperscript") hyperscript.trust = require("./render/trust") hyperscript.fragment = require("./render/fragment") module.exports = hyperscript ================================================ FILE: index.js ================================================ "use strict" var hyperscript = require("./hyperscript") var mountRedraw = require("./mount-redraw") var request = require("./request") var router = require("./route") 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 = router 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 = require("./render/domFor") module.exports = m ================================================ FILE: mithril.js ================================================ ;(function() { "use strict" function Vnode(tag, key, attrs0, children, text, dom) { return {tag: tag, key: key, attrs: attrs0, children: children, text: text, dom: dom, is: undefined, 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) { // Preallocate the array length (initially holey) and fill every index immediately in order. // Benchmarking shows better performance on V8. var children = new Array(input.length) // Count the number of keyed normalized vnodes for consistency check. // Note: this is a perf-sensitive check. // Fun fact: merging the loop like this is somehow faster than splitting // the check within updateNodes(), noticeably so. var numKeyed = 0 for (var i = 0; i < input.length; i++) { children[i] = Vnode.normalize(input[i]) if (children[i] !== null && children[i].key != null) numKeyed++ } if (numKeyed !== 0 && numKeyed !== input.length) { throw new TypeError(children.includes(null) ? "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." ) } return children } // Note: the processing of variadic parameters is perf-sensitive. // // In native ES6, it might be preferable to define hyperscript and fragment // factories with a final ...args parameter and call hyperscriptVnode(...args), // since modern engines can optimize spread calls. // // However, benchmarks showed this was not faster. As a result, spread is used // only in the parameter lists of hyperscript and fragment, while an array is // passed to hyperscriptVnode. 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 } return Vnode("", attrs1 && attrs1.key, attrs1, children0) } // This exists so I'm only saving it once. var hasOwn = {}.hasOwnProperty // This is an attrs object that is used by default when attrs is undefined or null. var emptyAttrs = {} // This Map manages the following: // - Whether an attrs is cached attrs generated by compileSelector(). // - Whether the cached attrs is "static", i.e., does not contain any form attributes. // These information will be useful to skip updating attrs in render(). // // Since the attrs used as keys in this map are not released from the selectorCache object, // there is no risk of memory leaks. Therefore, Map is used here instead of WeakMap. var cachedAttrsIsStaticMap = new Map([[emptyAttrs, true]]) 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 isFormAttributeKey(key) { return key === "value" || key === "checked" || key === "selectedIndex" || key === "selected" } function compileSelector(selector) { var match, tag = "div", classes = [], attrs = {}, isStatic = true 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 (isFormAttributeKey(match[4])) isStatic = false } } } if (classes.length > 0) attrs.className = classes.join(" ") if (isEmpty(attrs)) attrs = emptyAttrs else cachedAttrsIsStaticMap.set(attrs, isStatic) return selectorCache[selector] = {tag: tag, attrs: attrs, is: attrs.is} } function execSelector(state, vnode) { vnode.tag = state.tag var attrs = vnode.attrs if (attrs == null) { vnode.attrs = state.attrs vnode.is = state.is return vnode } if (hasOwn.call(attrs, "class")) { if (attrs.class != null) attrs.className = attrs.class attrs.class = null } if (state.attrs !== emptyAttrs) { var className = attrs.className attrs = Object.assign({}, state.attrs, attrs) if (state.attrs.className != null) attrs.className = className != null ? String(state.attrs.className) + " " + String(className) : state.attrs.className } // 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) } // This reduces the complexity of the evaluation of "is" within the render function. vnode.is = attrs.is vnode.attrs = attrs return vnode } function hyperscript(selector, attrs, ...children) { if (selector == null || typeof selector !== "string" && typeof selector !== "function" && typeof selector.view !== "function") { throw Error("The selector must be either a string or a component."); } var vnode = hyperscriptVnode(attrs, children) if (typeof selector === "string") { vnode.children = Vnode.normalizeChildren(vnode.children) if (selector !== "[") return execSelector(selectorCache[selector] || compileSelector(selector), vnode) } if (vnode.attrs == null) vnode.attrs = {} vnode.tag = selector return vnode } hyperscript.trust = function(html) { if (html == null) html = "" return Vnode("<", undefined, undefined, html, undefined, undefined) } hyperscript.fragment = function(attrs4, ...children1) { var vnode2 = hyperscriptVnode(attrs4, children1) if (vnode2.attrs == null) vnode2.attrs = {} vnode2.tag = "[" vnode2.children = Vnode.normalizeChildren(vnode2.children) return vnode2 } var delayedRemoval = new WeakMap function *domFor(vnode4) { // To avoid unintended mangling of the internal bundler, // parameter destructuring is not used here. var dom = vnode4.dom var domSize0 = vnode4.domSize var generation0 = delayedRemoval.get(dom) if (dom != null) do { var nextSibling = dom.nextSibling if (delayedRemoval.get(dom) === generation0) { yield dom domSize0-- } dom = nextSibling } while (domSize0) } var _14 = 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 `vnode.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 `vnode` 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 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 attrs5 = vnode3.attrs var is = vnode3.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 (attrs5 != null) { setAttrs(vnode3, attrs5, 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" && attrs5 != null) setLateSelectAttrs(vnode3, attrs5) } } } 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.instance.domSize } else { vnode3.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, vnode3, hooks, nextSibling, ns) { var oldTag = old.tag, tag = vnode3.tag if (oldTag === tag && old.is === vnode3.is) { 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) 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 } } } vnode3.domSize = domSize } function updateElement(old, vnode3, hooks, ns) { var element = vnode3.dom = old.dom ns = getNameSpace(vnode3) || ns if (old.attrs != vnode3.attrs || (vnode3.attrs != null && !cachedAttrsIsStaticMap.get(vnode3.attrs))) { 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.domSize = 0 } } 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 children (removed from vdom, but persisted in DOM through onbeforeremove) function moveDOM(parent, vnode3, nextSibling) { if (vnode3.dom != null) { var target if (vnode3.domSize == null || vnode3.domSize === 1) { // don't allocate for the common case target = vnode3.dom } else { target = getDocument(parent).createDocumentFragment() for (var dom of domFor(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 tryBlockRemove(parent, vnode3, source, counter) { var original = vnode3.state var result = callHook.call(source.onbeforeremove, vnode3) if (result == null) return var generation = currentRender for (var dom of domFor(vnode3)) delayedRemoval.set(dom, generation) counter.v++ Promise.resolve(result).finally(function () { checkState(vnode3, original) tryResumeRemove(parent, vnode3, counter) }) } function tryResumeRemove(parent, vnode3, counter) { if (--counter.v === 0) { onremove(vnode3) removeDOM(parent, vnode3) } } function removeNode(parent, vnode3) { var counter = {v: 1} if (typeof vnode3.tag !== "string" && typeof vnode3.state.onbeforeremove === "function") tryBlockRemove(parent, vnode3, vnode3.state, counter) if (vnode3.attrs && typeof vnode3.attrs.onbeforeremove === "function") tryBlockRemove(parent, vnode3, vnode3.attrs, counter) tryResumeRemove(parent, vnode3, counter) } function removeDOM(parent, vnode3) { if (vnode3.dom == null) return if (vnode3.domSize == null || vnode3.domSize === 1) { parent.removeChild(vnode3.dom) } else { for (var dom of domFor(vnode3)) 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 { if (vnode3.events != null) vnode3.events._ = null 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) } } } } //attrs function setAttrs(vnode3, attrs5, ns) { for (var key in attrs5) { setAttr(vnode3, key, null, attrs5[key], ns) } } function setAttr(vnode3, key, old, value, ns) { if (key === "key" || 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 */ //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 //minlength/maxlength validation isn't performed on script-set values(#2256) if ((vnode3.tag === "input" || vnode3.tag === "textarea") && vnode3.dom.value === "" + value) 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[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 (vnode3.tag === "input" && vnode3.attrs.type === "file" && "" + 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 (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" || 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, attrs5) { if ("value" in attrs5) { if(attrs5.value === null) { if (vnode3.dom.selectedIndex !== -1) vnode3.dom.value = null } else { var normalized = "" + attrs5.value // eslint-disable-line no-implicit-coercion if (vnode3.dom.value !== normalized || vnode3.dom.selectedIndex === -1) { vnode3.dom.value = normalized } } } if ("selectedIndex" in attrs5) setAttr(vnode3, "selectedIndex", null, attrs5.selectedIndex, undefined) } function updateAttrs(vnode3, old, attrs5, ns) { // Some attributes may NOT be case-sensitive (e.g. data-***), // so removal should be done first to prevent accidental removal for newly setting values. var val if (old != null) { if (old === attrs5 && !cachedAttrsIsStaticMap.has(attrs5)) { console.warn("Don't reuse attrs object, use new object for every redraw, this will throw in next major") } for (var key in old) { if (((val = old[key]) != null) && (attrs5 == null || attrs5[key] == null)) { removeAttr(vnode3, key, val, ns) } } } if (attrs5 != null) { for (var key in attrs5) { setAttr(vnode3, key, old && old[key], attrs5[key], 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.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 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 = "" // Add new style properties for (var key in style) { var value = style[key] if (value != null) { if (key.includes("-")) element.style.setProperty(key, String(value)) else element.style[key] = String(value) } } } else { // Both old & new are (different) objects. // Remove style properties that no longer exist // Style properties may have two cases(dash-case and camelCase), // so removal should be done first to prevent accidental removal for newly setting values. for (var key in old) { if (old[key] != null && style[key] == null) { if (key.includes("-")) element.style.removeProperty(key) else element.style[key] = "" } } // Update style properties that have changed for (var key in style) { var value = style[key] if (value != null && (value = String(value)) !== String(old[key])) { if (key.includes("-")) element.style.setProperty(key, value) else element.style[key] = value } } } } // 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) var self = this if (self._ != null) { if (ev.redraw !== false) (0, self._)() if (result != null && typeof result.then === "function") { Promise.resolve(result).then(function () { if (self._ != null && ev.redraw !== false) (0, self._)() }) } } 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 children 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 = _14() var _21 = function(render2, schedule, console) { var subscriptions = [] var pending = false var offset = -1 function sync() { for (offset = 0; offset < subscriptions.length; offset += 2) { try { render2(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 render2(root, []) } if (component != null) { subscriptions.push(root, component) render2(root, Vnode(component), redraw) } } return {mount: mount, redraw: redraw} } var mountRedraw = _21(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 _25 = 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 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 !== 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 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 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 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 } } } } var request = _25(typeof window !== "undefined" ? window : null, mountRedraw.redraw) /* Percent encodings encode UTF-8 bytes, so this regexp needs to match that. Here's how UTF-8 encodes stuff: - `00-7F`: 1-byte, for U+0000-U+007F - `C2-DF 80-BF`: 2-byte, for U+0080-U+07FF - `E0-EF 80-BF 80-BF`: 3-byte, encodes U+0800-U+FFFF - `F0-F4 80-BF 80-BF 80-BF`: 4-byte, encodes U+10000-U+10FFFF In this, there's a number of invalid byte sequences: - `80-BF`: Continuation byte, invalid as start - `C0-C1 80-BF`: Overlong encoding for U+0000-U+007F - `E0 80-9F 80-BF`: Overlong encoding for U+0080-U+07FF - `ED A0-BF 80-BF`: Encoding for UTF-16 surrogate U+D800-U+DFFF - `F0 80-8F 80-BF 80-BF`: Overlong encoding for U+0800-U+FFFF - `F4 90-BF`: RFC 3629 restricted UTF-8 to only code points UTF-16 could encode. - `F5-FF`: RFC 3629 restricted UTF-8 to only code points UTF-16 could encode. So in reality, only the following sequences can encode are valid characters: - 00-7F - C2-DF 80-BF - E0 A0-BF 80-BF - E1-EC 80-BF 80-BF - ED 80-9F 80-BF - EE-EF 80-BF 80-BF - F0 90-BF 80-BF 80-BF - F1-F3 80-BF 80-BF 80-BF - F4 80-8F 80-BF 80-BF The regexp just tries to match this as compactly as possible. */ var validUtf8Encodings = /%(?:[0-7]|(?!c[01]|e0%[89]|ed%[ab]|f0%8|f4%[9ab])(?:c|d|(?:e|f[0-4]%[89ab])[\da-f]%[89ab])[\da-f]%[89ab])[\da-f]/gi var decodeURIComponentSafe = function(str) { return String(str).replace(validUtf8Encodings, decodeURIComponent) } 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 = decodeURIComponentSafe(entry[0]) var value2 = entry.length === 2 ? decodeURIComponentSafe(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 `{path, 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 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. 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 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(m4, key5, extra) { if (key5 == null) return "\\" + m4 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 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 = hasOwn // const magic = [ // "key", "oninit", "oncreate", "onbeforeupdate", "onupdate", // "onbeforeremove", "onremove", // ] // var censor = (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 magic = /^(?:key|oninit|oncreate|onbeforeupdate|onupdate|onbeforeremove|onremove)$/ var censor = function(attrs7, extras) { var result2 = {} if (extras != null) { for (var key6 in attrs7) { if (hasOwn.call(attrs7, key6) && !magic.test(key6) && extras.indexOf(key6) < 0) { result2[key6] = attrs7[key6] } } } else { for (var key6 in attrs7) { if (hasOwn.call(attrs7, key6) && !magic.test(key6)) { result2[key6] = attrs7[key6] } } } return result2 } var _31 = function($window, mountRedraw0) { var p = Promise.resolve() var scheduled = false var ready = false var hasBeenResolved = false var dom0, compiled, fallbackRoute var currentResolver, component, attrs6, currentPath, lastUpdate var RouterRoot = { onremove: function() { ready = hasBeenResolved = false $window.removeEventListener("popstate", fireAsync, false) }, view: function() { // The route has already been resolved. // Therefore, the following early return is not needed. // if (!hasBeenResolved) return var vnode6 = Vnode(component, attrs6.key, attrs6) if (currentResolver) return currentResolver.render(vnode6) // Wrap in a fragment to preserve existing key semantics 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 } } var path0 = decodeURIComponentSafe(prefix).slice(route.prefix.length) var data = parsePathname(path0) Object.assign(data.params, $window.history.state) function reject(e) { console.error(e) route.set(fallbackRoute, null, {replace: true}) } loop(0) function loop(i) { 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" attrs6 = data.params, currentPath = path0, lastUpdate = null currentResolver = payload.render ? payload : null if (hasBeenResolved) mountRedraw0.redraw() else { hasBeenResolved = true mountRedraw0.mount(dom0, RouterRoot) } } // 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 + ".") } route.set(fallbackRoute, null, {replace: true}) } } 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. setTimeout(resolveRoute) } } 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.") } } dom0 = root $window.addEventListener("popstate", fireAsync, false) ready = true // The RouterRoot component is mounted when the route is first resolved. resolveRoute() } route.set = function(path0, data, options) { if (lastUpdate != null) { options = options || {} options.replace = true } lastUpdate = null 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 } } route.get = function() {return currentPath} route.prefix = "#!" route.Link = { view: function(vnode6) { // 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 child0 = hyperscript( 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 is 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-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 result1 !== 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 child0 }, } route.param = function(key3) { return attrs6 && key3 != null ? attrs6[key3] : attrs6 } return route } var router = _31(typeof window !== "undefined" ? window : null, mountRedraw) 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 = router 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 if (typeof module !== "undefined") module["exports"] = m else window.m = m }()); ================================================ FILE: mount-redraw.js ================================================ "use strict" var render = require("./render") module.exports = require("./api/mount-redraw")(render, typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : null, typeof console !== "undefined" ? console : null) ================================================ FILE: mount.js ================================================ "use strict" module.exports = require("./mount-redraw").mount ================================================ FILE: package.json ================================================ { "name": "mithril", "version": "2.3.8", "description": "A framework for building brilliant applications", "author": "Leo Horie", "license": "MIT", "unpkg": "mithril.min.js", "jsdelivr": "mithril.min.js", "repository": "github:MithrilJS/mithril.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", "pretest": "npm run lint", "test": "run-s test:js", "test:js": "ospec" }, "devDependencies": { "benchmark": "^2.1.4", "chokidar": "^4.0.1", "eslint": "^8.9.0", "glob": "^13.0.0", "npm-run-all": "^4.1.5", "ospec": "4.2.1", "rimraf": "^6.0.1", "terser": "^5.7.2" } } ================================================ FILE: pathname/build.js ================================================ "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 } ================================================ FILE: pathname/compileTemplate.js ================================================ "use strict" var parsePathname = require("./parse") var decodeURIComponentSafe = require("../util/decodeURIComponentSafe") // 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] : decodeURIComponentSafe(values[i + 1]) } return true } } ================================================ FILE: pathname/parse.js ================================================ "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)), } } ================================================ FILE: pathname/tests/test-buildPathname.js ================================================ "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://") }) }) ================================================ FILE: pathname/tests/test-compileTemplate.js ================================================ "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"}) }) }) ================================================ FILE: pathname/tests/test-parsePathname.js ================================================ "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: {} }) }) }) ================================================ FILE: performance/index.html ================================================ ================================================ FILE: performance/test-perf.js ================================================ "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 */ Benchmark = require("benchmark") 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.m = require("../index.js") global.rootElem = null /* eslint-enable global-require */ } function cycleRoot() { if (rootElem) document.body.removeChild(rootElem) document.body.appendChild(rootElem = document.createElement("div")) } // Initialize benchmark suite Benchmark.options.async = true Benchmark.options.initCount = 10 Benchmark.options.minSamples = 40 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 */ } var suite = new Benchmark.Suite("Mithril.js perf", { onStart: function () { this.start = Date.now() }, onCycle: function (e) { console.log(e.target.toString()) cycleRoot() }, onComplete: function () { console.log("Completed perf tests in " + (Date.now() - this.start) + "ms") }, onError: function (e) { console.error(e) }, }) // 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)) } }, 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) }, }) 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" ) ) ) ) )) }, }) 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") ) ) } } 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) ) } } 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" ) ) } } var NestedButton = { view: function (vnode) { return m("button", m.censor(vnode.attrs), vnode.children) } } var NestedMain = { view: function () { return m(NestedForm) } } this.NestedRoot = { view: function () { return m("div.foo.bar[data-foo=bar]", {p: 2}, m(NestedHeader), m(NestedMain) ) } } }, fn: function () { m.render(rootElem, m(this.NestedRoot)) }, }) 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 -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 } ================================================ FILE: querystring/tests/test-buildQueryString.js ================================================ "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") }) }) ================================================ FILE: querystring/tests/test-parseQueryString.js ================================================ "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") // decodes "%c5%a1" only o(data).deepEquals({test: "š%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"}}) }) }) ================================================ FILE: redraw.js ================================================ "use strict" module.exports = require("./mount-redraw").redraw ================================================ FILE: render/cachedAttrsIsStaticMap.js ================================================ "use strict" var emptyAttrs = require("./emptyAttrs") // This Map manages the following: // - Whether an attrs is cached attrs generated by compileSelector(). // - Whether the cached attrs is "static", i.e., does not contain any form attributes. // These information will be useful to skip updating attrs in render(). // // Since the attrs used as keys in this map are not released from the selectorCache object, // there is no risk of memory leaks. Therefore, Map is used here instead of WeakMap. module.exports = new Map([[emptyAttrs, true]]) ================================================ FILE: render/delayedRemoval.js ================================================ "use strict" module.exports = new WeakMap ================================================ FILE: render/domFor.js ================================================ "use strict" var delayedRemoval = require("./delayedRemoval") function *domFor(vnode) { // To avoid unintended mangling of the internal bundler, // parameter destructuring is not used here. var dom = vnode.dom var domSize = vnode.domSize var generation = delayedRemoval.get(dom) if (dom != null) do { var nextSibling = dom.nextSibling if (delayedRemoval.get(dom) === generation) { yield dom domSize-- } dom = nextSibling } while (domSize) } module.exports = domFor ================================================ FILE: render/emptyAttrs.js ================================================ "use strict" // This is an attrs object that is used by default when attrs is undefined or null. module.exports = {} ================================================ FILE: render/fragment.js ================================================ "use strict" var Vnode = require("../render/vnode") var hyperscriptVnode = require("./hyperscriptVnode") module.exports = function(attrs, ...children) { var vnode = hyperscriptVnode(attrs, children) if (vnode.attrs == null) vnode.attrs = {} vnode.tag = "[" vnode.children = Vnode.normalizeChildren(vnode.children) return vnode } ================================================ FILE: render/hyperscript.js ================================================ "use strict" var Vnode = require("../render/vnode") var hyperscriptVnode = require("./hyperscriptVnode") var hasOwn = require("../util/hasOwn") var emptyAttrs = require("./emptyAttrs") var cachedAttrsIsStaticMap = require("./cachedAttrsIsStaticMap") 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 isFormAttributeKey(key) { return key === "value" || key === "checked" || key === "selectedIndex" || key === "selected" } function compileSelector(selector) { var match, tag = "div", classes = [], attrs = {}, isStatic = true 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 (isFormAttributeKey(match[4])) isStatic = false } } } if (classes.length > 0) attrs.className = classes.join(" ") if (isEmpty(attrs)) attrs = emptyAttrs else cachedAttrsIsStaticMap.set(attrs, isStatic) return selectorCache[selector] = {tag: tag, attrs: attrs, is: attrs.is} } function execSelector(state, vnode) { vnode.tag = state.tag var attrs = vnode.attrs if (attrs == null) { vnode.attrs = state.attrs vnode.is = state.is return vnode } if (hasOwn.call(attrs, "class")) { if (attrs.class != null) attrs.className = attrs.class attrs.class = null } if (state.attrs !== emptyAttrs) { var className = attrs.className attrs = Object.assign({}, state.attrs, attrs) if (state.attrs.className != null) attrs.className = className != null ? String(state.attrs.className) + " " + String(className) : state.attrs.className } // 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) } // This reduces the complexity of the evaluation of "is" within the render function. vnode.is = attrs.is vnode.attrs = attrs return vnode } function hyperscript(selector, attrs, ...children) { if (selector == null || typeof selector !== "string" && typeof selector !== "function" && typeof selector.view !== "function") { throw Error("The selector must be either a string or a component."); } var vnode = hyperscriptVnode(attrs, children) if (typeof selector === "string") { vnode.children = Vnode.normalizeChildren(vnode.children) if (selector !== "[") return execSelector(selectorCache[selector] || compileSelector(selector), vnode) } if (vnode.attrs == null) vnode.attrs = {} vnode.tag = selector return vnode } module.exports = hyperscript ================================================ FILE: render/hyperscriptVnode.js ================================================ "use strict" var Vnode = require("../render/vnode") // Note: the processing of variadic parameters is perf-sensitive. // // In native ES6, it might be preferable to define hyperscript and fragment // factories with a final ...args parameter and call hyperscriptVnode(...args), // since modern engines can optimize spread calls. // // However, benchmarks showed this was not faster. As a result, spread is used // only in the parameter lists of hyperscript and fragment, while an array is // passed to hyperscriptVnode. 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 } return Vnode("", attrs && attrs.key, attrs, children) } ================================================ FILE: render/render.js ================================================ "use strict" var Vnode = require("./vnode") var delayedRemoval = require("./delayedRemoval") var domFor = require("./domFor") var cachedAttrsIsStaticMap = require("./cachedAttrsIsStaticMap") 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 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 = vnode.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.instance.domSize } 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, o, v if (isOldKeyed !== isKeyed) { removeNodes(parent, old, 0, old.length) createNodes(parent, vnodes, 0, 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. while (oldStart < old.length && old[oldStart] == null) oldStart++ while (start < vnodes.length && vnodes[start] == null) start++ 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, old.length, nextSibling)) else if (v == null) removeNode(parent, o) else updateNode(parent, o, v, hooks, getNextSibling(old, start + 1, old.length, 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, 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, oldEnd + 1, 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, oldEnd, 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, pos = 2147483647, matched = 0 var oldIndices = new Array(end - start + 1).fill(-1) var map = Object.create(null) for (var i = start; i <= end; i++) map[vnodes[i].key] = i for (var i = oldEnd; i >= oldStart; i--) { oe = old[i] var newIndex = map[oe.key] if (newIndex != null) { pos = (newIndex < pos) ? newIndex : -1 // becomes -1 if nodes were re-ordered oldIndices[newIndex-start] = i ve = vnodes[newIndex] old[i] = 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 var lisIndices = makeLisIndices(oldIndices) var li = lisIndices.length - 1 for (var i = end; i >= start; i--) { ve = vnodes[i] if (oldIndices[i-start] === -1) createNode(parent, ve, hooks, ns, nextSibling) else { if (lisIndices[li] === i - start) li-- else moveDOM(parent, ve, nextSibling) } if (ve.dom != null) nextSibling = ve.dom } } else { for (var i = end; i >= start; i--) { ve = vnodes[i] if (oldIndices[i-start] === -1) createNode(parent, ve, hooks, ns, nextSibling) if (ve.dom != null) nextSibling = ve.dom } } } } } } } function updateNode(parent, old, vnode, hooks, nextSibling, ns) { var oldTag = old.tag, tag = vnode.tag if (oldTag === tag && old.is === vnode.is) { 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) 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 } } } vnode.domSize = domSize } function updateElement(old, vnode, hooks, ns) { var element = vnode.dom = old.dom ns = getNameSpace(vnode) || ns if (old.attrs != vnode.attrs || (vnode.attrs != null && !cachedAttrsIsStaticMap.get(vnode.attrs))) { 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.domSize = 0 } } // 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, end, nextSibling) { for (; i < end; 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 || vnode.domSize === 1) { // 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 tryBlockRemove(parent, vnode, source, counter) { var original = vnode.state var result = callHook.call(source.onbeforeremove, vnode) if (result == null) return var generation = currentRender for (var dom of domFor(vnode)) delayedRemoval.set(dom, generation) counter.v++ Promise.resolve(result).finally(function () { checkState(vnode, original) tryResumeRemove(parent, vnode, counter) }) } function tryResumeRemove(parent, vnode, counter) { if (--counter.v === 0) { onremove(vnode) removeDOM(parent, vnode) } } function removeNode(parent, vnode) { var counter = {v: 1} if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeremove === "function") tryBlockRemove(parent, vnode, vnode.state, counter) if (vnode.attrs && typeof vnode.attrs.onbeforeremove === "function") tryBlockRemove(parent, vnode, vnode.attrs, counter) tryResumeRemove(parent, vnode, counter) } function removeDOM(parent, vnode) { if (vnode.dom == null) return if (vnode.domSize == null || vnode.domSize === 1) { parent.removeChild(vnode.dom) } else { for (var dom of domFor(vnode)) 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 { if (vnode.events != null) vnode.events._ = null 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" || 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 */ //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 //minlength/maxlength validation isn't performed on script-set values(#2256) if ((vnode.tag === "input" || vnode.tag === "textarea") && vnode.dom.value === "" + value) 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 (vnode.tag === "input" && vnode.attrs.type === "file" && "" + 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" || 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) { // Some attributes may NOT be case-sensitive (e.g. data-***), // so removal should be done first to prevent accidental removal for newly setting values. var val if (old != null) { if (old === attrs && !cachedAttrsIsStaticMap.has(attrs)) { console.warn("Don't reuse attrs object, use new object for every redraw, this will throw in next major") } for (var key in old) { if (((val = old[key]) != null) && (attrs == null || attrs[key] == null)) { removeAttr(vnode, key, val, ns) } } } if (attrs != null) { for (var key in attrs) { setAttr(vnode, key, old && old[key], attrs[key], 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.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 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 = "" // Add new style properties for (var key in style) { var value = style[key] if (value != null) { if (key.includes("-")) element.style.setProperty(key, String(value)) else element.style[key] = String(value) } } } else { // Both old & new are (different) objects. // Remove style properties that no longer exist // Style properties may have two cases(dash-case and camelCase), // so removal should be done first to prevent accidental removal for newly setting values. for (var key in old) { if (old[key] != null && style[key] == null) { if (key.includes("-")) element.style.removeProperty(key) else element.style[key] = "" } } // Update style properties that have changed for (var key in style) { var value = style[key] if (value != null && (value = String(value)) !== String(old[key])) { if (key.includes("-")) element.style.setProperty(key, value) else element.style[key] = value } } } } // 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) var self = this if (self._ != null) { if (ev.redraw !== false) (0, self._)() if (result != null && typeof result.then === "function") { Promise.resolve(result).then(function () { if (self._ != null && ev.redraw !== false) (0, self._)() }) } } 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 } } } ================================================ FILE: render/tests/.eslintrc.js ================================================ "use strict" module.exports = { "extends": "../../.eslintrc.js", "env": { "browser": null, "node": true, "es2022": true, }, "parserOptions": { "ecmaVersion": 2022, }, "rules": { "no-process-env": "off", }, }; ================================================ FILE: render/tests/manual/case-handling.html ================================================

This is a test for special case-handling of attribute and style properties. (#2988).

Open your browser's Developer Console and follow these steps:

  1. Check the background color of the "foo" below.
    • If it is light green, it is correct. The style has been updated properly.
    • If it is red or yellow, the style has not been updated properly.
  2. Check the logs displayed in the console.
    • If the attribute has been updated correctly, you should see the following message: "If you see this message, the update process is correct."
    • If "null" is displayed, the attribute has not been updated properly.
================================================ FILE: render/tests/manual/iframe.html ================================================
================================================ FILE: render/tests/manual/index.html ================================================ Various parent website content. There should be a clickable button below, which is inside an iframe containing a Mithril.js app:
================================================ FILE: render/tests/manual/minlength-input.html ================================================ This is minlength validity test (#2256). Open your browser's Developer Console and follow these steps:
  1. Type any (1 or 2) characters in the input field.
  2. Click “submit.”
  3. Click “submit” again.
  4. Check the logs displayed in the console.
================================================ FILE: render/tests/manual/minlength-textarea.html ================================================ This is minlength validity test (#2256). Open your browser's Developer Console and follow these steps:
  1. Type any (1 or 2) characters in the textarea field.
  2. Click “submit.”
  3. Click “submit” again.
  4. Check the logs displayed in the console.
================================================ FILE: render/tests/test-attributes.js ================================================ "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("attributes", function() { var $window, root, render o.beforeEach(function() { $window = domMock() root = $window.document.body render = vdom($window) }) o.spec("basics", function() { o("works (create/update/remove)", function() { var a = m("div") var b = m("div", {id: "test"}) var c = m("div") render(root, a); o(a.dom.hasAttribute("id")).equals(false) render(root, b); o(b.dom.getAttribute("id")).equals("test") render(root, c); o(c.dom.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); o(a.dom.hasAttribute("id")).equals(false) render(root, b); o(b.dom.hasAttribute("id")).equals(true) o(b.dom.getAttribute("id")).equals("test") // #1804 render(root, c); o(c.dom.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 spies = [] $window.document.createElement = function(tag, is){ var el = f(tag, is) var spy = o.spy(el.setAttribute) el.setAttribute = spy spies.push(spy) spy.elem = el return el } render(root, [ m("input", {value: "hello"}), m("input", {value: "hello"}), m("input", {value: "hello"}), m("custom-element", {custom: "x"}), m("input", {is: "something-special", custom: "x"}), m("custom-element", {is: "something-special", custom: "x"}) ]) o(spies[1].callCount).equals(0) 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: ["is", "something-special"]}, {this: spies[4].elem, args: ["custom", "x"]}]) o(spies[5].calls).deepEquals([{this: spies[5].elem, args: ["is", "something-special"]}, {this: spies[5].elem, args: ["custom", "x"]}]) }) o("when vnode is customElement with property, custom setAttribute not called", function(){ var f = $window.document.createElement var spies = [] var getters = [] var setters = [] $window.document.createElement = function(tag, is){ var el = f(tag, is) var spy = o.spy(el.setAttribute) el.setAttribute = spy spies.push(spy) spy.elem = el if (tag === "custom-element" || is && is.is === "something-special") { var custom = "foo" var getter, setter Object.defineProperty(el, "custom", { configurable: true, enumerable: true, get: getter = o.spy(function () { return custom }), set: setter = o.spy(function (value) { custom = value }) }) getters.push(getter) setters.push(setter) } return el } render(root, [ m("input", {value: "hello"}), m("input", {value: "hello"}), m("input", {value: "hello"}), m("custom-element", {custom: "x"}), m("input", {is: "something-special", custom: "x"}), m("custom-element", {is: "something-special", custom: "x"}) ]) o(spies[0].callCount).equals(0) o(spies[1].callCount).equals(0) o(spies[2].callCount).equals(0) o(spies[3].callCount).equals(0) o(spies[4].callCount).equals(1) // setAttribute("is", "something-special") is called o(spies[5].callCount).equals(1) // setAttribute("is", "something-special") is called o(getters[0].callCount).equals(0) o(getters[1].callCount).equals(0) o(getters[2].callCount).equals(0) o(setters[0].calls).deepEquals([{this: spies[3].elem, args: ["x"]}]) o(setters[1].calls).deepEquals([{this: spies[4].elem, args: ["x"]}]) o(setters[2].calls).deepEquals([{this: spies[5].elem, args: ["x"]}]) }) }) o.spec("input readonly", function() { o("when input readonly is true, attribute is present", function() { var a = m("input", {readonly: true}) render(root, a) o(a.dom.attributes["readonly"].value).equals("") }) o("when input readonly is false, attribute is not present", function() { var a = m("input", {readonly: false}) render(root, a) o(a.dom.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) o(a.dom.checked).equals(true) o(a.dom.attributes["checked"]).equals(undefined) }) o("when input checked is false, attribute is not present", function() { var a = m("input", {checked: false}) render(root, a) o(a.dom.checked).equals(false) o(a.dom.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) a.dom.checked = true //setting the javascript property makes the value no longer track the state of the attribute a.dom.checked = false render(root, b) o(a.dom.checked).equals(true) o(a.dom.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); o(a.dom.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) o(a.dom.value).equals("") render(root, b) o(a.dom.value).equals("test") // https://github.com/MithrilJS/mithril.js/issues/1804#issuecomment-304521235 render(root, c) o(a.dom.value).equals("") }) o("can be set as number", function() { var a = m("input", {value: 1}) render(root, a); o(a.dom.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); o(a.dom.value).equals("") o(a.dom.getAttribute("value")).equals(null) render(root, b); o(b.dom.value).equals("test") o(b.dom.getAttribute("value")).equals(null) render(root, c); o(c.dom.value).equals("") o(c.dom.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); o(a.dom.value).equals("0") render(root, b); o(b.dom.value).equals("") // #1595 redux render(root, c); o(c.dom.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) var a =m("input") var b = m("input", {value: "1"}) var c = m("input", {value: "1"}) var d = m("input", {value: 1}) var e = m("input", {value: 2}) render(root, a) var spies = $window.__getSpies(a.dom) a.dom.focus() o(spies.valueSetter.callCount).equals(0) render(root, b) o(b.dom.value).equals("1") o(spies.valueSetter.callCount).equals(1) render(root, c) o(c.dom.value).equals("1") o(spies.valueSetter.callCount).equals(1) render(root, d) o(d.dom.value).equals("1") o(spies.valueSetter.callCount).equals(1) render(root, e) o(d.dom.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) 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) o(spies.typeSetter.callCount).equals(0) o(a.dom.getAttribute("type")).equals("radio") render(root, b) o(spies.typeSetter.callCount).equals(0) o(b.dom.getAttribute("type")).equals("text") render(root, c) o(spies.typeSetter.callCount).equals(0) o(c.dom.hasAttribute("type")).equals(false) }) }) o.spec("textarea.value", function() { o("can be removed by not passing a value", function() { var a = m("textarea", {value:"x"}) var b = m("textarea") render(root, a) o(a.dom.value).equals("x") // https://github.com/MithrilJS/mithril.js/issues/1804#issuecomment-304521235 render(root, b) o(b.dom.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) var a = m("textarea") var b = m("textarea", {value: "1"}) var c = m("textarea", {value: "1"}) var d = m("textarea", {value: 1}) var e = m("textarea", {value: 2}) render(root, a) var spies = $window.__getSpies(a.dom) a.dom.focus() o(spies.valueSetter.callCount).equals(0) render(root, b) o(b.dom.value).equals("1") o(spies.valueSetter.callCount).equals(1) render(root, c) o(c.dom.value).equals("1") o(spies.valueSetter.callCount).equals(1) render(root, d) o(d.dom.value).equals("1") o(spies.valueSetter.callCount).equals(1) render(root, e) o(d.dom.value).equals("2") o(spies.valueSetter.callCount).equals(2) }) }) o.spec("link href", function() { o("when link href is true, attribute is present", function() { var a = m("a", {href: true}) render(root, a) o(a.dom.attributes["href"]).notEquals(undefined) }) o("when link href is false, attribute is not present", function() { var a = m("a", {href: false}) render(root, a) o(a.dom.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) o(canvas.dom.attributes["width"].value).equals("100%") o(canvas.dom.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); o(a.dom.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) o(vnode.dom.nodeName).equals("svg") o(vnode.dom.firstChild.attributes["href"].value).equals("javascript:;") o(vnode.dom.firstChild.attributes["href"].namespaceURI).equals("http://www.w3.org/1999/xlink") vnode = m("svg", {ns: "http://www.w3.org/2000/svg"}, m("a", {ns: "http://www.w3.org/2000/svg"}) ) render(root, vnode) o(vnode.dom.nodeName).equals("svg") o("href" in vnode.dom.firstChild.attributes).equals(false) }) /* eslint-enable no-script-url */ }) o.spec("option.value", function() { o("can be set as text", function() { var a = m("option", {value: "test"}) render(root, a); o(a.dom.value).equals("test") }) o("can be set as number", function() { var a = m("option", {value: 1}) render(root, a); o(a.dom.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); o(a.dom.value).equals("") o(a.dom.hasAttribute("value")).equals(false) render(root, b); o(b.dom.value).equals("test") o(b.dom.getAttribute("value")).equals("test") render(root, c); o(c.dom.value).equals("") o(c.dom.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); o(a.dom.value).equals("0") render(root, b); o(a.dom.value).equals("") // #1595 redux render(root, c); o(c.dom.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) var a = m("option") var b = m("option", {value: "1"}) var c = m("option", {value: "1"}) var d = m("option", {value: 1}) var e = m("option", {value: 2}) render(root, a) var spies = $window.__getSpies(a.dom) o(spies.valueSetter.callCount).equals(0) render(root, b) o(b.dom.value).equals("1") o(spies.valueSetter.callCount).equals(1) render(root, c) o(c.dom.value).equals("1") o(spies.valueSetter.callCount).equals(1) render(root, d) o(d.dom.value).equals("1") o(spies.valueSetter.callCount).equals(1) render(root, e) o(d.dom.value).equals("2") o(spies.valueSetter.callCount).equals(2) }) }) o.spec("select.value", function() { function makeSelect(value) { var attrs = (arguments.length === 0) ? {} : {value: value} return m("select", attrs, m("option", {value: "1"}), m("option", {value: "2"}), m("option", {value: "a"}), m("option", {value: "0"}), m("option", {value: ""}) ) } o("render select options with `selected` (#1916)", function() { var select1 = m("select", [m("option"), m("option")]) var select2 = m("select", [m("option", {selected: false}), m("option", {selected: true})]) var select3 = m("select", [m("option", {selected: false}), m("option", {selected: true})]) var select4 = m("select", [m("option", {selected: false}), m("option", {selected: true})]) var select5 = m("select", [m("option", {selected: true}), m("option", {selected: false})]) var select6 = m("select", [m("option", {selected: true}), m("option", {selected: false})]) var select7 = m("select", [m("option", {selected: true}), m("option", {selected: false})]) // selected: [undefined,undefined] // DomMock can't set/read `option.selected` when the option doesn't have a `select` parent, // so call render() without `option.selected` first. render(root, select1) var el = root.firstChild o(el.selectedIndex).equals(0) o(el.childNodes[0].selected).equals(true) o(el.childNodes[1].selected).equals(false) // selected: [undefined,undefined] -> [false,true] (changed -> update by render) render(root, select2) o(el.selectedIndex).equals(1) o(el.childNodes[0].selected).equals(false) o(el.childNodes[1].selected).equals(true) // selected: [false,true] -> [false,true] (unchanged, not focused -> not update by render) el.selectedIndex = 0 // set 0 without render render(root, select3) o(el.selectedIndex).equals(0) // unchanged o(el.childNodes[0].selected).equals(true) o(el.childNodes[1].selected).equals(false) // selected: [false,true] -> [false,true] (unchanged, focused -> update by render) el.focus() render(root, select4) o(el.selectedIndex).equals(1) o(el.childNodes[0].selected).equals(false) o(el.childNodes[1].selected).equals(true) // selected: [false,true] -> [true,false] (changed -> update by render) render(root, select5) o(el.selectedIndex).equals(0) o(el.childNodes[0].selected).equals(true) o(el.childNodes[1].selected).equals(false) // selected: [true,false] -> [true,false] (unchanged, not focused -> not update by render) el.selectedIndex = 1 // set 1 without render root.focus() render(root, select6) o(el.selectedIndex).equals(1) // unchanged o(el.childNodes[0].selected).equals(false) o(el.childNodes[1].selected).equals(true) // selected: [true,false] -> [true,false] (unchanged, focused -> update by render) el.focus() render(root, select7) o(el.selectedIndex).equals(0) o(el.childNodes[0].selected).equals(true) o(el.childNodes[1].selected).equals(false) }) o("can be set as text", function() { var a = makeSelect() var b = makeSelect("2") var c = makeSelect("a") 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) render(root, c) o(c.dom.value).equals("a") o(c.dom.selectedIndex).equals(2) }) o("setting null unsets the value", function() { var a = makeSelect(null) render(root, a) o(a.dom.value).equals("") o(a.dom.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 try { render(root, div) succeeded = true } catch(e){/* ignore */} o(succeeded).equals(true) }) }) o.spec("mutate attr object", function() { o("warn when reusing attrs object", function() { const _consoleWarn = console.warn console.warn = o.spy() const attrs = {className: "on"} render(root, {tag: "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 }) }) }) ================================================ FILE: render/tests/test-component.js ================================================ "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("vnode.domSize (vnode.instance == null)", function() { o("create and update", function() { var component = createComponent({ view: function() {return null} }) // create var v1 = m(component) render(root, v1) o(v1.domSize).equals(0) o(v1.instance).equals(null) // update var v2 = m(component) render(root, v2) o(v2.domSize).equals(0) o(v2.instance).equals(null) }) o("remove instance", function() { var v var component = createComponent({ view: function() {return v} }) // create (return element vnode) v = m("a") var v1 = m(component) render(root, v1) o(v1.instance.tag).equals("a") o(v1.domSize).equals(undefined) o(v1.instance.domSize).equals(undefined) // remove instance (return null) v = null var v2 = m(component) render(root, v2) o(v2.domSize).equals(0) o(v2.instance).equals(null) }) o("nested component", function() { var childComponent = createComponent({ view: function() {return null} }) var parentComponent = createComponent({ view: function() {return m(childComponent)} }) // create var v1 = m(parentComponent) render(root, v1) o(v1.domSize).equals(0) o(v1.instance.domSize).equals(0) o(v1.instance.instance).equals(null) // update var v2 = m(parentComponent) render(root, v2) o(v2.domSize).equals(0) o(v2.instance.domSize).equals(0) o(v2.instance.instance).equals(null) }) }) o.spec("vnode.domSize equals vnode.instance.domSize (vnode.instance != null)", function() { o("text", function() { var component = createComponent({ view: function() {return "text"} }) // create var v1 = m(component) render(root, v1) o(v1.instance.tag).equals("#") o(v1.domSize).equals(undefined) o(v1.instance.domSize).equals(undefined) o(v1.dom).equals(v1.instance.dom) // update var v2 = m(component) render(root, v2) o(v2.instance.tag).equals("#") o(v2.domSize).equals(undefined) o(v2.instance.domSize).equals(undefined) o(v2.dom).equals(v2.instance.dom) }) o("element", function() { var component = createComponent({ view: function() {return m("a")} }) // create var v1 = m(component) render(root, v1) o(v1.instance.tag).equals("a") o(v1.domSize).equals(undefined) o(v1.instance.domSize).equals(undefined) o(v1.dom).equals(v1.instance.dom) // update var v2 = m(component) render(root, v2) o(v2.instance.tag).equals("a") o(v2.domSize).equals(undefined) o(v2.instance.domSize).equals(undefined) o(v2.dom).equals(v2.instance.dom) }) o("trust(0)", function() { var component = createComponent({ view: function() {return m.trust("")} }) // create var v1 = m(component) render(root, v1) o(v1.instance.tag).equals("<") o(v1.domSize).equals(0) o(v1.instance.domSize).equals(0) o(v1.dom).equals(v1.instance.dom) // update var v2 = m(component) render(root, v2) o(v2.instance.tag).equals("<") o(v2.domSize).equals(0) o(v2.instance.domSize).equals(0) o(v2.dom).equals(v2.instance.dom) }) o("trust(1)", function() { var component = createComponent({ view: function() {return m.trust("")} }) // create var v1 = m(component) render(root, v1) o(v1.instance.tag).equals("<") o(v1.domSize).equals(1) o(v1.instance.domSize).equals(1) o(v1.dom).equals(v1.instance.dom) // update var v2 = m(component) render(root, v2) o(v2.instance.tag).equals("<") o(v2.domSize).equals(1) o(v2.instance.domSize).equals(1) o(v2.dom).equals(v2.instance.dom) }) o("trust(2)", function() { var component = createComponent({ view: function() {return m.trust("")} }) // create var v1 = m(component) render(root, v1) o(v1.instance.tag).equals("<") o(v1.domSize).equals(2) o(v1.instance.domSize).equals(2) o(v1.dom).equals(v1.instance.dom) // update var v2 = m(component) render(root, v2) o(v2.instance.tag).equals("<") o(v2.domSize).equals(2) o(v2.instance.domSize).equals(2) o(v2.dom).equals(v2.instance.dom) }) o("fragment(0)", function() { var component = createComponent({ view: function() {return []} }) // create var v1 = m(component) render(root, v1) o(v1.instance.tag).equals("[") o(v1.domSize).equals(0) o(v1.instance.domSize).equals(0) o(v1.dom).equals(v1.instance.dom) // update var v2 = m(component) render(root, v2) o(v2.instance.tag).equals("[") o(v2.domSize).equals(0) o(v2.instance.domSize).equals(0) o(v2.dom).equals(v2.instance.dom) }) o("fragment(1)", function() { var component = createComponent({ view: function() {return [m("a")]} }) // create var v1 = m(component) render(root, v1) o(v1.instance.tag).equals("[") o(v1.domSize).equals(1) o(v1.instance.domSize).equals(1) o(v1.dom).equals(v1.instance.dom) // update var v2 = m(component) render(root, v2) o(v2.instance.tag).equals("[") o(v2.domSize).equals(1) o(v2.instance.domSize).equals(1) o(v2.dom).equals(v2.instance.dom) }) o("fragment(2)", function() { var component = createComponent({ view: function() {return [m("a"), m("b")]} }) // create var v1 = m(component) render(root, v1) o(v1.instance.tag).equals("[") o(v1.domSize).equals(2) o(v1.instance.domSize).equals(2) o(v1.dom).equals(v1.instance.dom) // update var v2 = m(component) render(root, v2) o(v2.instance.tag).equals("[") o(v2.domSize).equals(2) o(v2.instance.domSize).equals(2) o(v2.dom).equals(v2.instance.dom) }) o("nested component", function() { var childComponent = createComponent({ view: function() {return [m("a"), m("b")]} }) var parentComponent = createComponent({ view: function() {return m(childComponent)} }) // create var v1 = m(parentComponent) render(root, v1) o(v1.instance.instance.tag).equals("[") o(v1.domSize).equals(2) o(v1.instance.domSize).equals(2) o(v1.instance.instance.domSize).equals(2) o(v1.dom).equals(v1.instance.dom) o(v1.instance.dom).equals(v1.instance.instance.dom) // update var v2 = m(parentComponent) render(root, v2) o(v2.instance.instance.tag).equals("[") o(v2.domSize).equals(2) o(v2.instance.domSize).equals(2) o(v2.instance.instance.domSize).equals(2) o(v2.dom).equals(v2.instance.dom) o(v2.instance.dom).equals(v2.instance.instance.dom) }) }) }) }) 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) }) }) }) }) ================================================ FILE: render/tests/test-createElement.js ================================================ "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("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") }) }) ================================================ FILE: render/tests/test-createFragment.js ================================================ "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(vnode.domSize).equals(1) }) 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") o(vnode.domSize).equals(1) }) }) ================================================ FILE: render/tests/test-createHTML.js ================================================ "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(vnode.domSize).equals(1) }) o("creates text HTML", function() { var vnode = trust("a") render(root, vnode) o(vnode.dom.nodeValue).equals("a") o(vnode.domSize).equals(1) }) 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(vnode.domSize).equals(1) }) o("creates text SVG", function() { var vnode = trust("a") render(root, m("svg", vnode)) o(vnode.dom.nodeValue).equals("a") o(vnode.domSize).equals(1) }) 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"]) }) }) ================================================ FILE: render/tests/test-createNodes.js ================================================ "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") }) }) ================================================ FILE: render/tests/test-createText.js ================================================ "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("™") }) }) ================================================ FILE: render/tests/test-domFor.js ================================================ "use strict" const o = require("ospec") const callAsync = require("../../test-utils/callAsync") const components = require("../../test-utils/components") const domMock = require("../../test-utils/domMock") const vdom = require("../../render/render") const m = require("../../render/hyperscript") const fragment = require("../../render/fragment") const domFor = require("../../render/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"), ] )) }) o("works in onbeforeremove and onremove", function (done) { const onbeforeremove = o.spy(function onbeforeremove(vnode){ o(root.childNodes.length).equals(1) o(root.childNodes[0].nodeName).equals("A") const iter = domFor(vnode) o(iter.next()).deepEquals({done:false, value: root.childNodes[0]}) o(iter.next().done).deepEquals(true) o(root.childNodes.length).equals(1) return {then(resolve){resolve()}} }) const onremove = o.spy(function onremove(vnode){ o(root.childNodes.length).equals(1) o(root.childNodes[0].nodeName).equals("A") const iter = domFor(vnode) o(iter.next()).deepEquals({done:false, value: root.childNodes[0]}) o(iter.next().done).deepEquals(true) o(root.childNodes.length).equals(1) }) render(root, [m("a", {onbeforeremove, onremove})]) render(root, []) o(onbeforeremove.callCount).equals(1) o(onremove.callCount).equals(0) callAsync(function(){ o(onremove.callCount).equals(1) done() }) }) o("works multiple vnodes with onbeforeremove (#3007, 1/6, BCA)", function (done) { let thenCBA, thenCBB, thenCBC const onbeforeremoveA = o.spy(function onbeforeremove(){ return {then(resolve){thenCBA = resolve}} }) const onbeforeremoveB = o.spy(function onbeforeremove(){ return {then(resolve){thenCBB = resolve}} }) const onbeforeremoveC = o.spy(function onbeforeremove(){ return {then(resolve){thenCBC = resolve}} }) // to avoid updating internal nodes only, vnodes have key attributes const A = fragment({key: 1, onbeforeremove: onbeforeremoveA}, [m("a1"), m("a2")]) const B = fragment({key: 2, onbeforeremove: onbeforeremoveB}, [m("b1"), m("b2")]) const C = fragment({key: 3, onbeforeremove: onbeforeremoveC}, [m("c1"), m("c2")]) render(root, [A]) o(onbeforeremoveA.callCount).equals(0) o(onbeforeremoveB.callCount).equals(0) o(onbeforeremoveC.callCount).equals(0) render(root, [B]) o(onbeforeremoveA.callCount).equals(1) o(onbeforeremoveB.callCount).equals(0) o(onbeforeremoveC.callCount).equals(0) render(root, [C]) o(onbeforeremoveA.callCount).equals(1) o(onbeforeremoveB.callCount).equals(1) o(onbeforeremoveC.callCount).equals(0) render(root, []) o(onbeforeremoveA.callCount).equals(1) o(onbeforeremoveB.callCount).equals(1) o(onbeforeremoveC.callCount).equals(1) // not resolved o(root.childNodes.length).equals(6) o(root.childNodes[0].nodeName).equals("A1") o(root.childNodes[1].nodeName).equals("A2") o(root.childNodes[2].nodeName).equals("B1") o(root.childNodes[3].nodeName).equals("B2") o(root.childNodes[4].nodeName).equals("C1") o(root.childNodes[5].nodeName).equals("C2") const iterA = domFor(A) o(iterA.next().value.nodeName).equals("A1") o(iterA.next().value.nodeName).equals("A2") o(iterA.next().done).deepEquals(true) const iterB = domFor(B) o(iterB.next().value.nodeName).equals("B1") o(iterB.next().value.nodeName).equals("B2") o(iterB.next().done).deepEquals(true) const iterC = domFor(C) o(iterC.next().value.nodeName).equals("C1") o(iterC.next().value.nodeName).equals("C2") o(iterC.next().done).deepEquals(true) callAsync(function(){ // not resolved yet o(root.childNodes.length).equals(6) o(root.childNodes[0].nodeName).equals("A1") o(root.childNodes[1].nodeName).equals("A2") o(root.childNodes[2].nodeName).equals("B1") o(root.childNodes[3].nodeName).equals("B2") o(root.childNodes[4].nodeName).equals("C1") o(root.childNodes[5].nodeName).equals("C2") const iterA = domFor(A) o(iterA.next().value.nodeName).equals("A1") o(iterA.next().value.nodeName).equals("A2") o(iterA.next().done).deepEquals(true) const iterB = domFor(B) o(iterB.next().value.nodeName).equals("B1") o(iterB.next().value.nodeName).equals("B2") o(iterB.next().done).deepEquals(true) const iterC = domFor(C) o(iterC.next().value.nodeName).equals("C1") o(iterC.next().value.nodeName).equals("C2") o(iterC.next().done).deepEquals(true) // resolve B thenCBB() callAsync(function(){ o(root.childNodes.length).equals(4) o(root.childNodes[0].nodeName).equals("A1") o(root.childNodes[1].nodeName).equals("A2") o(root.childNodes[2].nodeName).equals("C1") o(root.childNodes[3].nodeName).equals("C2") const iterA = domFor(A) o(iterA.next().value.nodeName).equals("A1") o(iterA.next().value.nodeName).equals("A2") o(iterA.next().done).deepEquals(true) const iterC = domFor(C) o(iterC.next().value.nodeName).equals("C1") o(iterC.next().value.nodeName).equals("C2") o(iterC.next().done).deepEquals(true) // resolve C thenCBC() callAsync(function(){ o(root.childNodes.length).equals(2) o(root.childNodes[0].nodeName).equals("A1") o(root.childNodes[1].nodeName).equals("A2") const iterA = domFor(A) o(iterA.next().value.nodeName).equals("A1") o(iterA.next().value.nodeName).equals("A2") o(iterA.next().done).deepEquals(true) // resolve A thenCBA() callAsync(function(){ o(root.childNodes.length).equals(0) done() }) }) }) }) }) o("works multiple vnodes with onbeforeremove (#3007, 2/6, CAB)", function (done) { let thenCBA, thenCBB, thenCBC const onbeforeremoveA = o.spy(function onbeforeremove(){ return {then(resolve){thenCBA = resolve}} }) const onbeforeremoveB = o.spy(function onbeforeremove(){ return {then(resolve){thenCBB = resolve}} }) const onbeforeremoveC = o.spy(function onbeforeremove(){ return {then(resolve){thenCBC = resolve}} }) // to avoid updating internal nodes only, vnodes have key attributes const A = fragment({key: 1, onbeforeremove: onbeforeremoveA}, [m("a1"), m("a2")]) const B = fragment({key: 2, onbeforeremove: onbeforeremoveB}, [m("b1"), m("b2")]) const C = fragment({key: 3, onbeforeremove: onbeforeremoveC}, [m("c1"), m("c2")]) render(root, [A]) o(onbeforeremoveA.callCount).equals(0) o(onbeforeremoveB.callCount).equals(0) o(onbeforeremoveC.callCount).equals(0) render(root, [B]) o(onbeforeremoveA.callCount).equals(1) o(onbeforeremoveB.callCount).equals(0) o(onbeforeremoveC.callCount).equals(0) render(root, [C]) o(onbeforeremoveA.callCount).equals(1) o(onbeforeremoveB.callCount).equals(1) o(onbeforeremoveC.callCount).equals(0) render(root, []) o(onbeforeremoveA.callCount).equals(1) o(onbeforeremoveB.callCount).equals(1) o(onbeforeremoveC.callCount).equals(1) // not resolved o(root.childNodes.length).equals(6) o(root.childNodes[0].nodeName).equals("A1") o(root.childNodes[1].nodeName).equals("A2") o(root.childNodes[2].nodeName).equals("B1") o(root.childNodes[3].nodeName).equals("B2") o(root.childNodes[4].nodeName).equals("C1") o(root.childNodes[5].nodeName).equals("C2") const iterA = domFor(A) o(iterA.next().value.nodeName).equals("A1") o(iterA.next().value.nodeName).equals("A2") o(iterA.next().done).deepEquals(true) const iterB = domFor(B) o(iterB.next().value.nodeName).equals("B1") o(iterB.next().value.nodeName).equals("B2") o(iterB.next().done).deepEquals(true) const iterC = domFor(C) o(iterC.next().value.nodeName).equals("C1") o(iterC.next().value.nodeName).equals("C2") o(iterC.next().done).deepEquals(true) callAsync(function(){ // not resolved yet o(root.childNodes.length).equals(6) o(root.childNodes[0].nodeName).equals("A1") o(root.childNodes[1].nodeName).equals("A2") o(root.childNodes[2].nodeName).equals("B1") o(root.childNodes[3].nodeName).equals("B2") o(root.childNodes[4].nodeName).equals("C1") o(root.childNodes[5].nodeName).equals("C2") const iterA = domFor(A) o(iterA.next().value.nodeName).equals("A1") o(iterA.next().value.nodeName).equals("A2") o(iterA.next().done).deepEquals(true) const iterB = domFor(B) o(iterB.next().value.nodeName).equals("B1") o(iterB.next().value.nodeName).equals("B2") o(iterB.next().done).deepEquals(true) const iterC = domFor(C) o(iterC.next().value.nodeName).equals("C1") o(iterC.next().value.nodeName).equals("C2") o(iterC.next().done).deepEquals(true) // resolve C thenCBC() callAsync(function(){ o(root.childNodes.length).equals(4) o(root.childNodes[0].nodeName).equals("A1") o(root.childNodes[1].nodeName).equals("A2") o(root.childNodes[2].nodeName).equals("B1") o(root.childNodes[3].nodeName).equals("B2") const iterB = domFor(B) o(iterB.next().value.nodeName).equals("B1") o(iterB.next().value.nodeName).equals("B2") o(iterB.next().done).deepEquals(true) const iterA = domFor(A) o(iterA.next().value.nodeName).equals("A1") o(iterA.next().value.nodeName).equals("A2") o(iterA.next().done).deepEquals(true) // resolve A thenCBA() callAsync(function(){ o(root.childNodes.length).equals(2) o(root.childNodes[0].nodeName).equals("B1") o(root.childNodes[1].nodeName).equals("B2") const iterB = domFor(B) o(iterB.next().value.nodeName).equals("B1") o(iterB.next().value.nodeName).equals("B2") o(iterB.next().done).deepEquals(true) // resolve B thenCBB() callAsync(function(){ o(root.childNodes.length).equals(0) done() }) }) }) }) }) o("works multiple vnodes with onbeforeremove (#3007, 3/6, ABC)", function (done) { let thenCBA, thenCBB, thenCBC const onbeforeremoveA = o.spy(function onbeforeremove(){ return {then(resolve){thenCBA = resolve}} }) const onbeforeremoveB = o.spy(function onbeforeremove(){ return {then(resolve){thenCBB = resolve}} }) const onbeforeremoveC = o.spy(function onbeforeremove(){ return {then(resolve){thenCBC = resolve}} }) // to avoid updating internal nodes only, vnodes have key attributes const A = fragment({key: 1, onbeforeremove: onbeforeremoveA}, [m("a1"), m("a2")]) const B = fragment({key: 2, onbeforeremove: onbeforeremoveB}, [m("b1"), m("b2")]) const C = fragment({key: 3, onbeforeremove: onbeforeremoveC}, [m("c1"), m("c2")]) render(root, [A]) o(onbeforeremoveA.callCount).equals(0) o(onbeforeremoveB.callCount).equals(0) o(onbeforeremoveC.callCount).equals(0) render(root, [B]) o(onbeforeremoveA.callCount).equals(1) o(onbeforeremoveB.callCount).equals(0) o(onbeforeremoveC.callCount).equals(0) render(root, [C]) o(onbeforeremoveA.callCount).equals(1) o(onbeforeremoveB.callCount).equals(1) o(onbeforeremoveC.callCount).equals(0) render(root, []) o(onbeforeremoveA.callCount).equals(1) o(onbeforeremoveB.callCount).equals(1) o(onbeforeremoveC.callCount).equals(1) // not resolved o(root.childNodes.length).equals(6) o(root.childNodes[0].nodeName).equals("A1") o(root.childNodes[1].nodeName).equals("A2") o(root.childNodes[2].nodeName).equals("B1") o(root.childNodes[3].nodeName).equals("B2") o(root.childNodes[4].nodeName).equals("C1") o(root.childNodes[5].nodeName).equals("C2") const iterA = domFor(A) o(iterA.next().value.nodeName).equals("A1") o(iterA.next().value.nodeName).equals("A2") o(iterA.next().done).deepEquals(true) const iterB = domFor(B) o(iterB.next().value.nodeName).equals("B1") o(iterB.next().value.nodeName).equals("B2") o(iterB.next().done).deepEquals(true) const iterC = domFor(C) o(iterC.next().value.nodeName).equals("C1") o(iterC.next().value.nodeName).equals("C2") o(iterC.next().done).deepEquals(true) callAsync(function(){ // not resolved yet o(root.childNodes.length).equals(6) o(root.childNodes[0].nodeName).equals("A1") o(root.childNodes[1].nodeName).equals("A2") o(root.childNodes[2].nodeName).equals("B1") o(root.childNodes[3].nodeName).equals("B2") o(root.childNodes[4].nodeName).equals("C1") o(root.childNodes[5].nodeName).equals("C2") const iterA = domFor(A) o(iterA.next().value.nodeName).equals("A1") o(iterA.next().value.nodeName).equals("A2") o(iterA.next().done).deepEquals(true) const iterB = domFor(B) o(iterB.next().value.nodeName).equals("B1") o(iterB.next().value.nodeName).equals("B2") o(iterB.next().done).deepEquals(true) const iterC = domFor(C) o(iterC.next().value.nodeName).equals("C1") o(iterC.next().value.nodeName).equals("C2") o(iterC.next().done).deepEquals(true) // resolve A thenCBA() callAsync(function(){ o(root.childNodes.length).equals(4) o(root.childNodes[0].nodeName).equals("B1") o(root.childNodes[1].nodeName).equals("B2") o(root.childNodes[2].nodeName).equals("C1") o(root.childNodes[3].nodeName).equals("C2") const iterB = domFor(B) o(iterB.next().value.nodeName).equals("B1") o(iterB.next().value.nodeName).equals("B2") o(iterB.next().done).deepEquals(true) const iterC = domFor(C) o(iterC.next().value.nodeName).equals("C1") o(iterC.next().value.nodeName).equals("C2") o(iterC.next().done).deepEquals(true) // resolve B thenCBB() callAsync(function(){ o(root.childNodes.length).equals(2) o(root.childNodes[0].nodeName).equals("C1") o(root.childNodes[1].nodeName).equals("C2") const iterC = domFor(C) o(iterC.next().value.nodeName).equals("C1") o(iterC.next().value.nodeName).equals("C2") o(iterC.next().done).deepEquals(true) // resolve C thenCBC() callAsync(function(){ o(root.childNodes.length).equals(0) done() }) }) }) }) }) o("works multiple vnodes with onbeforeremove (#3007, 4/6, ACB)", function (done) { let thenCBA, thenCBB, thenCBC const onbeforeremoveA = o.spy(function onbeforeremove(){ return {then(resolve){thenCBA = resolve}} }) const onbeforeremoveB = o.spy(function onbeforeremove(){ return {then(resolve){thenCBB = resolve}} }) const onbeforeremoveC = o.spy(function onbeforeremove(){ return {then(resolve){thenCBC = resolve}} }) // to avoid updating internal nodes only, vnodes have key attributes const A = fragment({key: 1, onbeforeremove: onbeforeremoveA}, [m("a1"), m("a2")]) const B = fragment({key: 2, onbeforeremove: onbeforeremoveB}, [m("b1"), m("b2")]) const C = fragment({key: 3, onbeforeremove: onbeforeremoveC}, [m("c1"), m("c2")]) render(root, [A]) o(onbeforeremoveA.callCount).equals(0) o(onbeforeremoveB.callCount).equals(0) o(onbeforeremoveC.callCount).equals(0) render(root, [B]) o(onbeforeremoveA.callCount).equals(1) o(onbeforeremoveB.callCount).equals(0) o(onbeforeremoveC.callCount).equals(0) render(root, [C]) o(onbeforeremoveA.callCount).equals(1) o(onbeforeremoveB.callCount).equals(1) o(onbeforeremoveC.callCount).equals(0) render(root, []) o(onbeforeremoveA.callCount).equals(1) o(onbeforeremoveB.callCount).equals(1) o(onbeforeremoveC.callCount).equals(1) // not resolved o(root.childNodes.length).equals(6) o(root.childNodes[0].nodeName).equals("A1") o(root.childNodes[1].nodeName).equals("A2") o(root.childNodes[2].nodeName).equals("B1") o(root.childNodes[3].nodeName).equals("B2") o(root.childNodes[4].nodeName).equals("C1") o(root.childNodes[5].nodeName).equals("C2") const iterA = domFor(A) o(iterA.next().value.nodeName).equals("A1") o(iterA.next().value.nodeName).equals("A2") o(iterA.next().done).deepEquals(true) const iterB = domFor(B) o(iterB.next().value.nodeName).equals("B1") o(iterB.next().value.nodeName).equals("B2") o(iterB.next().done).deepEquals(true) const iterC = domFor(C) o(iterC.next().value.nodeName).equals("C1") o(iterC.next().value.nodeName).equals("C2") o(iterC.next().done).deepEquals(true) callAsync(function(){ // not resolved yet o(root.childNodes.length).equals(6) o(root.childNodes[0].nodeName).equals("A1") o(root.childNodes[1].nodeName).equals("A2") o(root.childNodes[2].nodeName).equals("B1") o(root.childNodes[3].nodeName).equals("B2") o(root.childNodes[4].nodeName).equals("C1") o(root.childNodes[5].nodeName).equals("C2") const iterA = domFor(A) o(iterA.next().value.nodeName).equals("A1") o(iterA.next().value.nodeName).equals("A2") o(iterA.next().done).deepEquals(true) const iterB = domFor(B) o(iterB.next().value.nodeName).equals("B1") o(iterB.next().value.nodeName).equals("B2") o(iterB.next().done).deepEquals(true) const iterC = domFor(C) o(iterC.next().value.nodeName).equals("C1") o(iterC.next().value.nodeName).equals("C2") o(iterC.next().done).deepEquals(true) // resolve A thenCBA() callAsync(function(){ o(root.childNodes.length).equals(4) o(root.childNodes[0].nodeName).equals("B1") o(root.childNodes[1].nodeName).equals("B2") o(root.childNodes[2].nodeName).equals("C1") o(root.childNodes[3].nodeName).equals("C2") const iterB = domFor(B) o(iterB.next().value.nodeName).equals("B1") o(iterB.next().value.nodeName).equals("B2") o(iterB.next().done).deepEquals(true) const iterC = domFor(C) o(iterC.next().value.nodeName).equals("C1") o(iterC.next().value.nodeName).equals("C2") o(iterC.next().done).deepEquals(true) // resolve C thenCBC() callAsync(function(){ o(root.childNodes.length).equals(2) o(root.childNodes[0].nodeName).equals("B1") o(root.childNodes[1].nodeName).equals("B2") const iterC = domFor(B) o(iterC.next().value.nodeName).equals("B1") o(iterC.next().value.nodeName).equals("B2") o(iterC.next().done).deepEquals(true) // resolve B thenCBB() callAsync(function(){ o(root.childNodes.length).equals(0) done() }) }) }) }) }) o("works multiple vnodes with onbeforeremove (#3007, 5/6, BAC)", function (done) { let thenCBA, thenCBB, thenCBC const onbeforeremoveA = o.spy(function onbeforeremove(){ return {then(resolve){thenCBA = resolve}} }) const onbeforeremoveB = o.spy(function onbeforeremove(){ return {then(resolve){thenCBB = resolve}} }) const onbeforeremoveC = o.spy(function onbeforeremove(){ return {then(resolve){thenCBC = resolve}} }) // to avoid updating internal nodes only, vnodes have key attributes const A = fragment({key: 1, onbeforeremove: onbeforeremoveA}, [m("a1"), m("a2")]) const B = fragment({key: 2, onbeforeremove: onbeforeremoveB}, [m("b1"), m("b2")]) const C = fragment({key: 3, onbeforeremove: onbeforeremoveC}, [m("c1"), m("c2")]) render(root, [A]) o(onbeforeremoveA.callCount).equals(0) o(onbeforeremoveB.callCount).equals(0) o(onbeforeremoveC.callCount).equals(0) render(root, [B]) o(onbeforeremoveA.callCount).equals(1) o(onbeforeremoveB.callCount).equals(0) o(onbeforeremoveC.callCount).equals(0) render(root, [C]) o(onbeforeremoveA.callCount).equals(1) o(onbeforeremoveB.callCount).equals(1) o(onbeforeremoveC.callCount).equals(0) render(root, []) o(onbeforeremoveA.callCount).equals(1) o(onbeforeremoveB.callCount).equals(1) o(onbeforeremoveC.callCount).equals(1) // not resolved o(root.childNodes.length).equals(6) o(root.childNodes[0].nodeName).equals("A1") o(root.childNodes[1].nodeName).equals("A2") o(root.childNodes[2].nodeName).equals("B1") o(root.childNodes[3].nodeName).equals("B2") o(root.childNodes[4].nodeName).equals("C1") o(root.childNodes[5].nodeName).equals("C2") const iterA = domFor(A) o(iterA.next().value.nodeName).equals("A1") o(iterA.next().value.nodeName).equals("A2") o(iterA.next().done).deepEquals(true) const iterB = domFor(B) o(iterB.next().value.nodeName).equals("B1") o(iterB.next().value.nodeName).equals("B2") o(iterB.next().done).deepEquals(true) const iterC = domFor(C) o(iterC.next().value.nodeName).equals("C1") o(iterC.next().value.nodeName).equals("C2") o(iterC.next().done).deepEquals(true) callAsync(function(){ // not resolved yet o(root.childNodes.length).equals(6) o(root.childNodes[0].nodeName).equals("A1") o(root.childNodes[1].nodeName).equals("A2") o(root.childNodes[2].nodeName).equals("B1") o(root.childNodes[3].nodeName).equals("B2") o(root.childNodes[4].nodeName).equals("C1") o(root.childNodes[5].nodeName).equals("C2") const iterA = domFor(A) o(iterA.next().value.nodeName).equals("A1") o(iterA.next().value.nodeName).equals("A2") o(iterA.next().done).deepEquals(true) const iterB = domFor(B) o(iterB.next().value.nodeName).equals("B1") o(iterB.next().value.nodeName).equals("B2") o(iterB.next().done).deepEquals(true) const iterC = domFor(C) o(iterC.next().value.nodeName).equals("C1") o(iterC.next().value.nodeName).equals("C2") o(iterC.next().done).deepEquals(true) // resolve B thenCBB() callAsync(function(){ o(root.childNodes.length).equals(4) o(root.childNodes[0].nodeName).equals("A1") o(root.childNodes[1].nodeName).equals("A2") o(root.childNodes[2].nodeName).equals("C1") o(root.childNodes[3].nodeName).equals("C2") const iterB = domFor(A) o(iterB.next().value.nodeName).equals("A1") o(iterB.next().value.nodeName).equals("A2") o(iterB.next().done).deepEquals(true) const iterC = domFor(C) o(iterC.next().value.nodeName).equals("C1") o(iterC.next().value.nodeName).equals("C2") o(iterC.next().done).deepEquals(true) // resolve A thenCBA() callAsync(function(){ o(root.childNodes.length).equals(2) o(root.childNodes[0].nodeName).equals("C1") o(root.childNodes[1].nodeName).equals("C2") const iterC = domFor(C) o(iterC.next().value.nodeName).equals("C1") o(iterC.next().value.nodeName).equals("C2") o(iterC.next().done).deepEquals(true) // resolve C thenCBC() callAsync(function(){ o(root.childNodes.length).equals(0) done() }) }) }) }) }) o("works multiple vnodes with onbeforeremove (#3007, 6/6, CBA)", function (done) { let thenCBA, thenCBB, thenCBC const onbeforeremoveA = o.spy(function onbeforeremove(){ return {then(resolve){thenCBA = resolve}} }) const onbeforeremoveB = o.spy(function onbeforeremove(){ return {then(resolve){thenCBB = resolve}} }) const onbeforeremoveC = o.spy(function onbeforeremove(){ return {then(resolve){thenCBC = resolve}} }) // to avoid updating internal nodes only, vnodes have key attributes const A = fragment({key: 1, onbeforeremove: onbeforeremoveA}, [m("a1"), m("a2")]) const B = fragment({key: 2, onbeforeremove: onbeforeremoveB}, [m("b1"), m("b2")]) const C = fragment({key: 3, onbeforeremove: onbeforeremoveC}, [m("c1"), m("c2")]) render(root, [A]) o(onbeforeremoveA.callCount).equals(0) o(onbeforeremoveB.callCount).equals(0) o(onbeforeremoveC.callCount).equals(0) render(root, [B]) o(onbeforeremoveA.callCount).equals(1) o(onbeforeremoveB.callCount).equals(0) o(onbeforeremoveC.callCount).equals(0) render(root, [C]) o(onbeforeremoveA.callCount).equals(1) o(onbeforeremoveB.callCount).equals(1) o(onbeforeremoveC.callCount).equals(0) render(root, []) o(onbeforeremoveA.callCount).equals(1) o(onbeforeremoveB.callCount).equals(1) o(onbeforeremoveC.callCount).equals(1) // not resolved o(root.childNodes.length).equals(6) o(root.childNodes[0].nodeName).equals("A1") o(root.childNodes[1].nodeName).equals("A2") o(root.childNodes[2].nodeName).equals("B1") o(root.childNodes[3].nodeName).equals("B2") o(root.childNodes[4].nodeName).equals("C1") o(root.childNodes[5].nodeName).equals("C2") const iterA = domFor(A) o(iterA.next().value.nodeName).equals("A1") o(iterA.next().value.nodeName).equals("A2") o(iterA.next().done).deepEquals(true) const iterB = domFor(B) o(iterB.next().value.nodeName).equals("B1") o(iterB.next().value.nodeName).equals("B2") o(iterB.next().done).deepEquals(true) const iterC = domFor(C) o(iterC.next().value.nodeName).equals("C1") o(iterC.next().value.nodeName).equals("C2") o(iterC.next().done).deepEquals(true) callAsync(function(){ // not resolved yet o(root.childNodes.length).equals(6) o(root.childNodes[0].nodeName).equals("A1") o(root.childNodes[1].nodeName).equals("A2") o(root.childNodes[2].nodeName).equals("B1") o(root.childNodes[3].nodeName).equals("B2") o(root.childNodes[4].nodeName).equals("C1") o(root.childNodes[5].nodeName).equals("C2") const iterA = domFor(A) o(iterA.next().value.nodeName).equals("A1") o(iterA.next().value.nodeName).equals("A2") o(iterA.next().done).deepEquals(true) const iterB = domFor(B) o(iterB.next().value.nodeName).equals("B1") o(iterB.next().value.nodeName).equals("B2") o(iterB.next().done).deepEquals(true) const iterC = domFor(C) o(iterC.next().value.nodeName).equals("C1") o(iterC.next().value.nodeName).equals("C2") o(iterC.next().done).deepEquals(true) // resolve C thenCBC() callAsync(function(){ o(root.childNodes.length).equals(4) o(root.childNodes[0].nodeName).equals("A1") o(root.childNodes[1].nodeName).equals("A2") o(root.childNodes[2].nodeName).equals("B1") o(root.childNodes[3].nodeName).equals("B2") const iterB = domFor(A) o(iterB.next().value.nodeName).equals("A1") o(iterB.next().value.nodeName).equals("A2") o(iterB.next().done).deepEquals(true) const iterC = domFor(B) o(iterC.next().value.nodeName).equals("B1") o(iterC.next().value.nodeName).equals("B2") o(iterC.next().done).deepEquals(true) // resolve B thenCBB() callAsync(function(){ o(root.childNodes.length).equals(2) o(root.childNodes[0].nodeName).equals("A1") o(root.childNodes[1].nodeName).equals("A2") const iterC = domFor(A) o(iterC.next().value.nodeName).equals("A1") o(iterC.next().value.nodeName).equals("A2") o(iterC.next().done).deepEquals(true) // resolve A thenCBA() callAsync(function(){ o(root.childNodes.length).equals(0) done() }) }) }) }) }) 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) }) o("works in state.onbeforeremove and attrs.onbeforeremove", function () { const onbeforeremove = o.spy(function onbeforeremove(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) return {then(){}, finally(){}} }) const C = createComponent({ view({children}){return children}, onbeforeremove }) render(root, m(C, {onbeforeremove}, [ m("a"), m("b"), m("c") ])) render(root, []) o(onbeforeremove.callCount).equals(2) }) }) }) }) ================================================ FILE: render/tests/test-event.js ================================================ "use strict" var o = require("ospec") var callAsync = require("../../test-utils/callAsync") 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) }) o("async function", function(done) { var div = m("div", {onclick: async function () {}}) var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) render(root, div) div.dom.dispatchEvent(e) o(redraw.callCount).equals(1) o(redraw.this).equals(undefined) o(redraw.args.length).equals(0) callAsync(function() { o(redraw.callCount).equals(2) o(redraw.this).equals(undefined) o(redraw.args.length).equals(0) done() }) }) o("async function (await Promise)", function(done) { var thenCB var div = m("div", {onclick: async function () { await new Promise(function(resolve){thenCB = resolve}) }}) var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) render(root, div) div.dom.dispatchEvent(e) o(redraw.callCount).equals(1) o(redraw.this).equals(undefined) o(redraw.args.length).equals(0) callAsync(function() { // not resolved yet o(redraw.callCount).equals(1) // resolve thenCB() callAsync(function() { o(redraw.callCount).equals(2) o(redraw.this).equals(undefined) o(redraw.args.length).equals(0) done() }) }) }) o("async function (await thenable)", function(done) { var thenCB var div = m("div", {onclick: async function () { await {then(resolve){thenCB = resolve}} }}) var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) render(root, div) div.dom.dispatchEvent(e) o(redraw.callCount).equals(1) o(redraw.this).equals(undefined) o(redraw.args.length).equals(0) callAsync(function() { // not resolved yet o(redraw.callCount).equals(1) // resolve thenCB() callAsync(function() { o(redraw.callCount).equals(2) o(redraw.this).equals(undefined) o(redraw.args.length).equals(0) done() }) }) }) o("return Promise", function(done) { var thenCB var div = m("div", {onclick: function () { return new Promise(function(resolve){thenCB = resolve}) }}) var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) render(root, div) div.dom.dispatchEvent(e) o(redraw.callCount).equals(1) o(redraw.this).equals(undefined) o(redraw.args.length).equals(0) callAsync(function() { // not resolved yet o(redraw.callCount).equals(1) // resolve thenCB() callAsync(function() { o(redraw.callCount).equals(2) o(redraw.this).equals(undefined) o(redraw.args.length).equals(0) done() }) }) }) o("return thenable", function(done) { var thenCB var div = m("div", {onclick: function () { return {then(resolve){thenCB = resolve}} }}) var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) render(root, div) div.dom.dispatchEvent(e) o(redraw.callCount).equals(1) o(redraw.this).equals(undefined) o(redraw.args.length).equals(0) callAsync(function() { // not resolved yet o(redraw.callCount).equals(1) // resolve thenCB() callAsync(function() { o(redraw.callCount).equals(2) o(redraw.this).equals(undefined) o(redraw.args.length).equals(0) done() }) }) }) o.spec("do not asynchronous redraw when returned Promise is rejected", function() { var error o.beforeEach(function(){ error = console.error }) o.afterEach(function(){ console.error = error }) o("async function (throw Error)", function(done) { var consoleSpy = o.spy() console.error = consoleSpy var div = m("div", {onclick: async function () {throw Error("error")}}) var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) render(root, div) div.dom.dispatchEvent(e) // sync redraw o(redraw.callCount).equals(1) o(redraw.this).equals(undefined) o(redraw.args.length).equals(0) callAsync(function() { // do not async redraw o(redraw.callCount).equals(1) // called console.error o(consoleSpy.callCount).equals(1) done() }) }) o("async function (await Promise, reject)", function(done) { var consoleSpy = o.spy() console.error = consoleSpy var rejectCB var div = m("div", {onclick: async function () { await new Promise(function(_, reject){rejectCB = reject}) }}) var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) render(root, div) div.dom.dispatchEvent(e) // sync redraw o(redraw.callCount).equals(1) o(redraw.this).equals(undefined) o(redraw.args.length).equals(0) callAsync(function() { // not resolved yet o(redraw.callCount).equals(1) // reject rejectCB("error") callAsync(function() { // do not async redraw o(redraw.callCount).equals(1) // called console.error o(consoleSpy.callCount).equals(1) done() }) }) }) o("async function (await thenable, reject)", function(done) { var consoleSpy = o.spy() console.error = consoleSpy var rejectCB var div = m("div", {onclick: async function () { await {then(_, reject){rejectCB = reject}} }}) var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) render(root, div) div.dom.dispatchEvent(e) // sync redraw o(redraw.callCount).equals(1) o(redraw.this).equals(undefined) o(redraw.args.length).equals(0) callAsync(function() { // not resolved yet o(redraw.callCount).equals(1) // reject rejectCB("error") callAsync(function() { // do not async redraw o(redraw.callCount).equals(1) // called console.error o(consoleSpy.callCount).equals(1) done() }) }) }) o("async function (await Promise, throw Error)", function(done) { var consoleSpy = o.spy() console.error = consoleSpy var thenCB var div = m("div", {onclick: async function () { await new Promise(function(resolve){thenCB = resolve}) throw Error("error") }}) var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) render(root, div) div.dom.dispatchEvent(e) // sync redraw o(redraw.callCount).equals(1) o(redraw.this).equals(undefined) o(redraw.args.length).equals(0) callAsync(function() { // not resolved yet o(redraw.callCount).equals(1) // resolve (and throw Error) thenCB() callAsync(function() { // do not async redraw o(redraw.callCount).equals(1) // called console.error o(consoleSpy.callCount).equals(1) done() }) }) }) o("async function (await thenable, throw Error)", function(done) { var consoleSpy = o.spy() console.error = consoleSpy var thenCB var div = m("div", {onclick: async function () { await {then(resolve){thenCB = resolve}} throw Error("error") }}) var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) render(root, div) div.dom.dispatchEvent(e) // sync redraw o(redraw.callCount).equals(1) o(redraw.this).equals(undefined) o(redraw.args.length).equals(0) callAsync(function() { // not resolved yet o(redraw.callCount).equals(1) // resolve (and throw Error) thenCB() callAsync(function() { // do not async redraw o(redraw.callCount).equals(1) // called console.error o(consoleSpy.callCount).equals(1) done() }) }) }) o("return Promise (reject)", function(done) { var consoleSpy = o.spy() console.error = consoleSpy var rejectCB var div = m("div", {onclick: function () { return new Promise(function(_, reject){rejectCB = reject}) }}) var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) render(root, div) div.dom.dispatchEvent(e) // sync redraw o(redraw.callCount).equals(1) o(redraw.this).equals(undefined) o(redraw.args.length).equals(0) callAsync(function() { // not resolved yet o(redraw.callCount).equals(1) // reject rejectCB("error") callAsync(function() { // do not async redraw o(redraw.callCount).equals(1) // called console.error o(consoleSpy.callCount).equals(1) done() }) }) }) o("return thenable (reject)", function(done) { var consoleSpy = o.spy() console.error = consoleSpy var rejectCB var div = m("div", {onclick: function () { return {then(_, reject){rejectCB = reject}} }}) var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) render(root, div) div.dom.dispatchEvent(e) // sync redraw o(redraw.callCount).equals(1) o(redraw.this).equals(undefined) o(redraw.args.length).equals(0) callAsync(function() { // not resolved yet o(redraw.callCount).equals(1) // resolve rejectCB("error") callAsync(function() { // do not async redraw o(redraw.callCount).equals(1) // called console.error o(consoleSpy.callCount).equals(1) done() }) }) }) }) o("async function (event.redraw = false)", function(done) { var spy = o.spy() var div = m("div", {onclick: async function (ev) { // set event.redraw = false to prevent redraws ev.redraw = false spy() }}) var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) render(root, div) // event listener has not yet been called o(spy.callCount).equals(0) div.dom.dispatchEvent(e) // event listener called but not redraw o(spy.callCount).equals(1) o(redraw.callCount).equals(0) callAsync(function() { o(spy.callCount).equals(1) o(redraw.callCount).equals(0) done() }) }) o("async function (event.redraw = false, await Promise)", function(done) { var thenCB var spy = o.spy(function(resolve){thenCB = resolve}) var div = m("div", {onclick: async function (ev) { // set event.redraw = false to prevent redraws ev.redraw = false await new Promise(spy) }}) var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) render(root, div) // event listener has not yet been called o(spy.callCount).equals(0) o(thenCB).equals(undefined) div.dom.dispatchEvent(e) // event listener called but not redraw o(spy.callCount).equals(1) o(thenCB).notEquals(undefined) o(redraw.callCount).equals(0) callAsync(function() { // not resolved yet o(spy.callCount).equals(1) o(redraw.callCount).equals(0) // resolve thenCB() callAsync(function() { o(spy.callCount).equals(1) o(redraw.callCount).equals(0) done() }) }) }) o("async function (await Promise, event.redraw = false)", function(done) { var thenCB var div = m("div", {onclick: async function (ev) { await new Promise(function(resolve){thenCB = resolve}) // set event.redraw = false to prevent additional redraw ev.redraw = false }}) var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) render(root, div) div.dom.dispatchEvent(e) o(redraw.callCount).equals(1) o(redraw.this).equals(undefined) o(redraw.args.length).equals(0) callAsync(function() { // not resolved yet o(redraw.callCount).equals(1) // resolve thenCB() callAsync(function() { o(redraw.callCount).equals(1) done() }) }) }) o("async function (event.redraw = false, await Promise, event.redraw = true)", function(done) { var thenCB var spy = o.spy(function(resolve){thenCB = resolve}) var div = m("div", {onclick: async function (ev) { // set event.redraw = false to prevent sync redraw ev.redraw = false await new Promise(spy) // set event.redraw = true to enable async redraw ev.redraw = true }}) var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) render(root, div) // event listener has not yet been called o(spy.callCount).equals(0) o(thenCB).equals(undefined) div.dom.dispatchEvent(e) // event listener called but not redraw o(spy.callCount).equals(1) o(thenCB).notEquals(undefined) o(redraw.callCount).equals(0) callAsync(function() { // not resolved yet o(spy.callCount).equals(1) o(redraw.callCount).equals(0) // resolve thenCB() callAsync(function() { o(spy.callCount).equals(1) o(redraw.callCount).equals(1) o(redraw.this).equals(undefined) o(redraw.args.length).equals(0) done() }) }) }) o("async function (multiple await)", function(done) { var thenCB1, thenCB2 var div = m("div", {onclick: async function () { await new Promise(function(resolve){thenCB1 = resolve}) await new Promise(function(resolve){thenCB2 = resolve}) }}) var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) render(root, div) div.dom.dispatchEvent(e) o(redraw.callCount).equals(1) o(redraw.this).equals(undefined) o(redraw.args.length).equals(0) callAsync(function() { o(redraw.callCount).equals(1) // resolve 1 thenCB1() callAsync(function() { o(redraw.callCount).equals(1) // resolve 2 thenCB2() callAsync(function() { o(redraw.callCount).equals(2) o(redraw.this).equals(undefined) o(redraw.args.length).equals(0) done() }) }) }) }) o("avoid sync redraw after removal", function() { var spy = o.spy() var div = m("div", {onclick: spy}) var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) render(root, div) // remove div render(root, []) // event listener has not yet been called o(spy.callCount).equals(0) div.dom.dispatchEvent(e) // event listener called but not redraw o(spy.callCount).equals(1) o(redraw.callCount).equals(0) }) o("avoid async redraw after removal", function(done) { var thenCB var spy = o.spy(function(resolve){thenCB = resolve}) var div = m("div", {onclick: async function () { await new Promise(spy) }}) var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) render(root, div) // event listener has not yet been called o(spy.callCount).equals(0) o(thenCB).equals(undefined) div.dom.dispatchEvent(e) // event listener called o(spy.callCount).equals(1) o(thenCB).notEquals(undefined) o(redraw.callCount).equals(1) o(redraw.this).equals(undefined) o(redraw.args.length).equals(0) callAsync(function() { // not resolved yet o(spy.callCount).equals(1) o(redraw.callCount).equals(1) // remove div render(root, []) // resolve thenCB() callAsync(function() { o(spy.callCount).equals(1) o(redraw.callCount).equals(1) done() }) }) }) }) ================================================ FILE: render/tests/test-fragment.js ================================================ "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.attrs).deepEquals({}) 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.attrs).deepEquals({}) o(vnode.children[0].tag).equals("#") o(vnode.children[0].children).equals("") }) o("handles number single child", function() { var vnode = fragment([1]) o(vnode.attrs).deepEquals({}) 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.attrs).deepEquals({}) 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.attrs).deepEquals({}) o(vnode.children).deepEquals([null]) }) o("handles falsy boolean single child", function() { var vnode = fragment([false]) o(vnode.attrs).deepEquals({}) o(vnode.children).deepEquals([null]) }) o("handles null single child", function() { var vnode = fragment([null]) o(vnode.attrs).deepEquals({}) o(vnode.children[0]).equals(null) }) o("handles undefined single child", function() { var vnode = fragment([undefined]) o(vnode.attrs).deepEquals({}) o(vnode.children).deepEquals([null]) }) o("handles multiple string children", function() { var vnode = fragment(["", "a"]) o(vnode.attrs).deepEquals({}) 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.attrs).deepEquals({}) 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.attrs).deepEquals({}) o(vnode.children).deepEquals([null, null]) }) o("handles multiple null/undefined child", function() { var vnode = fragment([null, undefined]) o(vnode.attrs).deepEquals({}) o(vnode.children).deepEquals([null, null]) }) o("handles falsy number single child without attrs", function() { var vnode = fragment(0) o(vnode.attrs).deepEquals({}) 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.attrs).deepEquals({}) 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.attrs).deepEquals({}) o(vnode.children[0].tag).equals("#") o(vnode.children[0].children).equals("") }) o("handles number single child", function() { var vnode = fragment({}, [1]) o(vnode.attrs).deepEquals({}) 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.attrs).deepEquals({}) 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.attrs).deepEquals({}) o(vnode.children).deepEquals([null]) }) o("handles falsy boolean single child", function() { var vnode = fragment({}, [false]) o(vnode.attrs).deepEquals({}) o(vnode.children).deepEquals([null]) }) o("handles null single child", function() { var vnode = fragment({}, [null]) o(vnode.attrs).deepEquals({}) o(vnode.children).deepEquals([null]) }) o("handles undefined single child", function() { var vnode = fragment({}, [undefined]) o(vnode.attrs).deepEquals({}) o(vnode.children).deepEquals([null]) }) o("handles multiple string children", function() { var vnode = fragment({}, ["", "a"]) o(vnode.attrs).deepEquals({}) 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.attrs).deepEquals({}) 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.attrs).deepEquals({}) o(vnode.children).deepEquals([null, null]) }) o("handles multiple null/undefined child", function() { var vnode = fragment({}, [null, undefined]) o(vnode.attrs).deepEquals({}) o(vnode.children).deepEquals([null, null]) }) }) }) } runTest("fragment", fragment); runTest("fragment-string-selector", fragmentStr); ================================================ FILE: render/tests/test-hyperscript.js ================================================ "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 children without attr (fragment)", function() { var vnode = m("[", [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 (fragment)", function() { var vnode = m("[", m("i")) o(vnode.attrs).deepEquals({}) o(vnode.children[0].tag).equals("i") }) o("handles children without attr unwrapped (fragment)", function() { var vnode = m("[", 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") }) o("works with POJOs (without attrs)", function() { var component = { view: function() {} } var vnode = m(component, "b") o(vnode.tag).equals(component) o(vnode.attrs).deepEquals({}) o(vnode.children.length).equals(1) o(vnode.children[0]).equals("b") }) o("works with constructibles (without attrs)", function() { var component = o.spy() component.prototype.view = function() {} var vnode = m(component, "b") o(component.callCount).equals(0) o(vnode.tag).equals(component) o(vnode.attrs).deepEquals({}) o(vnode.children.length).equals(1) o(vnode.children[0]).equals("b") }) o("works with closures (without attrs)", function () { var component = o.spy() var vnode = m(component, "b") o(component.callCount).equals(0) o(vnode.tag).equals(component) o(vnode.attrs).deepEquals({}) o(vnode.children.length).equals(1) o(vnode.children[0]).equals("b") }) }) }) ================================================ FILE: render/tests/test-input.js ================================================ "use strict" var o = require("ospec") var domMock = require("../../test-utils/domMock") var vdom = require("../../render/render") var m = require("../../render/hyperscript") 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 }) 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}) render(root, [input, a, b]) input.dom.focus() render(root, [a, input, b]) o($window.document.activeElement).equals(input.dom) }) o("maintains focus when changed manually in hook", function() { var input = m("input", {oncreate: function() { input.dom.focus(); }}); render(root, input) o($window.document.activeElement).equals(input.dom) }) 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() {}}) render(root, input) //simulate user typing var e = $window.document.createEvent("KeyboardEvent") e.initEvent("input", true, true) input.dom.focus() input.dom.value += "a" input.dom.dispatchEvent(e) //re-render may use same vdom value as previous render call render(root, updated) o(updated.dom.value).equals("aaa") }) 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) o(updated.dom.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() {}}) render(root, input) //simulate user clicking checkbox var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) input.dom.focus() input.dom.dispatchEvent(e) //re-render may use same vdom value as previous render call render(root, updated) o(updated.dom.checked).equals(true) }) o("syncs file input value attribute if DOM value differs from vdom value and is empty", function() { var input = m("input", {type: "file", value: "", onclick: function() {}}) var updated = m("input", {type: "file", value: "", onclick: function() {}}) var spy = o.spy() var error = console.error render(root, input) input.dom.value = "test.png" try { console.error = spy render(root, updated) } finally { console.error = error } o(updated.dom.value).equals("") o(spy.callCount).equals(0) }) o("warns and ignores file input value attribute if DOM value differs from vdom value and is non-empty", function() { var input = m("input", {type: "file", value: "", onclick: function() {}}) var updated = m("input", {type: "file", value: "other.png", onclick: function() {}}) var spy = o.spy() var error = console.error render(root, input) input.dom.value = "test.png" try { console.error = spy render(root, updated) } finally { console.error = error } o(updated.dom.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) 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) // 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) try { console.error = spy render(root, updated1) } finally { console.error = error } o(updated1.dom.value).equals("test.png") o(spy.callCount).equals(0) o($window.__getSpies(updated1.dom).valueSetter.callCount).equals(1) try { console.error = spy render(root, updated2) } finally { console.error = error } o(updated2.dom.value).equals("test.png") o(spy.callCount).equals(0) o($window.__getSpies(updated2.dom).valueSetter.callCount).equals(1) }) }) o.spec("select", function() { o("select works without attributes", function() { var select = m("select", m("option", {value: "a"}, "aaa") ) render(root, select) o(select.dom.value).equals("a") o(select.dom.selectedIndex).equals(0) }) o("select option can have empty string value", function() { var select = m("select", m("option", {value: ""}, "aaa") ) render(root, select) o(select.dom.firstChild.value).equals("") }) o("option value defaults to textContent unless explicitly set", function() { var select = m("select", m("option", "aaa") ) render(root, select) o(select.dom.firstChild.value).equals("aaa") o(select.dom.value).equals("aaa") //test that value changes when content changes select = m("select", m("option", "bbb") ) render(root, select) o(select.dom.firstChild.value).equals("bbb") o(select.dom.value).equals("bbb") //test that value can be set to "" in subsequent render select = m("select", m("option", {value: ""}, "aaa") ) render(root, select) o(select.dom.firstChild.value).equals("") o(select.dom.value).equals("") //test that value reverts to textContent when value omitted select = m("select", m("option", "aaa") ) render(root, select) o(select.dom.firstChild.value).equals("aaa") o(select.dom.value).equals("aaa") }) o("select yields invalid value without children", function() { var select = m("select", {value: "a"}) render(root, select) o(select.dom.value).equals("") o(select.dom.selectedIndex).equals(-1) }) o("select value is set correctly on first render", function() { var select = m("select", {value: "b"}, m("option", {value: "a"}, "aaa"), m("option", {value: "b"}, "bbb"), m("option", {value: "c"}, "ccc") ) render(root, select) o(select.dom.value).equals("b") o(select.dom.selectedIndex).equals(1) }) o("syncs select value if DOM value differs from vdom value", function() { function makeSelect() { return m("select", {value: "b"}, m("option", {value: "a"}, "aaa"), m("option", {value: "b"}, "bbb"), m("option", {value: "c"}, "ccc") ) } render(root, makeSelect()) //simulate user selecting option root.firstChild.value = "c" root.firstChild.focus() //re-render may use same vdom value as previous render call render(root, makeSelect()) o(root.firstChild.value).equals("b") o(root.firstChild.selectedIndex).equals(1) }) }) }) ================================================ FILE: render/tests/test-normalize.js ================================================ "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) }) }) ================================================ FILE: render/tests/test-normalizeChildren.js ================================================ "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) }) o("disallows mixed keys, ending with null", function() { o(function() { Vnode.normalizeChildren([ {key: 1}, null, ]) }).throws(TypeError) }) o("disallows mixed keys, starting with null", function() { o(function() { Vnode.normalizeChildren([ null, {key: 2}, ]) }).throws(TypeError) }) o("disallows mixed keys, ending with undefined", function() { o(function() { Vnode.normalizeChildren([ {key: 1}, undefined, ]) }).throws(TypeError) }) o("disallows mixed keys, starting with undefined", function() { o(function() { Vnode.normalizeChildren([ undefined, {key: 2}, ]) }).throws(TypeError) }) o("disallows mixed keys, ending with false", function() { o(function() { Vnode.normalizeChildren([ {key: 1}, false, ]) }).throws(TypeError) }) o("disallows mixed keys, starting with false", function() { o(function() { Vnode.normalizeChildren([ false, {key: 2}, ]) }).throws(TypeError) }) o("disallows mixed keys, ending with true", function() { o(function() { Vnode.normalizeChildren([ {key: 1}, true, ]) }).throws(TypeError) }) o("disallows mixed keys, starting with true", function() { o(function() { Vnode.normalizeChildren([ true, {key: 2}, ]) }).throws(TypeError) }) }) ================================================ FILE: render/tests/test-normalizeComponentChildren.js ================================================ "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") }) }) }) ================================================ FILE: render/tests/test-onbeforeremove.js ================================================ "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() }) }) o("handles thenable objecs (#2592)", function(done) { var remove = function() {return {then: function(resolve) {resolve()}}} var vnodes = m("div", {key: 1, onbeforeremove: remove}, "a") var updated = [] render(root, vnodes) render(root, updated) o(root.childNodes.length).equals(1) o(root.firstChild.firstChild.nodeValue).equals("a") callAsync(function() { o(root.childNodes.length).equals(0) 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() }) }) }) }) }) }) ================================================ FILE: render/tests/test-onbeforeupdate.js ================================================ "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) }) }) }) ================================================ FILE: render/tests/test-oncreate.js ================================================ "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) }) }) ================================================ FILE: render/tests/test-oninit.js ================================================ "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) }) }) ================================================ FILE: render/tests/test-onremove.js ================================================ "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 callAsync = require("../../test-utils/callAsync") 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 (done) { // 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 thenCB1 var thenCB2 var C = createComponent({ view({children}){return children}, onbeforeremove(){ return {then(resolve){thenCB1=resolve}} } }) 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(resolve){thenCB2=resolve}} }, 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(thenCB1).equals(undefined) o(thenCB2).equals(undefined) const hooks2 = update("2", true, false) o(root).satisfies(template([ ["A", "sync child"], ["DIV", "1"], ])) o(thenCB1).equals(undefined) o(thenCB2).equals(undefined) // Promises (micro-tasks) are processed before the callAsync callback. callAsync(() => { o(typeof thenCB1).equals("function") o(typeof thenCB2).equals("function") var original1 = thenCB1 var original2 = thenCB2 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(thenCB1).equals(original1) o(thenCB2).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(thenCB1).equals(original1) o(thenCB2).equals(original2) const hooks5 = update("5", true, true) o(root).satisfies(template([ ["DIV", "1"], ["A", "sync child"], ["DIV", "5"], ])) o(thenCB1).equals(original1) o(thenCB2).equals(original2) o(hooks1.removeAsyncChild.callCount).equals(0) thenCB1() o(hooks1.removeAsyncChild.callCount).equals(0) callAsync(() => { o(hooks1.removeAsyncChild.callCount).equals(0) thenCB2() o(hooks1.removeAsyncChild.callCount).equals(0) callAsync(() => { o(hooks1.removeAsyncChild.callCount).equals(1) o(root).satisfies(template([ ["A", "sync child"], ["DIV", "5"], ])) o(thenCB1).equals(original1) o(thenCB2).equals(original2) const hooks6 = update("6", true, true) o(root).satisfies(template([ ["A", "sync child"], ["DIV", "6"], ])) o(thenCB1).equals(original1) o(thenCB2).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) done() }) }) }) }) }) }) }) ================================================ FILE: render/tests/test-onupdate.js ================================================ "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) }) }) ================================================ FILE: render/tests/test-render-hyperscript-integration.js ================================================ "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") }) }) }) }) ================================================ FILE: render/tests/test-render.js ================================================ "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", ]) }) }) ================================================ FILE: render/tests/test-textContent.js ================================================ "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]) }) }) ================================================ FILE: render/tests/test-trust.js ================================================ "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("") }) }) ================================================ FILE: render/tests/test-updateElement.js ================================================ "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("use style property setter only when cameCase keys", function() { var spySetProperty = o.spy() var spyRemoveProperty = o.spy() var spyDashed1 = o.spy() var spyDashed2 = o.spy() var spyDashed3 = o.spy() var spyCamelCase1 = o.spy() var spyCamelCase2 = o.spy() render(root, m("a")) var el = root.firstChild el.style.setProperty = spySetProperty el.style.removeProperty = spyRemoveProperty Object.defineProperties(el.style, { /* eslint-disable accessor-pairs */ "background-color": {set: spyDashed1}, "-webkit-border-radius": {set: spyDashed2}, "--foo": {set: spyDashed3}, backgroundColor: {set: spyCamelCase1}, color: {set: spyCamelCase2} /* eslint-enable accessor-pairs */ }) // sets dashed properties render(root, m("a", { style: { "background-color": "red", "-webkit-border-radius": "10px", "--foo": "bar" } })) o(spySetProperty.callCount).equals(3) o(spySetProperty.calls[0].args).deepEquals(["background-color", "red"]) o(spySetProperty.calls[1].args).deepEquals(["-webkit-border-radius", "10px"]) o(spySetProperty.calls[2].args).deepEquals(["--foo", "bar"]) // sets camelCase properties and removes dashed properties render(root, m("a", { style: { backgroundColor: "green", color: "red", } })) o(spyCamelCase1.callCount).equals(1) o(spyCamelCase2.callCount).equals(1) o(spyCamelCase1.calls[0].args).deepEquals(["green"]) o(spyCamelCase2.calls[0].args).deepEquals(["red"]) o(spyRemoveProperty.callCount).equals(3) o(spyRemoveProperty.calls[0].args).deepEquals(["background-color"]) o(spyRemoveProperty.calls[1].args).deepEquals(["-webkit-border-radius"]) o(spyRemoveProperty.calls[2].args).deepEquals(["--foo"]) // updates "color" and removes "backgroundColor" render(root, m("a", {style: {color: "blue"}})) o(spyCamelCase1.callCount).equals(2) // set and remove o(spyCamelCase2.callCount).equals(2) // set and update o(spyCamelCase1.calls[1].args).deepEquals([""]) o(spyCamelCase2.calls[1].args).deepEquals(["blue"]) // unchanged by camelCase properties o(spySetProperty.callCount).equals(3) o(spyRemoveProperty.callCount).equals(3) // never calls dashed property setter o(spyDashed1.callCount).equals(0) o(spyDashed2.callCount).equals(0) o(spyDashed3.callCount).equals(0) }) 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 }) o.spec("element node with `is` attribute", function() { o("recreate element node with `is` attribute (set `is`)", function() { var vnode = m("a") var updated = m("a", {is: "bar"}) render(root, vnode) render(root, updated) o(vnode.dom).notEquals(root.firstChild) o(updated.dom).equals(root.firstChild) o(updated.dom.nodeName).equals("A") o(updated.dom.getAttribute("is")).equals("bar") }) o("recreate element node without `is` attribute (remove `is`)", function() { var vnode = m("a", {is: "foo"}) var updated = m("a") render(root, vnode) render(root, updated) o(vnode.dom).notEquals(root.firstChild) o(updated.dom).equals(root.firstChild) o(updated.dom.nodeName).equals("A") o(updated.dom.getAttribute("is")).equals(null) }) o("recreate element node with `is` attribute (same tag, different `is`)", function() { var vnode = m("a", {is: "foo"}) var updated = m("a", {is: "bar"}) render(root, vnode) render(root, updated) o(vnode.dom).notEquals(root.firstChild) o(updated.dom).equals(root.firstChild) o(updated.dom.nodeName).equals("A") o(updated.dom.getAttribute("is")).equals("bar") }) o("recreate element node with `is` attribute (different tag, same `is`)", function() { var vnode = m("a", {is: "foo"}) var updated = m("b", {is: "foo"}) render(root, vnode) render(root, updated) o(vnode.dom).notEquals(root.firstChild) o(updated.dom).equals(root.firstChild) o(updated.dom.nodeName).equals("B") o(updated.dom.getAttribute("is")).equals("foo") }) o("recreate element node with `is` attribute (different tag, different `is`)", function() { var vnode = m("a", {is: "foo"}) var updated = m("b", {is: "bar"}) render(root, vnode) render(root, updated) o(vnode.dom).notEquals(root.firstChild) o(updated.dom).equals(root.firstChild) o(updated.dom.nodeName).equals("B") o(updated.dom.getAttribute("is")).equals("bar") }) o("keep element node with `is` attribute (same tag, same `is`)", function() { var vnode = m("a", {is: "foo"}) var updated = m("a", {is: "foo"}, "x") render(root, vnode) render(root, updated) o(vnode.dom).equals(root.firstChild) o(updated.dom).equals(root.firstChild) o(updated.dom.nodeName).equals("A") o(updated.dom.getAttribute("is")).equals("foo") o(updated.dom.firstChild.nodeValue).equals("x") }) o("recreate element node with `is` attribute (set `is`, CSS selector)", function() { var vnode = m("a") var updated = m("a[is=bar]") render(root, vnode) render(root, updated) o(vnode.dom).notEquals(root.firstChild) o(updated.dom).equals(root.firstChild) o(updated.dom.nodeName).equals("A") o(updated.dom.getAttribute("is")).equals("bar") }) o("recreate element node without `is` attribute (remove `is`, CSS selector)", function() { var vnode = m("a[is=foo]") var updated = m("a") render(root, vnode) render(root, updated) o(vnode.dom).notEquals(root.firstChild) o(updated.dom).equals(root.firstChild) o(updated.dom.nodeName).equals("A") o(updated.dom.getAttribute("is")).equals(null) }) o("recreate element node with `is` attribute (same tag, different `is`, CSS selector)", function() { var vnode = m("a[is=foo]") var updated = m("a[is=bar]") render(root, vnode) render(root, updated) o(vnode.dom).notEquals(root.firstChild) o(updated.dom).equals(root.firstChild) o(updated.dom.nodeName).equals("A") o(updated.dom.getAttribute("is")).equals("bar") }) o("recreate element node with `is` attribute (different tag, same `is`, CSS selector)", function() { var vnode = m("a[is=foo]") var updated = m("b[is=foo]") render(root, vnode) render(root, updated) o(vnode.dom).notEquals(root.firstChild) o(updated.dom).equals(root.firstChild) o(updated.dom.nodeName).equals("B") o(updated.dom.getAttribute("is")).equals("foo") }) o("recreate element node with `is` attribute (different tag, different `is`, CSS selector)", function() { var vnode = m("a[is=foo]") var updated = m("b[is=bar]") render(root, vnode) render(root, updated) o(vnode.dom).notEquals(root.firstChild) o(updated.dom).equals(root.firstChild) o(updated.dom.nodeName).equals("B") o(updated.dom.getAttribute("is")).equals("bar") }) o("keep element node with `is` attribute (same tag, same `is`, CSS selector)", function() { var vnode = m("a[is=foo]") var updated = m("a[is=foo]", "x") render(root, vnode) render(root, updated) o(vnode.dom).equals(root.firstChild) o(updated.dom).equals(root.firstChild) o(updated.dom.nodeName).equals("A") o(updated.dom.getAttribute("is")).equals("foo") o(updated.dom.firstChild.nodeValue).equals("x") }) o("keep element node with `is` attribute (same tag, same `is`, from attrs to CSS selector)", function() { var vnode = m("a", {is: "foo"}) var updated = m("a[is=foo]", "x") render(root, vnode) render(root, updated) o(vnode.dom).equals(root.firstChild) o(updated.dom).equals(root.firstChild) o(updated.dom.nodeName).equals("A") o(updated.dom.getAttribute("is")).equals("foo") o(updated.dom.firstChild.nodeValue).equals("x") }) o("keep element node with `is` attribute (same tag, same `is`, from CSS selector to attrs)", function() { var vnode = m("a[is=foo]") var updated = m("a", {is: "foo"}, "x") render(root, vnode) render(root, updated) o(vnode.dom).equals(root.firstChild) o(updated.dom).equals(root.firstChild) o(updated.dom.nodeName).equals("A") o(updated.dom.getAttribute("is")).equals("foo") o(updated.dom.firstChild.nodeValue).equals("x") }) }) }) ================================================ FILE: render/tests/test-updateFragment.js ================================================ "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(updated.domSize).equals(1) }) 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(updated.domSize).equals(1) }) 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(updated.domSize).equals(0) o(root.childNodes.length).equals(0) }) }) ================================================ FILE: render/tests/test-updateHTML.js ================================================ "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"]) }) }) ================================================ FILE: render/tests/test-updateNodes.js ================================================ "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(updated.domSize).equals(1) }) 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(updated.domSize).equals(1) }) 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[0].domSize).equals(1) 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[0].domSize).equals(2) 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) }) o("update keyed element vnodes with another tag (#3059)", function() { var e = function(k) {return m(k, {key: k})} // element vnode var p = function(k) {return m(k + "p", {key: k})} // element vnode (another tag) const o1 = [e("k1"),e("k2"),e("k3"),e("k4")] const v1 = [p("k2"),e("k4"),e("k3")] // create render(root, v1) o(Array.from(root.childNodes).map(function(n) {return n.nodeName})).deepEquals(["K2P", "K4", "K3"]) // update render(root, []) render(root, o1) render(root, v1) o(Array.from(root.childNodes).map(function(n) {return n.nodeName})).deepEquals(["K2P", "K4", "K3"]) }) o("update keyed element vnodes with dom and keyed fragment vnodes without dom (1) (#3059)", function() { o(function() { var e = function(k) {return m(k, {key: k})} // element vnode (with dom) var f = function(k) {return m("[", {key: k})} // fragment vnode (without dom) var o1 = [f("k1"),e("k2")] var v1 = [e("k1"),e("a"),f("k2")] render(root, o1) render(root, v1) o(Array.from(root.childNodes).map(function(n) {return n.nodeName})).deepEquals(["K1", "A"]) }).notThrows(Error) }) o("update keyed element vnodes with dom and keyed fragment vnodes without dom (2) (#3059)", function() { o(function() { var e = function(k) {return m(k, {key: k})} // element vnode (with dom) var f = function(k) {return m("[", {key: k})} // fragment vnode (without dom) var o1 = [f("k1"),f("k2"),e("k3")] var v1 = [e("k1"),f("k3"),e("k2")] render(root, o1) render(root, v1) o(Array.from(root.childNodes).map(function(n) {return n.nodeName})).deepEquals(["K1", "K2"]) }).notThrows(Error) }) 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) } }) }) }) }) ================================================ FILE: render/tests/test-updateNodesFuzzer.js ================================================ "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--) { const test = fuzzTest(c.delMax, c.movMax, c.insMax) const id = i++ o(id + ": " + 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})})) // FIXME: This does not take into account the "swaps and list reversals" heuristic in updateNodes(). // Here, we’re checking whether the number of node moves matches the theoretical value derived from the LIS. // However, in updateNodes(), when patterns such as swaps or reversed lists are detected, // nodes are moved before the LIS-based reordering is applied. // Once these heuristic moves occur, the actual number of moves no longer matches the LIS-based theoretical value. // 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) }) o(id + ": including tag changes", function() { // change some tags before and after the update var list = test.list.map(function(x){return m(x + (Math.random() > 0.5 ? "_" : ""), {key: x})}) var updated = test.updated.map(function(x){return m(x, {key: x})}) var str = list.map(function(v) {return v.tag}).join() + " -> " + updated.map(function(v) {return v.tag}).join() render(root, list) render(root, updated) o([].map.call(root.childNodes, function(n){return n.nodeName.toLowerCase()})).deepEquals(test.updated)(str) }) o(id + ": including empty fragments (without dom)", function() { // change some vnodes to empty fragments without DOM before and after the update var list = test.list.map(function(x){return m(Math.random() > 0.5 ? x : "[", {key: x})}) var updated = test.updated.map(function(x){return m(Math.random() > 0.5 ? x : "[", {key: x})}) var expected = updated.map(function(v){return v.tag}).filter(function(x){return x !== "["}) var str = list.map(function(v) {return v.tag + "." + v.key}).join() + " -> " + updated.map(function(v) {return v.tag + "." + v.key}).join() render(root, list) render(root, updated) o([].map.call(root.childNodes, function(n){return n.nodeName.toLowerCase()})).deepEquals(expected)(str) }) } }) }) // 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) } ================================================ FILE: render/tests/test-updateText.js ================================================ "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") }) }) ================================================ FILE: render/trust.js ================================================ "use strict" var Vnode = require("../render/vnode") module.exports = function(html) { if (html == null) html = "" return Vnode("<", undefined, undefined, html, undefined, undefined) } ================================================ FILE: render/vnode.js ================================================ "use strict" function Vnode(tag, key, attrs, children, text, dom) { return {tag: tag, key: key, attrs: attrs, children: children, text: text, dom: dom, is: undefined, 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) { // Preallocate the array length (initially holey) and fill every index immediately in order. // Benchmarking shows better performance on V8. var children = new Array(input.length) // Count the number of keyed normalized vnodes for consistency check. // Note: this is a perf-sensitive check. // Fun fact: merging the loop like this is somehow faster than splitting // the check within updateNodes(), noticeably so. var numKeyed = 0 for (var i = 0; i < input.length; i++) { children[i] = Vnode.normalize(input[i]) if (children[i] !== null && children[i].key != null) numKeyed++ } if (numKeyed !== 0 && numKeyed !== input.length) { throw new TypeError(children.includes(null) ? "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." ) } return children } module.exports = Vnode ================================================ FILE: render.js ================================================ "use strict" module.exports = require("./render/render")() ================================================ FILE: request/request.js ================================================ "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 } } } } ================================================ FILE: request/tests/test-request.js ================================================ "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. } }) ================================================ FILE: request.js ================================================ "use strict" var mountRedraw = require("./mount-redraw") module.exports = require("./request/request")(typeof window !== "undefined" ? window : null, mountRedraw.redraw) ================================================ FILE: route.js ================================================ "use strict" var mountRedraw = require("./mount-redraw") module.exports = require("./api/router")(typeof window !== "undefined" ? window : null, mountRedraw) ================================================ FILE: scripts/.eslintrc.js ================================================ "use strict" module.exports = { "extends": "../.eslintrc.js", "env": { "browser": null, "node": true, "es2022": true, }, "parserOptions": { "ecmaVersion": 2022, }, "rules": { "no-process-env": "off", }, }; ================================================ FILE: scripts/_bundler-impl.js ================================================ "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)) { if (!bindings.has(binding)) 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 regexp literals // Note: This regexp, while it doesn't technically capture all cases a regexp could appear, should hopefully work for now. const regexpLiteral = /([=({[](?:[\s\u2028\u2029]|\/\/.*?[\r\n\u2028\u2029]|\/\*[\s\S]*?\*\/)*)(\/(?:[^\\\/[\r\n\u2028\u2029]|\\[^\r\n\u2028\u2029]|\[(?:[^\]\\\r\n\u2028\u2029]|\\[^\r\n\u2028\u2029])*\])+\/[$\p{ID_Continue}]*)/ug code = code.replace(regexpLiteral, (match, pre, literal) => { const fixed = literal.replace(variables, (match) => match.replace(/\d+$/, "")) return pre + fixed }) //fix props const props = new RegExp(`(\\.\\.)?((?:[^:]\\/\\/.*)?\\.\\s*)(${candidates})|([\\{,]\\s*)(${candidates})(\\s*:)`, "gm") code = code.replace(props, (match, dotdot, dot, a, pre, b, post) => { // Don't do anything because dot was matched in a comment if (dot && dot.indexOf("//") === 1) return match // Don't do anything because dot is a part of spread syntax or destructuring if (dotdot) return match if (dot) return dot + a.replace(/\d+$/, "") return pre + b.replace(/\d+$/, "") + post }) // fix comment‑only lines const commentOnlyLines = /^(?:[ \t]*\/\/[^\r\n]*|[ \t]*\/\*[\s\S]*?\*\/[ \t]*)\r?$/gm code = code.replace(commentOnlyLines, (comment) => comment.replace(variables, (match) => match.replace(/\d+$/, "")) ) 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 } ================================================ FILE: scripts/bundler-readme.md ================================================ # 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. ================================================ FILE: scripts/bundler.js ================================================ "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...") // Terser's "conditionals" and "reduce_funcs" options seem to degrade performance. So, disable them. const minified = await Terser.minify(original, {compress: {conditionals: false, reduce_funcs: false}, mangle: true}) 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) ================================================ FILE: scripts/minify-stream.js ================================================ #!/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 = await 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() ================================================ FILE: scripts/set-versioned-branch.sh ================================================ #!/usr/bin/env bash set -euo pipefail base="$1" if [[ -z "$base" ]]; then echo '::error::Base branch is missing. Invoke as `bash scripts/set-versioned-branch.sh BASE' >&2 exit 1 fi major=$(node -pe 'require("./package.json").version.replace(/\..*$/,"")') # Can't do a force push due to branch protection rules. git checkout "${base}" git checkout -B "${base}-v${major}" git push --force origin "${base}-v${major}" ================================================ FILE: scripts/tests/test-bundler.js ================================================ "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 regexp literals", async () => { await setup({ "a.js": 'var b = require("./b")\nvar c = require("./c")', "b.js": "var b = /b/\nvar g = 0\nmodule.exports = function() {return b}", "c.js": "var b =\n\t/ b \\/ \\/ [a-b]/g\nvar d = b/b\nmodule.exports = function() {return b}", }) o(await bundle(p("a.js"))).equals(";(function() {\nvar b0 = /b/\nvar g = 0\nvar b = function() {return b0}\nvar b1 =\n\t/ b \\/ \\/ [a-b]/g\nvar d = b1/b1\nvar c = function() {return b1}\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}());") }) o.spec("fix comments", () => { o("fix /* */ comments", async () => { await setup({ "a.js": 'var b = require("./b")\nvar c = require("./c")', "b.js": "var a = 1\nmodule.exports = a", "c.js": "var a = 2\n/* a */\nmodule.exports = a", }) o(await bundle(p("a.js"))).equals(";(function() {\nvar a = 1\nvar b = a\nvar a0 = 2\n/* a */\nvar c = a0\n}());") }) o("fix // comments", async () => { await setup({ "a.js": 'var b = require("./b")\nvar c = require("./c")', "b.js": "var a = 1\nmodule.exports = a", "c.js": "var a = 2\n// a\nmodule.exports = a", }) o(await bundle(p("a.js"))).equals(";(function() {\nvar a = 1\nvar b = a\nvar a0 = 2\n// a\nvar c = a0\n}());") }) o("fix multi-line /* */ comments", async () => { await setup({ "a.js": 'var b = require("./b")\nvar c = require("./c")', "b.js": "var a = 1\nmodule.exports = a", "c.js": "var a = 2\n/* \na */\nmodule.exports = a", }) o(await bundle(p("a.js"))).equals(";(function() {\nvar a = 1\nvar b = a\nvar a0 = 2\n/* \na */\nvar c = a0\n}());") }) o("does not fix trailing /* */ comments", async () => { await setup({ "a.js": 'var b = require("./b")\nvar c = require("./c")', "b.js": "var a = 1\nmodule.exports = a", "c.js": "var a = 2/* a */\nmodule.exports = a", }) o(await bundle(p("a.js"))).equals(";(function() {\nvar a = 1\nvar b = a\nvar a0 = 2/* a0 */\nvar c = a0\n}());") }) o("does not fix trailing // comments", async () => { await setup({ "a.js": 'var b = require("./b")\nvar c = require("./c")', "b.js": "var a = 1\nmodule.exports = a", "c.js": "var a = 2// a\nmodule.exports = a", }) o(await bundle(p("a.js"))).equals(";(function() {\nvar a = 1\nvar b = a\nvar a0 = 2// a0\nvar c = a0\n}());") }) o("does not fix trailing multi-line /* */ comments", async () => { await setup({ "a.js": 'var b = require("./b")\nvar c = require("./c")', "b.js": "var a = 1\nmodule.exports = a", "c.js": "var a = 2/* \na */\nmodule.exports = a", }) o(await bundle(p("a.js"))).equals(";(function() {\nvar a = 1\nvar b = a\nvar a0 = 2/* \na0 */\nvar c = a0\n}());") }) }) o("prevents double suffixes (mountRedraw00)", async () => { await setup({ // /index.js (request(b), mount-redraw(z), route(c)) "a.js": 'var b = require("./b")\nvar z = require("./z")\nvar c = require("./c")', // /request.js "b.js": 'var z = require("./z")\nmodule.exports = require("./p")(z)', // /route.js "c.js": 'var z = require("./z")\nmodule.exports = require("./q")(z)', // /request/request.js "p.js": "module.exports = function(z){}", // /api/router.js "q.js": "module.exports = function(z){}", // /mount-redraw.js "z.js": "module.exports = {}", }) // check that the argument z2 is not z00 o(await bundle(p("a.js"))).equals(";(function() {\nvar z0 = {}\nvar _1 = function(z1){}\nvar b = _1(z0)\nvar z = z0\nvar _5 = function(z2){}\nvar c = _5(z)\n}());") }) o.spec("spread syntax and destructuring (...)", () => { o("rest parameter", async () => { await setup({ "a.js": 'var b = require("./b")\nvar c = require("./c")', "b.js": "var a = 1\nmodule.exports = a", "c.js": "function f(d, ...a){}\nmodule.exports = f", }) o(await bundle(p("a.js"))).equals(";(function() {\nvar a = 1\nvar b = a\nfunction f(d, ...a0){}\nvar c = f\n}());") }) o("function call", async () => { await setup({ "a.js": 'var b = require("./b")\nvar c = require("./c")', "b.js": "var a = 1\nmodule.exports = a", "c.js": "var a = [1, 2, 3]\nvar d = f(...a)\nmodule.exports = d", }) o(await bundle(p("a.js"))).equals(";(function() {\nvar a = 1\nvar b = a\nvar a0 = [1, 2, 3]\nvar d = f(...a0)\nvar c = d\n}());") }) o("new", async () => { await setup({ "a.js": 'var b = require("./b")\nvar c = require("./c")', "b.js": "var a = 1\nmodule.exports = a", "c.js": "var a = [1, 2, 3]\nvar d = new f(...a)\nmodule.exports = d", }) o(await bundle(p("a.js"))).equals(";(function() {\nvar a = 1\nvar b = a\nvar a0 = [1, 2, 3]\nvar d = new f(...a0)\nvar c = d\n}());") }) o("array spread", async () => { await setup({ "a.js": 'var b = require("./b")\nvar c = require("./c")', "b.js": "var a = 1\nmodule.exports = a", "c.js": "var a = [1, 2, 3]\nvar arr = [...a]\nmodule.exports = arr", }) o(await bundle(p("a.js"))).equals(";(function() {\nvar a = 1\nvar b = a\nvar a0 = [1, 2, 3]\nvar arr = [...a0]\nvar c = arr\n}());") }) o("array spread (merge)", async () => { await setup({ "a.js": 'var b = require("./b")\nvar c = require("./c")', "b.js": "var a = 1\nmodule.exports = a", "c.js": "var a = [1, 2, 3]\nvar arr = [0, ...a, 4]\nmodule.exports = arr", }) o(await bundle(p("a.js"))).equals(";(function() {\nvar a = 1\nvar b = a\nvar a0 = [1, 2, 3]\nvar arr = [0, ...a0, 4]\nvar c = arr\n}());") }) o("array destructuring", async () => { await setup({ "a.js": 'var b = require("./b")\nvar c = require("./c")', "b.js": "var a = 1\nmodule.exports = a", "c.js": "var a = [1, 2, 3]\nvar d\n[d, ...a] = a\nmodule.exports = a", }) o(await bundle(p("a.js"))).equals(";(function() {\nvar a = 1\nvar b = a\nvar a0 = [1, 2, 3]\nvar d\n[d, ...a0] = a0\nvar c = a0\n}());") }) o("object spread", async () => { await setup({ "a.js": 'var b = require("./b")\nvar c = require("./c")', "b.js": "var a = 1\nmodule.exports = a", "c.js": "var a = { p: 1, q: 2, r: 3 }\nvar d = {...a}\nmodule.exports = d", }) o(await bundle(p("a.js"))).equals(";(function() {\nvar a = 1\nvar b = a\nvar a0 = { p: 1, q: 2, r: 3 }\nvar d = {...a0}\nvar c = d\n}());") }) o("object spread (merge)", async () => { await setup({ "a.js": 'var b = require("./b")\nvar c = require("./c")', "b.js": "var a = 1\nmodule.exports = a", "c.js": "var a = { p: 1, q: 2, r: 3 }\nvar d = {o:0,...a}\nmodule.exports = d", }) o(await bundle(p("a.js"))).equals(";(function() {\nvar a = 1\nvar b = a\nvar a0 = { p: 1, q: 2, r: 3 }\nvar d = {o:0,...a0}\nvar c = d\n}());") }) o("object destructuring", async () => { await setup({ "a.js": 'var b = require("./b")\nvar c = require("./c")', "b.js": "var a = 1\nmodule.exports = a", "c.js": "var obj = { p: 1, q: 2, r: 3 }\nvar p,a\n({p,...a}=obj)\nmodule.exports = a", }) o(await bundle(p("a.js"))).equals(";(function() {\nvar a = 1\nvar b = a\nvar obj = { p: 1, q: 2, r: 3 }\nvar p,a0\n({p,...a0}=obj)\nvar c = a0\n}());") }) }) }) ================================================ FILE: stream/stream.js ================================================ /* 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} }()); ================================================ FILE: stream/tests/test-scan.js ================================================ "use strict" var o = require("ospec") var stream = require("../stream") o.spec("scan", function() { o("defaults to seed", function() { var parent = stream() var child = stream.scan(function(out, p) { return out - p }, 123, parent) o(child()).equals(123) }) o("accumulates values as expected", function() { var parent = stream() var child = stream.scan(function(arr, p) { return arr.concat(p) }, [], parent) parent(7) parent("11") parent(undefined) parent({a: 1}) var result = child() // deepEquals fails on arrays? o(result[0]).equals(7) o(result[1]).equals("11") o(result[2]).equals(undefined) o(result[3]).deepEquals({a: 1}) }) o("reducer can return SKIP to prevent child updates", function() { var count = 0 var action = stream() var store = stream.scan(function (arr, value) { switch (typeof value) { case "number": return arr.concat(value) default: return stream.SKIP } }, [], action) var child = store.map(function (p) { count++ return p }) var result action(7) action("11") action(undefined) action({a: 1}) action(8) // assures we didn't break the accumulator result = child() // check we got the expect result o(result[0]).equals(7) o(result[1]).equals(8) // check child received minimum # of updates o(count).equals(3) }) }) ================================================ FILE: stream/tests/test-scanMerge.js ================================================ "use strict" var o = require("ospec") var stream = require("../stream") o.spec("scanMerge", function() { o("defaults to seed", function() { var parent1 = stream() var parent2 = stream() var child = stream.scanMerge([ [parent1, function(out, p1) { return out + p1 }], [parent2, function(out, p2) { return out + p2 }] ], -10) o(child()).equals(-10) }) o("accumulates as expected", function() { var parent1 = stream() var parent2 = stream() var child = stream.scanMerge([ [parent1, function(out, p1) { return out + p1 }], [parent2, function(out, p2) { return out + p2 + p2 }] ], "a") parent1("b") parent2("c") parent1("b") o(child()).equals("abccb") }) }) ================================================ FILE: stream/tests/test-stream.js ================================================ "use strict" var o = require("ospec") var Stream = require("../stream") o.spec("stream", function() { o.spec("stream", function() { o("works as getter/setter", function() { var stream = Stream(1) var initialValue = stream() stream(2) var newValue = stream() o(initialValue).equals(1) o(newValue).equals(2) }) o("has undefined value by default", function() { var stream = Stream() o(stream()).equals(undefined) }) o("can update to undefined", function() { var stream = Stream(1) stream(undefined) o(stream()).equals(undefined) }) o("can be stream of streams", function() { var stream = Stream(Stream(1)) o(stream()()).equals(1) }) o("can SKIP", function() { var a = Stream(2) var b = a.map(function(value) { return value === 5 ? Stream.SKIP : value }) a(5) 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]) stream(2) o(doubled()).equals(4) }) o("transforms default value", function() { var stream = Stream(2) var doubled = Stream.combine(function(s) {return s() * 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]) s1(2) s2(3) o(added()).equals(5) }) 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]) 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]) s2(3) o(added()).equals(5) }) 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) { count++ return b() + c() }, [b, c]) a(3) o(d()).equals(15) o(count).equals(1) }) 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) { count++ return b() + c() }, [b, c]) o(d()).equals(15) o(count).equals(1) }) 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 d = c.map(function(x){return x}) var e = Stream.combine(function(x) {return x()}, [d]) var f = Stream.combine(function(b, e) { count++ return b() + e() }, [b, e]) o(f()).equals(15) o(count).equals(1) }) o("combine lists only changed upstreams in last arg", function() { var streams = [] var a = Stream() var b = Stream() Stream.combine(function(a, b, changed) { streams = changed }, [a, b]) a(3) b(5) o(streams.length).equals(2) o(streams[0]).equals(a) o(streams[1]).equals(b) }) o("combine continues with ended streams", function() { var a = Stream() var b = Stream() var combined = Stream.combine(function(a, b) { return a() + b() }, [a, b]) a(3) a.end(true) b(5) o(combined()).equals(8) }) o("combine lists only changed upstreams in last arg with default value", function() { var streams = [] var a = Stream(3) var b = Stream(5) Stream.combine(function(a, b, changed) { streams = changed }, [a, b]) a(7) o(streams.length).equals(1) o(streams[0]).equals(a) }) o("combine can return undefined", function() { var a = Stream(1) var b = Stream.combine(function() { return undefined }, [a]) o(b()).equals(undefined) }) o("combine can return stream", function() { var a = Stream(1) var b = Stream.combine(function() { return Stream(2) }, [a]) o(b()()).equals(2) }) o("combine can return pending stream", function() { var a = Stream(1) var b = Stream.combine(function() { return Stream() }, [a]) o(b()()).equals(undefined) }) o("combine can skip", function() { var count = 0 var a = Stream(1) var b = Stream.combine(function() { return Stream.SKIP }, [a])["fantasy-land/map"](function() { count++ return 1 }) o(b()).equals(undefined) o(count).equals(0) }) o("combine can conditionaly skip", function() { var count = 0 var skip = false var a = Stream(1) var b = Stream.combine(function(a) { if (skip) { return Stream.SKIP } return a() }, [a])["fantasy-land/map"](function(a) { count++ return a }) o(b()).equals(1) o(count).equals(1) skip = true count = 0 a(2) o(b()).equals(1) o(count).equals(0) }) o("combine will throw with a helpful error if given non-stream values", function () { var spy = o.spy() var a = Stream(1) var thrown = null; try { Stream.combine(spy, [a, ""]) } catch (e) { thrown = e } o(thrown).notEquals(null) o(thrown.constructor === TypeError).equals(false) o(spy.callCount).equals(0) }) o("combine callback not called when child stream was ended", function () { var spy = o.spy() var a = Stream(1) var b = Stream(2) var mapped = Stream.combine(spy, [a, b]) mapped.end(true) a(11) o(spy.callCount).equals(1) }) }) o.spec("lift", function() { o("transforms value", function() { var stream = Stream() var doubled = Stream.lift(function(s) {return s * 2}, stream) stream(2) o(doubled()).equals(4) }) o("transforms default value", function() { var stream = Stream(2) var doubled = Stream.lift(function(s) {return s * 2}, stream) o(doubled()).equals(4) }) o("transforms multiple values", function() { var s1 = Stream() var s2 = Stream() var added = Stream.lift(function(s1, s2) {return s1 + s2}, s1, s2) s1(2) s2(3) o(added()).equals(5) }) o("transforms multiple default values", function() { var s1 = Stream(2) var s2 = Stream(3) var added = Stream.lift(function(s1, s2) {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.lift(function(s1, s2) {return s1 + s2}, s1, s2) s2(3) o(added()).equals(5) }) o("lifts atomically", function() { var count = 0 var a = Stream() var b = Stream.lift(function(a) {return a * 2}, a) var c = Stream.lift(function(a) {return a * a}, a) var d = Stream.lift(function(b, c) { count++ return b + c }, b, c) a(3) o(d()).equals(15) o(count).equals(1) }) o("lifts default value atomically", function() { var count = 0 var a = Stream(3) var b = Stream.lift(function(a) {return a * 2}, a) var c = Stream.lift(function(a) {return a * a}, a) var d = Stream.lift(function(b, c) { count++ return b + c }, b, c) o(d()).equals(15) o(count).equals(1) }) o("lift can return undefined", function() { var a = Stream(1) var b = Stream.lift(function() { return undefined }, a) o(b()).equals(undefined) }) o("lift can return stream", function() { var a = Stream(1) var b = Stream.lift(function() { return Stream(2) }, a) o(b()()).equals(2) }) o("lift can return pending stream", function() { var a = Stream(1) var b = Stream.lift(function() { return Stream() }, a) o(b()()).equals(undefined) }) o("lift can halt", function() { var count = 0 var a = Stream(1) var b = Stream.lift(function() { return Stream.SKIP }, a)["fantasy-land/map"](function() { count++ return 1 }) o(b()).equals(undefined) o(count).equals(0) }) o("lift will throw with a helpful error if given non-stream values", function () { var spy = o.spy() var a = Stream(1) var thrown = null; try { Stream.lift(spy, a, "") } catch (e) { thrown = e } o(thrown).notEquals(null) o(thrown.constructor === TypeError).equals(false) o(spy.callCount).equals(0) }) }) o.spec("merge", function() { o("transforms an array of streams to an array of values", function() { var all = Stream.merge([ Stream(10), Stream("20"), Stream({value: 30}), ]) o(all()).deepEquals([10, "20", {value: 30}]) }) o("remains pending until all streams are active", function() { var straggler = Stream() var all = Stream.merge([ Stream(10), Stream("20"), straggler, ]) o(all()).equals(undefined) straggler(30) o(all()).deepEquals([10, "20", 30]) }) o("calls run callback after all parents are active", function() { var value = 0 var id = function(value) {return value} var a = Stream() var b = Stream() Stream.merge([a.map(id), b.map(id)]).map(function(data) { value = data[0] + data[1] return undefined }) a(1) b(2) o(value).equals(3) a(3) b(4) o(value).equals(7) }) }) o.spec("end", function() { o("end stream works", function() { var stream = Stream() var doubled = Stream.combine(function(stream) {return stream() * 2}, [stream]) stream.end(true) stream(3) o(doubled()).equals(undefined) }) o("end stream works with default value", function() { var stream = Stream(2) var doubled = Stream.combine(function(stream) {return stream() * 2}, [stream]) stream.end(true) stream(3) o(doubled()).equals(4) }) o("cannot add downstream to ended stream", function() { var stream = Stream(2) stream.end(true) var doubled = Stream.combine(function(stream) {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]) doubled.end(true) stream(4) o(doubled()).equals(4) }) o("end stream can be mapped to", function() { var stream = Stream() var spy = o.spy() stream.end.map(spy) o(spy.callCount).equals(0) stream.end(true) o(spy.callCount).equals(1) }) o("ended stream works like a container", function() { var stream = Stream(1) stream.end(true) stream(2) o(stream()).equals(2) }) // https://github.com/MithrilJS/mithril.js/issues/2601 o("ended stream doesn't affect emit of subsequent streams", function() { const refreshing = Stream() const o1Received = [] const waitingReceived = [] const o2Received = [] const o3Received = [] const o4Received = [] /* eslint-disable array-callback-return */ refreshing.map(function(v) { o1Received.push(v) }) const waiting = refreshing.map(function(v) { waitingReceived.push(v) if (v === false) { waiting.end(true) } }) refreshing.map(function(v) { o2Received.push(v) }) refreshing.map(function(v) { o3Received.push(v) }) refreshing.map(function(v) { o4Received.push(v) }) /* eslint-enable array-callback-return */ refreshing(true) refreshing(false) refreshing("more") o(o1Received).deepEquals([true, false, "more"]) o(waitingReceived).deepEquals([true, false]) o(o2Received).deepEquals([true, false, "more"]) o(o3Received).deepEquals([true, false, "more"]) o(o4Received).deepEquals([true, false, "more"]) }) }) o.spec("toJSON", function() { o("works", function() { o(Stream(1).toJSON()).equals(1) o(Stream("a").toJSON()).equals("a") o(Stream(true).toJSON()).equals(true) o(Stream(null).toJSON()).equals(null) o(Stream(undefined).toJSON()).equals(undefined) o(Stream({a: 1}).toJSON()).deepEquals({a: 1}) o(Stream([1, 2, 3]).toJSON()).deepEquals([1, 2, 3]) o(Stream().toJSON()).equals(undefined) o(Stream(new Date(0)).toJSON()).equals(new Date(0).toJSON()) }) o("works w/ JSON.stringify", function() { o(JSON.stringify(Stream(1))).equals(JSON.stringify(1)) o(JSON.stringify(Stream("a"))).equals(JSON.stringify("a")) o(JSON.stringify(Stream(true))).equals(JSON.stringify(true)) o(JSON.stringify(Stream(null))).equals(JSON.stringify(null)) o(JSON.stringify(Stream(undefined))).equals(JSON.stringify(undefined)) o(JSON.stringify(Stream({a: 1}))).deepEquals(JSON.stringify({a: 1})) o(JSON.stringify(Stream([1, 2, 3]))).deepEquals(JSON.stringify([1, 2, 3])) o(JSON.stringify(Stream())).equals(JSON.stringify(undefined)) o(JSON.stringify(Stream(new Date(0)))).equals(JSON.stringify(new Date(0))) }) }) o.spec("map", function() { o("works", function() { var stream = Stream() var doubled = stream["fantasy-land/map"](function(value) {return value * 2}) stream(3) o(doubled()).equals(6) }) o("works with default value", function() { var stream = Stream(3) var doubled = stream["fantasy-land/map"](function(value) {return value * 2}) o(doubled()).equals(6) }) o("works with undefined value", function() { var stream = Stream() var mapped = stream["fantasy-land/map"](function(value) {return String(value)}) stream(undefined) o(mapped()).equals("undefined") }) o("works with default undefined value", function() { var stream = Stream(undefined) var mapped = stream["fantasy-land/map"](function(value) {return String(value)}) o(mapped()).equals("undefined") }) o("works with pending stream", function() { var stream = Stream(undefined) var mapped = stream["fantasy-land/map"](function() {return Stream()}) o(mapped()()).equals(undefined) }) o("has alias", function() { var stream = Stream(undefined) o(stream["fantasy-land/map"]).equals(stream.map) }) o("mapping function is not invoked after ending", function () { var stream = Stream(undefined) var fn = o.spy() var mapped = stream.map(fn) mapped.end(true) stream(undefined) o(fn.callCount).equals(1) }) }) o.spec("ap", function() { o("works", function() { var apply = Stream(function(value) {return value * 2}) var stream = Stream(3) var applied = stream["fantasy-land/ap"](apply) o(applied()).equals(6) apply(function(value) {return value / 3}) o(applied()).equals(1) stream(9) o(applied()).equals(3) }) o("works with undefined value", function() { var apply = Stream(function(value) {return String(value)}) var stream = Stream(undefined) var applied = stream["fantasy-land/ap"](apply) o(applied()).equals("undefined") apply(function(value) {return String(value) + "a"}) o(applied()).equals("undefineda") }) }) o.spec("fantasy-land", function() { o.spec("functor", function() { o("identity", function() { var stream = Stream(3) var mapped = stream["fantasy-land/map"](function(value) {return value}) o(stream()).equals(mapped()) }) o("composition", function() { function f(x) {return x * 2} function g(x) {return x * x} var stream = Stream(3) var mapped = stream["fantasy-land/map"](function(value) {return f(g(value))}) var composed = stream["fantasy-land/map"](g)["fantasy-land/map"](f) o(mapped()).equals(18) o(mapped()).equals(composed()) }) }) o.spec("apply", function() { o("composition", function() { var a = Stream(function(value) {return value * 2}) var u = Stream(function(value) {return value * 3}) var v = Stream(5) var mapped = v["fantasy-land/ap"](u["fantasy-land/ap"](a["fantasy-land/map"](function(f) { return function(g) { return function(x) { return f(g(x)) } } }))) var composed = v["fantasy-land/ap"](u)["fantasy-land/ap"](a) o(mapped()).equals(30) o(mapped()).equals(composed()) }) }) o.spec("applicative", function() { o("identity", function() { var a = Stream["fantasy-land/of"](function(value) {return value}) var v = Stream(5) o(v["fantasy-land/ap"](a)()).equals(5) o(v["fantasy-land/ap"](a)()).equals(v()) }) o("homomorphism", function() { var a = Stream(0) var f = function(value) {return value * 2} var x = 3 o(a.constructor["fantasy-land/of"](x)["fantasy-land/ap"](a.constructor["fantasy-land/of"](f))()).equals(6) o(a.constructor["fantasy-land/of"](x)["fantasy-land/ap"](a.constructor["fantasy-land/of"](f))()).equals(a.constructor["fantasy-land/of"](f(x))()) }) o("interchange", function() { var u = Stream(function(value) {return value * 2}) var a = Stream() var y = 3 o(a.constructor["fantasy-land/of"](y)["fantasy-land/ap"](u)()).equals(6) o(a.constructor["fantasy-land/of"](y)["fantasy-land/ap"](u)()).equals(u["fantasy-land/ap"](a.constructor["fantasy-land/of"](function(f) {return f(y)}))()) }) }) }) }) ================================================ FILE: stream.js ================================================ "use strict" module.exports = require("./stream/stream") ================================================ FILE: test-utils/browserMock.js ================================================ "use strict" var pushStateMock = require("./pushStateMock") var domMock = require("./domMock") var xhrMock = require("./xhrMock") 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) return $window } ================================================ FILE: test-utils/callAsync.js ================================================ "use strict" module.exports = setTimeout ================================================ FILE: test-utils/components.js ================================================ "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 } } } ] ================================================ FILE: test-utils/domMock.js ================================================ "use strict" /* 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 - ... */ /* options: - spy:(f: Function) => Function */ module.exports = function(options) { options = options || {} var spy = options.spy || function(f){return f} var spymap = [] // 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 // care only about HTTP(S) URLs. var urlHash = "#[?!/+=&;%@.\\w_-]*" var urlQuery = "\\?[!/+=&;%@.\\w_-]*" var urlPath = "/[+~%/.\\w_-]*" var urlRelative = urlPath + "(?:" + urlQuery + ")?(?:" + urlHash + ")?" var urlDomain = "https?://[A-Za-z0-9][A-Za-z0-9.-]+[A-Za-z0-9]" var validURLRegex = new RegExp( "^" + urlDomain + "(" + urlRelative + ")?$|" + "^" + urlRelative + "$|" + "^" + urlQuery + "(?:" + urlHash + ")?$|" + "^" + urlHash + "$" ) var hasOwn = ({}.hasOwnProperty) function registerSpies(element, spies) { if(options.spy) { var i = spymap.indexOf(element) if (i === -1) { spymap.push(element, spies) } else { var existing = spymap[i + 1] for (var k in spies) existing[k] = spies[k] } } } function getSpies(element) { if (element == null || typeof element !== "object") throw new Error("Element expected") if(options.spy) return spymap[spymap.indexOf(element) + 1] } function isModernEvent(type) { return type === "transitionstart" || type === "transitionend" || type === "animationstart" || type === "animationend" } function dispatchEvent(e) { var stopped = false e.stopImmediatePropagation = function() { e.stopPropagation() stopped = true } e.currentTarget = this if (this._events[e.type] != null) { for (var i = 0; i < this._events[e.type].handlers.length; i++) { var useCapture = this._events[e.type].options[i].capture if (useCapture && e.eventPhase < 3 || !useCapture && e.eventPhase > 1) { var handler = this._events[e.type].handlers[i] if (typeof handler === "function") try {handler.call(this, e)} catch(e) {setTimeout(function(){throw e})} else try {handler.handleEvent(e)} catch(e) {setTimeout(function(){throw e})} if (stopped) return } } } // this is inaccurate. Normally the event fires in definition order, including legacy events // this would require getters/setters for each of them though and we haven't gotten around to // adding them since it would be at a high perf cost or would entail some heavy refactoring of // the mocks (prototypes instead of closures). if (e.eventPhase > 1 && typeof this["on" + e.type] === "function" && !isModernEvent(e.type)) try {this["on" + e.type](e)} catch(e) {setTimeout(function(){throw e})} } function appendChild(child) { var ancestor = this while (ancestor !== child && ancestor !== null) ancestor = ancestor.parentNode 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 } } } 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 } } } 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 * individual declarations, ignoring semicolons in strings. * * Comments are also stripped. * * @param {string} declList * @return {string[]} */ function splitDeclList(declList) { var indices = [], res = [], match // remove comments, preserving comments in strings. declList = declList.replace( /("(?:\\.|[^"\n])*"|'(?:\\.|[^'\n])*')|\/\*[\s\S]*?\*\//g, function(m, str){ return str || "" } ) /*eslint-disable no-cond-assign*/ while (match = declListTokenizer.exec(declList)) { if (match[0] === ";") indices.push(match.index) } /*eslint-enable no-cond-assign*/ for (var i = indices.length; i--;){ res.unshift(declList.slice(indices[i] + 1)) declList = declList.slice(0, indices[i]) } 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()}) } 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: { writable: true, value: function(key){ style[key] = style[camelCase(key)] = "" } }, setProperty: { writable: true, 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 } } } }, onclick: null, _events: events } 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, }) } 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 }) } 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 }) } /* 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())}, }) } /* eslint-enable radix */ 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])) } 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 }) } 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, }) } return element }, createElementNS: function(ns, tag, is) { var element = this.createElement(tag, is) element.nodeName = tag element.namespaceURI = ns 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 }, } }, createEvent: function() { return { eventPhase: 0, initEvent: function(type) {this.type = type} } }, 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 return $window } ================================================ FILE: test-utils/parseURL.js ================================================ "use strict" module.exports = function parseURL(url, root) { var data = {} var protocolIndex = url.indexOf("://") var pathnameIndex = protocolIndex > -1 ? url.indexOf("/", protocolIndex + 3) : url.indexOf("/") var searchIndex = url.indexOf("?") var hashIndex = url.indexOf("#") if ((pathnameIndex > searchIndex && searchIndex > -1) || (pathnameIndex > hashIndex && hashIndex > -1)) pathnameIndex = -1 if (searchIndex > hashIndex && hashIndex > -1) searchIndex = -1 var pathnameEnd = searchIndex > -1 ? searchIndex : hashIndex > -1 ? hashIndex : url.length if (protocolIndex > -1) { //it's a full URL if (pathnameIndex < 0) pathnameIndex = url.length var portIndex = url.indexOf(":", protocolIndex + 1) if (portIndex < 0) portIndex = pathnameIndex data.protocol = url.slice(0, protocolIndex + 1) data.hostname = url.slice(protocolIndex + 3, portIndex) data.port = url.slice(portIndex + 1, pathnameIndex) data.pathname = url.slice(pathnameIndex, pathnameEnd) || "/" } else { data.protocol = root.protocol data.hostname = root.hostname data.port = root.port if (pathnameIndex === 0) { //it's an absolute path data.pathname = url.slice(pathnameIndex, pathnameEnd) || "/" } else if (searchIndex !== 0 && hashIndex !== 0) { //it's a relative path var slashIndex = root.pathname.lastIndexOf("/") var path = slashIndex > -1 ? root.pathname.slice(0, slashIndex + 1) : "./" var normalized = url.slice(0, pathnameEnd).replace(/^\.$/, root.pathname.slice(slashIndex + 1)).replace(/^\.\//, "") var dotdot = /\/[^\/]+?\/\.{2}/g var pathname = path + normalized pathname = path + normalized while (dotdot.test(pathname)) pathname = pathname.replace(dotdot, "") pathname = pathname.replace(/\/\.\//g, "/").replace(/^(\/\.{2})+/, "") || "/" data.pathname = pathname } } var searchEnd = hashIndex > -1 ? hashIndex : url.length data.search = searchIndex > -1 ? url.slice(searchIndex, searchEnd) : "" data.hash = hashIndex > -1 ? url.slice(hashIndex) : "" return data } ================================================ FILE: test-utils/pushStateMock.js ================================================ "use strict" 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) { if (options == null) options = {} var $window = options.window || {} var protocol = options.protocol || "http:" var hostname = options.hostname || "localhost" var port = "" var pathname = "/" var search = "" var hash = "" var past = [{url: getURL(), isNew: true, state: null, title: null}], future = [] function getURL() { if (protocol === "file:") return protocol + "//" + pathname + search + hash return protocol + "//" + hostname + prefix(":", port) + pathname + search + hash } function setURL(value) { var data = parseURL(value, {protocol: protocol, hostname: hostname, port: port, pathname: pathname}) var isNew = false if (data.protocol != null && data.protocol !== protocol) protocol = data.protocol, isNew = true if (data.hostname != null && data.hostname !== hostname) hostname = data.hostname, isNew = true if (data.port != null && data.port !== port) port = data.port, isNew = true if (data.pathname != null && data.pathname !== pathname) pathname = data.pathname, isNew = true if (data.search != null && data.search !== search) search = data.search, isNew = true if (data.hash != null && data.hash !== hash) { hash = data.hash if (!isNew) { hashchange() } } return isNew } function prefix(prefix, value) { if (value === "") return "" return (value.charAt(0) !== prefix ? prefix : "") + value } function _hashchange() { if (typeof $window.onhashchange === "function") $window.onhashchange({type: "hashchange"}) } var hashchange = debouncedAsync(_hashchange) function popstate() { if (typeof $window.onpopstate === "function") $window.onpopstate({type: "popstate", state: $window.history.state}) } function unload() { if (typeof $window.onunload === "function") $window.onunload({type: "unload"}) } $window.location = { get protocol() { return protocol }, get hostname() { return hostname }, get port() { return port }, get pathname() { return pathname }, get search() { return search }, get hash() { return hash }, get origin() { if (protocol === "file:") return "null" return protocol + "//" + hostname + prefix(":", port) }, get host() { if (protocol === "file:") return "" return hostname + prefix(":", port) }, get href() { return getURL() }, set protocol(value) { throw new Error("Protocol is read-only") }, set hostname(value) { unload() past.push({url: getURL(), isNew: true}) future = [] hostname = value }, set port(value) { if (protocol === "file:") throw new Error("Port is read-only under `file://` protocol") unload() past.push({url: getURL(), isNew: true}) future = [] port = value }, set pathname(value) { if (protocol === "file:") throw new Error("Pathname is read-only under `file://` protocol") unload() past.push({url: getURL(), isNew: true}) future = [] pathname = prefix("/", value) }, set search(value) { unload() past.push({url: getURL(), isNew: true}) future = [] search = prefix("?", value) }, set hash(value) { var oldHash = hash past.push({url: getURL(), isNew: false}) future = [] hash = prefix("#", value) if (oldHash != hash) hashchange() }, set origin(value) { //origin is writable but ignored }, set host(value) { //host is writable but ignored in Chrome }, set href(value) { var url = getURL() var isNew = setURL(value) if (isNew) { setURL(url) unload() setURL(value) } past.push({url: url, isNew: isNew}) future = [] }, } $window.history = { pushState: function(state, title, url) { past.push({url: getURL(), isNew: false, state: state, title: title}) future = [] setURL(url) }, replaceState: function(state, title, url) { var entry = past[past.length - 1] entry.state = state entry.title = title setURL(url) }, back: function() { if (past.length > 1) { var entry = past.pop() if (entry.isNew) unload() future.push({url: getURL(), isNew: false, state: entry.state, title: entry.title}) setURL(entry.url) if (!entry.isNew) popstate() } }, forward: function() { var entry = future.pop() if (entry != null) { if (entry.isNew) unload() past.push({url: getURL(), isNew: false, state: entry.state, title: entry.title}) setURL(entry.url) if (!entry.isNew) popstate() } }, get state() { return past.length === 0 ? null : past[past.length - 1].state }, } $window.onpopstate = null, $window.onhashchange = null, $window.onunload = null $window.addEventListener = function (name, handler) { $window["on" + name] = handler } $window.removeEventListener = function (name, handler) { $window["on" + name] = handler } return $window } ================================================ FILE: test-utils/tests/test-browserMock.js ================================================ "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) }) }) ================================================ FILE: test-utils/tests/test-callAsync.js ================================================ "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) }) }) ================================================ FILE: test-utils/tests/test-components.js ================================================ "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)) }) }) ================================================ FILE: test-utils/tests/test-domMock.js ================================================ "use strict" var o = require("ospec") var domMock = require("../../test-utils/domMock") o.spec("domMock", function() { var $document, $window o.beforeEach(function() { $window = domMock() $document = $window.document }) o.spec("createElement", function() { o("works", function() { var node = $document.createElement("div") o(node.nodeType).equals(1) o(node.nodeName).equals("DIV") o(node.namespaceURI).equals("http://www.w3.org/1999/xhtml") o(node.parentNode).equals(null) o(node.childNodes.length).equals(0) o(node.firstChild).equals(null) o(node.nextSibling).equals(null) }) }) o.spec("createElementNS", function() { o("works", function() { var node = $document.createElementNS("http://www.w3.org/2000/svg", "svg") o(node.nodeType).equals(1) o(node.nodeName).equals("svg") o(node.namespaceURI).equals("http://www.w3.org/2000/svg") o(node.parentNode).equals(null) o(node.childNodes.length).equals(0) o(node.firstChild).equals(null) o(node.nextSibling).equals(null) }) }) o.spec("createTextNode", function() { o("works", function() { var node = $document.createTextNode("abc") o(node.nodeType).equals(3) o(node.nodeName).equals("#text") o(node.parentNode).equals(null) o(node.nodeValue).equals("abc") }) o("works w/ number", function() { var node = $document.createTextNode(123) o(node.nodeValue).equals("123") }) o("works w/ null", function() { var node = $document.createTextNode(null) o(node.nodeValue).equals("null") }) o("works w/ undefined", function() { var node = $document.createTextNode(undefined) o(node.nodeValue).equals("undefined") }) o("works w/ object", function() { var node = $document.createTextNode({}) o(node.nodeValue).equals("[object Object]") }) o("does not unescape HTML", function() { var node = $document.createTextNode("&") o(node.nodeValue).equals("&") }) o("nodeValue casts to string", function() { var node = $document.createTextNode("a") node.nodeValue = true o(node.nodeValue).equals("true") }) if (typeof Symbol === "function") { o("doesn't work with symbols", function(){ var threw = false try { $document.createTextNode(Symbol("nono")) } catch(e) { threw = true } o(threw).equals(true) }) o("symbols can't be used as nodeValue", function(){ var threw = false try { var node = $document.createTextNode("a") node.nodeValue = Symbol("nono") } catch(e) { threw = true } o(threw).equals(true) }) } }) 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") parent.appendChild(child) o(parent.childNodes.length).equals(1) o(parent.childNodes[0]).equals(child) o(parent.firstChild).equals(child) o(child.parentNode).equals(parent) }) o("moves existing", function() { var parent = $document.createElement("div") var a = $document.createElement("a") var b = $document.createElement("b") parent.appendChild(a) parent.appendChild(b) parent.appendChild(a) o(parent.childNodes.length).equals(2) o(parent.childNodes[0]).equals(b) o(parent.childNodes[1]).equals(a) o(parent.firstChild).equals(b) o(parent.firstChild.nextSibling).equals(a) o(a.parentNode).equals(parent) 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") 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") 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") 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 child = 1 try {parent.appendChild(child)} catch (e) {done()} }) }) o.spec("removeChild", function() { o("works", function() { var parent = $document.createElement("div") var child = $document.createElement("a") parent.appendChild(child) parent.removeChild(child) o(parent.childNodes.length).equals(0) o(parent.firstChild).equals(null) o(child.parentNode).equals(null) }) o("throws if not a child", function(done) { var parent = $document.createElement("div") var child = $document.createElement("a") try {parent.removeChild(child)} catch (e) {done()} }) }) o.spec("insertBefore", function() { o("works", function() { var parent = $document.createElement("div") var a = $document.createElement("a") var b = $document.createElement("b") parent.appendChild(a) parent.insertBefore(b, a) o(parent.childNodes.length).equals(2) o(parent.childNodes[0]).equals(b) o(parent.childNodes[1]).equals(a) o(parent.firstChild).equals(b) o(parent.firstChild.nextSibling).equals(a) o(a.parentNode).equals(parent) o(b.parentNode).equals(parent) }) o("moves existing", function() { var parent = $document.createElement("div") var a = $document.createElement("a") var b = $document.createElement("b") parent.appendChild(a) parent.appendChild(b) parent.insertBefore(b, a) o(parent.childNodes.length).equals(2) o(parent.childNodes[0]).equals(b) o(parent.childNodes[1]).equals(a) o(parent.firstChild).equals(b) o(parent.firstChild.nextSibling).equals(a) o(a.parentNode).equals(parent) o(b.parentNode).equals(parent) }) o("moves existing node forward but not at the end", function() { var parent = $document.createElement("div") var a = $document.createElement("a") var b = $document.createElement("b") var c = $document.createElement("c") parent.appendChild(a) parent.appendChild(b) parent.appendChild(c) parent.insertBefore(a, c) o(parent.childNodes.length).equals(3) o(parent.childNodes[0]).equals(b) o(parent.childNodes[1]).equals(a) o(parent.childNodes[2]).equals(c) o(parent.firstChild).equals(b) o(parent.firstChild.nextSibling).equals(a) o(parent.firstChild.nextSibling.nextSibling).equals(c) o(a.parentNode).equals(parent) o(b.parentNode).equals(parent) o(c.parentNode).equals(parent) }) o("removes from old parent", function() { var parent = $document.createElement("div") var source = $document.createElement("span") var a = $document.createElement("a") var b = $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") parent.appendChild(a) parent.insertBefore(b, null) o(parent.childNodes.length).equals(2) o(parent.childNodes[0]).equals(a) o(parent.childNodes[1]).equals(b) o(parent.firstChild).equals(a) o(parent.firstChild.nextSibling).equals(b) o(a.parentNode).equals(parent) }) o("throws if appended to self", function(done) { var div = $document.createElement("div") var a = $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") 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") 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") 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") 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") try {parent.insertBefore(a, b)} catch (e) {done()} }) }) o.spec("getAttribute", function() { o("works", function() { var div = $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") div.setAttributeNS("http://www.w3.org/1999/xlink", "href", "aaa") o(div.getAttribute("href")).equals("aaa") }) }) o.spec("setAttribute", function() { o("works", function() { var div = $document.createElement("div") div.setAttribute("id", "aaa") o(div.attributes["id"].value).equals("aaa") o(div.attributes["id"].nodeValue).equals("aaa") o(div.attributes["id"].namespaceURI).equals(null) }) o("works w/ number", function() { var div = $document.createElement("div") div.setAttribute("id", 123) o(div.attributes["id"].value).equals("123") }) o("works w/ null", function() { var div = $document.createElement("div") div.setAttribute("id", null) o(div.attributes["id"].value).equals("null") }) o("works w/ undefined", function() { var div = $document.createElement("div") div.setAttribute("id", undefined) o(div.attributes["id"].value).equals("undefined") }) o("works w/ object", function() { var div = $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") div.setAttribute("id", "a") div.attributes["id"].value = 123 o(div.attributes["id"].value).equals("123") div.attributes["id"].nodeValue = 456 o(div.attributes["id"].value).equals("456") }) }) o.spec("hasAttribute", function() { o("works", function() { var div = $document.createElement("div") o(div.hasAttribute("id")).equals(false) div.setAttribute("id", "aaa") o(div.hasAttribute("id")).equals(true) div.removeAttribute("id") o(div.hasAttribute("id")).equals(false) }) }) o.spec("setAttributeNS", function() { o("works", function() { var a = $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"}) o(a.attributes["href"].value).equals("/aaa") o(a.attributes["href"].namespaceURI).equals("http://www.w3.org/1999/xlink") }) o("works w/ number", function() { var a = $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"}) o(a.attributes["href"].value).equals("123") o(a.attributes["href"].namespaceURI).equals("http://www.w3.org/1999/xlink") }) o("attributes with a namespace can be querried, updated and removed with non-NS functions", function() { var a = $document.createElementNS("http://www.w3.org/2000/svg", "a") a.setAttributeNS("http://www.w3.org/1999/xlink", "href", "/aaa") o(a.hasAttribute("href")).equals(true) o(a.getAttribute("href")).equals("/aaa") a.setAttribute("href", "/bbb") o(a.href).deepEquals({baseVal: "/bbb", animVal: "/bbb"}) o(a.getAttribute("href")).equals("/bbb") o(a.attributes["href"].value).equals("/bbb") o(a.attributes["href"].namespaceURI).equals("http://www.w3.org/1999/xlink") a.removeAttribute("href") o(a.hasAttribute("href")).equals(false) o(a.getAttribute("href")).equals(null) o("href" in a.attributes).equals(false) }) }) o.spec("removeAttribute", function() { o("works", function() { var div = $document.createElement("div") div.setAttribute("id", "aaa") div.removeAttribute("id") o("id" in div.attributes).equals(false) }) }) o.spec("textContent", function() { o("works", function() { var div = $document.createElement("div") div.textContent = "aaa" o(div.childNodes.length).equals(1) o(div.firstChild.nodeType).equals(3) o(div.firstChild.nodeValue).equals("aaa") }) o("works with empty string", function() { var div = $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("focus changes activeElement", function() { var input = $document.createElement("input") $document.body.appendChild(input) input.focus() o($document.activeElement).equals(input) $document.body.removeChild(input) }) }) o.spec("style", function() { o("has style property", function() { var div = $document.createElement("div") o(typeof div.style).equals("object") }) o("setting style.cssText string works", function() { var div = $document.createElement("div") div.style.cssText = "background-color: red; border-bottom: 1px solid red;" o(div.style.backgroundColor).equals("red") o(div.style.borderBottom).equals("1px solid red") o(div.attributes.style.value).equals("background-color: red; border-bottom: 1px solid red;") }) o("removing via setting style.cssText string works", function() { var div = $document.createElement("div") div.style.cssText = "background: red;" div.style.cssText = "" o(div.style.background).equals("") o(div.attributes.style.value).equals("") }) o("the final semicolon is optional when setting style.cssText", function() { var div = $document.createElement("div") div.style.cssText = "background: red" o(div.style.background).equals("red") o(div.style.cssText).equals("background: red;") o(div.attributes.style.value).equals("background: red;") }) o("'cssText' as a property name is ignored when setting style.cssText", function(){ var div = $document.createElement("div") 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") div.style.cssText = "background: url(';'); font-family: \";\"" o(div.style.background).equals("url(';')") o(div.style.fontFamily).equals('";"') o(div.style.cssText).equals("background: url(';'); font-family: \";\";") }) o("comments in style.cssText are stripped", function(){ var div = $document.createElement("div") div.style.cssText = "/**/background/*:*/: /*>;)*/red/**/;/**/" o(div.style.background).equals("red") o(div.style.cssText).equals("background: red;") }) o("comments in strings in style.cssText are preserved", function(){ var div = $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") div.style = "background: red;" o(div.style.background).equals("red") o(div.style.cssText).equals("background: red;") }) }) o.spec("events", function() { o.spec("click", function() { var spy, div, e o.beforeEach(function() { spy = o.spy() div = $document.createElement("div") e = $document.createEvent("MouseEvents") e.initEvent("click", true, true) $document.body.appendChild(div) }) o.afterEach(function() { $document.body.removeChild(div) }) o("has onclick", function() { o("onclick" in div).equals(true) }) o("addEventListener works", function() { div.addEventListener("click", spy, false) div.dispatchEvent(e) o(spy.callCount).equals(1) o(spy.this).equals(div) o(spy.args[0].type).equals("click") o(spy.args[0].target).equals(div) }) o("removeEventListener works (bubbling phase)", function() { div.addEventListener("click", spy, false) div.removeEventListener("click", spy, false) div.dispatchEvent(e) o(spy.callCount).equals(0) }) o("removeEventListener works (capture phase)", function() { div.addEventListener("click", spy, true) div.removeEventListener("click", spy, true) div.dispatchEvent(e) o(spy.callCount).equals(0) }) o("removeEventListener is selective (bubbling phase)", function() { var other = o.spy() div.addEventListener("click", spy, false) div.addEventListener("click", other, false) div.removeEventListener("click", spy, false) div.dispatchEvent(e) o(spy.callCount).equals(0) o(other.callCount).equals(1) }) o("removeEventListener is selective (capture phase)", function() { var other = o.spy() div.addEventListener("click", spy, true) div.addEventListener("click", other, true) div.removeEventListener("click", spy, true) div.dispatchEvent(e) o(spy.callCount).equals(0) o(other.callCount).equals(1) }) o("removeEventListener only removes the handler related to a given phase (1/2)", function() { spy = o.spy(function(e) {o(e.eventPhase).equals(3)}) $document.body.addEventListener("click", spy, true) $document.body.addEventListener("click", spy, false) $document.body.removeEventListener("click", spy, true) div.dispatchEvent(e) o(spy.callCount).equals(1) }) o("removeEventListener only removes the handler related to a given phase (2/2)", function() { spy = o.spy(function(e) {o(e.eventPhase).equals(1)}) $document.body.addEventListener("click", spy, true) $document.body.addEventListener("click", spy, false) $document.body.removeEventListener("click", spy, false) div.dispatchEvent(e) o(spy.callCount).equals(1) }) o("click fires onclick", function() { div.onclick = spy div.dispatchEvent(e) o(spy.callCount).equals(1) o(spy.this).equals(div) o(spy.args[0].type).equals("click") o(spy.args[0].target).equals(div) }) o("click without onclick doesn't throw", function(done) { div.dispatchEvent(e) done() }) }) o.spec("transitionend", function() { var spy, div, e o.beforeEach(function() { spy = o.spy() div = $document.createElement("div") e = $document.createEvent("HTMLEvents") e.initEvent("transitionend", true, true) $document.body.appendChild(div) }) o.afterEach(function() { $document.body.removeChild(div) }) o("ontransitionend does not fire", function(done) { div.ontransitionend = spy div.dispatchEvent(e) o(spy.callCount).equals(0) done() }) }) o.spec("capture and bubbling phases", function() { var div, e o.beforeEach(function() { div = $document.createElement("div") e = $document.createEvent("MouseEvents") e.initEvent("click", true, true) $document.body.appendChild(div) }) o.afterEach(function() { $document.body.removeChild(div) }) o("capture and bubbling events both fire on the target in the order they were defined, regardless of the phase", function () { var sequence = [] var capture = o.spy(function(ev){ sequence.push("capture") o(ev).equals(e) o(ev.eventPhase).equals(2) o(ev.target).equals(div) o(ev.currentTarget).equals(div) }) var bubble = o.spy(function(ev){ sequence.push("bubble") o(ev).equals(e) o(ev.eventPhase).equals(2) o(ev.target).equals(div) o(ev.currentTarget).equals(div) }) div.addEventListener("click", bubble, false) div.addEventListener("click", capture, true) div.dispatchEvent(e) o(capture.callCount).equals(1) o(bubble.callCount).equals(1) o(sequence).deepEquals(["bubble", "capture"]) }) o("capture and bubbling events both fire on the parent", function () { var sequence = [] var capture = o.spy(function(ev){ sequence.push("capture") o(ev).equals(e) o(ev.eventPhase).equals(1) o(ev.target).equals(div) o(ev.currentTarget).equals($document.body) }) var bubble = o.spy(function(ev){ sequence.push("bubble") o(ev).equals(e) o(ev.eventPhase).equals(3) o(ev.target).equals(div) o(ev.currentTarget).equals($document.body) }) $document.body.addEventListener("click", bubble, false) $document.body.addEventListener("click", capture, true) div.dispatchEvent(e) o(capture.callCount).equals(1) o(bubble.callCount).equals(1) o(sequence).deepEquals(["capture", "bubble"]) }) o("useCapture defaults to false", function () { var sequence = [] var parent = o.spy(function(ev){ sequence.push("parent") o(ev).equals(e) o(ev.eventPhase).equals(3) o(ev.target).equals(div) o(ev.currentTarget).equals($document.body) }) var target = o.spy(function(ev){ sequence.push("target") o(ev).equals(e) o(ev.eventPhase).equals(2) o(ev.target).equals(div) o(ev.currentTarget).equals(div) }) $document.body.addEventListener("click", parent) div.addEventListener("click", target) div.dispatchEvent(e) o(parent.callCount).equals(1) o(target.callCount).equals(1) o(sequence).deepEquals(["target", "parent"]) }) o("legacy handlers fire on the bubbling phase", function () { var sequence = [] var parent = o.spy(function(ev){ sequence.push("parent") o(ev).equals(e) o(ev.eventPhase).equals(3) o(ev.target).equals(div) o(ev.currentTarget).equals($document.body) }) var target = o.spy(function(ev){ sequence.push("target") o(ev).equals(e) o(ev.eventPhase).equals(2) o(ev.target).equals(div) o(ev.currentTarget).equals(div) }) $document.body.addEventListener("click", parent) $document.body.onclick = parent div.addEventListener("click", target) div.dispatchEvent(e) o(parent.callCount).equals(2) o(target.callCount).equals(1) o(sequence).deepEquals(["target", "parent", "parent"]) }) o("events do not propagate to child nodes", function() { var target = o.spy(function(ev){ o(ev).equals(e) o(ev.eventPhase).equals(2) o(ev.target).equals($document.body) o(ev.currentTarget).equals($document.body) }) var child = o.spy(function(){ }) $document.body.addEventListener("click", target) div.addEventListener("click", child) $document.body.dispatchEvent(e) o(target.callCount).equals(1) o(child.callCount).equals(0) }) o("e.stopPropagation 1/6", function () { var capParent = o.spy(function(e){e.stopPropagation()}) var capTarget = o.spy() var bubTarget = o.spy() var legacyTarget = o.spy() var bubParent = o.spy() var legacyParent = o.spy() $document.body.addEventListener("click", capParent, true) $document.body.addEventListener("click", bubParent, false) $document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) div.onclick = legacyTarget div.dispatchEvent(e) o(capParent.callCount).equals(1) o(capTarget.callCount).equals(0) o(bubTarget.callCount).equals(0) o(legacyTarget.callCount).equals(0) o(bubParent.callCount).equals(0) o(legacyParent.callCount).equals(0) }) o("e.stopPropagation 2/6", function () { var capParent = o.spy() var capTarget = o.spy(function(e){e.stopPropagation()}) var bubTarget = o.spy() var legacyTarget = o.spy() var bubParent = o.spy() var legacyParent = o.spy() $document.body.addEventListener("click", capParent, true) $document.body.addEventListener("click", bubParent, false) $document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) div.onclick = legacyTarget div.dispatchEvent(e) o(capParent.callCount).equals(1) o(capTarget.callCount).equals(1) o(bubTarget.callCount).equals(1) o(legacyTarget.callCount).equals(1) o(bubParent.callCount).equals(0) o(legacyParent.callCount).equals(0) }) o("e.stopPropagation 3/6", function () { var capParent = o.spy() var capTarget = o.spy() var bubTarget = o.spy(function(e){e.stopPropagation()}) var legacyTarget = o.spy() var bubParent = o.spy() var legacyParent = o.spy() $document.body.addEventListener("click", capParent, true) $document.body.addEventListener("click", bubParent, false) $document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) div.onclick = legacyTarget div.dispatchEvent(e) o(capParent.callCount).equals(1) o(capTarget.callCount).equals(1) o(bubTarget.callCount).equals(1) o(legacyTarget.callCount).equals(1) o(bubParent.callCount).equals(0) o(legacyParent.callCount).equals(0) }) o("e.stopPropagation 4/6", function () { var capParent = o.spy() var capTarget = o.spy() var bubTarget = o.spy() var legacyTarget = o.spy(function(e){e.stopPropagation()}) var bubParent = o.spy() var legacyParent = o.spy() $document.body.addEventListener("click", capParent, true) $document.body.addEventListener("click", bubParent, false) $document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) div.onclick = legacyTarget div.dispatchEvent(e) o(capParent.callCount).equals(1) o(capTarget.callCount).equals(1) o(bubTarget.callCount).equals(1) o(legacyTarget.callCount).equals(1) o(bubParent.callCount).equals(0) o(legacyParent.callCount).equals(0) }) o("e.stopPropagation 5/6", function () { var capParent = o.spy() var capTarget = o.spy() var bubTarget = o.spy() var legacyTarget = o.spy() var bubParent = o.spy(function(e){e.stopPropagation()}) var legacyParent = o.spy() $document.body.addEventListener("click", capParent, true) $document.body.addEventListener("click", bubParent, false) $document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) div.onclick = legacyTarget div.dispatchEvent(e) o(capParent.callCount).equals(1) o(capTarget.callCount).equals(1) o(bubTarget.callCount).equals(1) o(legacyTarget.callCount).equals(1) o(bubParent.callCount).equals(1) o(legacyParent.callCount).equals(1) }) o("e.stopPropagation 6/6", function () { var capParent = o.spy() var capTarget = o.spy() var legacyTarget = o.spy() var bubTarget = o.spy() var bubParent = o.spy() var legacyParent = o.spy(function(e){e.stopPropagation()}) $document.body.addEventListener("click", capParent, true) $document.body.addEventListener("click", bubParent, false) $document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) div.onclick = legacyTarget div.dispatchEvent(e) o(capParent.callCount).equals(1) o(capTarget.callCount).equals(1) o(bubTarget.callCount).equals(1) o(legacyTarget.callCount).equals(1) o(bubParent.callCount).equals(1) o(legacyParent.callCount).equals(1) }) o("e.stopImmediatePropagation 1/6", function () { var capParent = o.spy(function(e){e.stopImmediatePropagation()}) var capTarget = o.spy() var bubTarget = o.spy() var legacyTarget = o.spy() var bubParent = o.spy() var legacyParent = o.spy() $document.body.addEventListener("click", capParent, true) $document.body.addEventListener("click", bubParent, false) $document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) div.onclick = legacyTarget div.dispatchEvent(e) o(capParent.callCount).equals(1) o(capTarget.callCount).equals(0) o(bubTarget.callCount).equals(0) o(legacyTarget.callCount).equals(0) o(bubParent.callCount).equals(0) o(legacyParent.callCount).equals(0) }) o("e.stopImmediatePropagation 2/6", function () { var capParent = o.spy() var capTarget = o.spy(function(e){e.stopImmediatePropagation()}) var bubTarget = o.spy() var legacyTarget = o.spy() var bubParent = o.spy() var legacyParent = o.spy() $document.body.addEventListener("click", capParent, true) $document.body.addEventListener("click", bubParent, false) $document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) div.onclick = legacyTarget div.dispatchEvent(e) o(capParent.callCount).equals(1) o(capTarget.callCount).equals(1) o(bubTarget.callCount).equals(0) o(legacyTarget.callCount).equals(0) o(bubParent.callCount).equals(0) o(legacyParent.callCount).equals(0) }) o("e.stopImmediatePropagation 3/6", function () { var capParent = o.spy() var capTarget = o.spy() var bubTarget = o.spy(function(e){e.stopImmediatePropagation()}) var legacyTarget = o.spy() var bubParent = o.spy() var legacyParent = o.spy() $document.body.addEventListener("click", capParent, true) $document.body.addEventListener("click", bubParent, false) $document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) div.onclick = legacyTarget div.dispatchEvent(e) o(capParent.callCount).equals(1) o(capTarget.callCount).equals(1) o(bubTarget.callCount).equals(1) o(legacyTarget.callCount).equals(0) o(bubParent.callCount).equals(0) o(legacyParent.callCount).equals(0) }) o("e.stopImmediatePropagation 4/6", function () { var capParent = o.spy() var capTarget = o.spy() var bubTarget = o.spy() var legacyTarget = o.spy(function(e){e.stopImmediatePropagation()}) var bubParent = o.spy() var legacyParent = o.spy() $document.body.addEventListener("click", capParent, true) $document.body.addEventListener("click", bubParent, false) $document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) div.onclick = legacyTarget div.dispatchEvent(e) o(capParent.callCount).equals(1) o(capTarget.callCount).equals(1) o(bubTarget.callCount).equals(1) o(legacyTarget.callCount).equals(1) o(bubParent.callCount).equals(0) o(legacyParent.callCount).equals(0) }) o("e.stopImmediatePropagation 5/6", function () { var capParent = o.spy() var capTarget = o.spy() var bubTarget = o.spy() var legacyTarget = o.spy() var bubParent = o.spy(function(e){e.stopImmediatePropagation()}) var legacyParent = o.spy() $document.body.addEventListener("click", capParent, true) $document.body.addEventListener("click", bubParent, false) $document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) div.onclick = legacyTarget div.dispatchEvent(e) o(capParent.callCount).equals(1) o(capTarget.callCount).equals(1) o(bubTarget.callCount).equals(1) o(legacyTarget.callCount).equals(1) o(bubParent.callCount).equals(1) o(legacyParent.callCount).equals(0) }) o("e.stopImmediatePropagation 6/6", function () { var capParent = o.spy() var capTarget = o.spy() var legacyTarget = o.spy() var bubTarget = o.spy() var bubParent = o.spy() var legacyParent = o.spy(function(e){e.stopImmediatePropagation()}) $document.body.addEventListener("click", capParent, true) $document.body.addEventListener("click", bubParent, false) $document.body.onclick = legacyParent div.addEventListener("click", capTarget, true) div.addEventListener("click", bubTarget, false) div.onclick = legacyTarget div.dispatchEvent(e) o(capParent.callCount).equals(1) o(capTarget.callCount).equals(1) o(bubTarget.callCount).equals(1) o(legacyTarget.callCount).equals(1) o(bubParent.callCount).equals(1) o(legacyParent.callCount).equals(1) }) o("errors thrown in handlers don't interrupt the chain", function(done) { var errMsg = "The presence of these six errors in the log is expected in non-Node.js environments" var handler = o.spy(function(){throw errMsg}) $document.body.addEventListener("click", handler, true) $document.body.addEventListener("click", handler, false) $document.body.onclick = handler div.addEventListener("click", handler, true) div.addEventListener("click", handler, false) div.onclick = handler div.dispatchEvent(e) o(handler.callCount).equals(6) // Swallow the async errors in NodeJS if (typeof process !== "undefined" && typeof process.once === "function"){ process.once("uncaughtException", function(e) { if (e !== errMsg) throw e process.once("uncaughtException", function(e) { if (e !== errMsg) throw e process.once("uncaughtException", function(e) { if (e !== errMsg) throw e process.once("uncaughtException", function(e) { if (e !== errMsg) throw e process.once("uncaughtException", function(e) { if (e !== errMsg) throw e process.once("uncaughtException", function(e) { if (e !== errMsg) throw e done() }) }) }) }) }) }) } else { done() } }) }) }) o.spec("attributes", function() { o.spec("a[href]", function() { o("is empty string if no attribute", function() { var a = $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") 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") 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") a.href = "/foo" o(a.href).deepEquals({baseVal: "", animVal: ""}) o("href" in a.attributes).equals(false) }) }) o.spec("input[checked]", function() { o("only exists in input elements", function() { var input = $document.createElement("input") var a = $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") input.setAttribute("type", "checkbox") o(input.checked).equals(false) o(input.attributes["checked"]).equals(undefined) input.setAttribute("checked", "") o(input.checked).equals(true) o(input.attributes["checked"].value).equals("") input.removeAttribute("checked") o(input.checked).equals(false) o(input.attributes["checked"]).equals(undefined) }) o("does not track attribute value when set", function() { var input = $document.createElement("input") input.setAttribute("type", "checkbox") input.checked = true o(input.checked).equals(true) o(input.attributes["checked"]).equals(undefined) input.checked = false input.setAttribute("checked", "") input.checked = true input.removeAttribute("checked") o(input.checked).equals(true) }) o("toggles on click", function() { var input = $document.createElement("input") input.setAttribute("type", "checkbox") input.checked = false var e = $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") input.setAttribute("type", "checkbox") input.checked = false input.onclick = function(e) {e.preventDefault()} var e = $document.createEvent("MouseEvents") e.initEvent("click", true, true) input.dispatchEvent(e) o(input.checked).equals(false) }) }) o.spec("input[value]", function() { o("only exists in input elements", function() { var input = $document.createElement("input") var a = $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") input.value = "x" o(input.value).equals("x") input.value = null o(input.value).equals("") }) o("converts values to strings", function() { var input = $document.createElement("input") input.value = 5 o(input.value).equals("5") input.value = 0 o(input.value).equals("0") input.value = undefined o(input.value).equals("undefined") }) if (typeof Symbol === "function") o("throws when set to a symbol", function() { var threw = false var input = $document.createElement("input") try { input.value = Symbol("") } catch (e) { o(e instanceof TypeError).equals(true) threw = true } o(input.value).equals("") o(threw).equals(true) }) }) o.spec("input[type]", function(){ o("only exists in input elements", function() { var input = $document.createElement("input") var a = $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") 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") 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") input.type = "badbad" + type o(input.getAttribute("type")).equals("badbad" + type) o(input.type).equals("text") }) }) }) 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")) o(textarea.value).equals("aaa") }) o("ignores child if value set", function() { var textarea = $document.createElement("textarea") textarea.value = null textarea.appendChild($document.createTextNode("aaa")) o(textarea.value).equals("") }) o("textarea[value] doesn't reflect `attributes.value`", function() { var textarea = $document.createElement("textarea") textarea.value = "aaa" textarea.setAttribute("value", "bbb") o(textarea.value).equals("aaa") }) }) 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") o("value" in select).equals(true) o("value" in a).equals(false) o("selectedIndex" in select).equals(true) o("selectedIndex" in a).equals(false) }) o("value defaults to value at first index", function() { var select = $document.createElement("select") var option1 = $document.createElement("option") option1.setAttribute("value", "a") select.appendChild(option1) var option2 = $document.createElement("option") option2.setAttribute("value", "b") select.appendChild(option2) o(select.value).equals("a") o(select.selectedIndex).equals(0) }) o("value falls back to child nodeValue if no attribute", function() { var select = $document.createElement("select") var option1 = $document.createElement("option") option1.appendChild($document.createTextNode("a")) var option2 = $document.createElement("option") option2.appendChild($document.createTextNode("b")) select.appendChild(option1) select.appendChild(option2) o(select.value).equals("a") o(select.selectedIndex).equals(0) o(select.childNodes[0].selected).equals(true) o(select.childNodes[0].value).equals("a") o(select.childNodes[1].value).equals("b") }) o("value defaults to invalid if no options", function() { var select = $document.createElement("select") o(select.value).equals("") o(select.selectedIndex).equals(-1) }) o("setting valid value works", function() { var select = $document.createElement("select") var option1 = $document.createElement("option") option1.setAttribute("value", "a") select.appendChild(option1) var option2 = $document.createElement("option") option2.setAttribute("value", "b") select.appendChild(option2) var option3 = $document.createElement("option") option3.setAttribute("value", "") select.appendChild(option3) var option4 = $document.createElement("option") option4.setAttribute("value", "null") select.appendChild(option4) select.value = "b" o(select.value).equals("b") o(select.selectedIndex).equals(1) select.value = "" o(select.value).equals("") o(select.selectedIndex).equals(2) select.value = "null" o(select.value).equals("null") o(select.selectedIndex).equals(3) select.value = null o(select.value).equals("") o(select.selectedIndex).equals(-1) }) o("setting valid value works with type conversion", function() { var select = $document.createElement("select") var option1 = $document.createElement("option") option1.setAttribute("value", "0") select.appendChild(option1) var option2 = $document.createElement("option") option2.setAttribute("value", "undefined") select.appendChild(option2) var option3 = $document.createElement("option") option3.setAttribute("value", "") select.appendChild(option3) select.value = 0 o(select.value).equals("0") o(select.selectedIndex).equals(0) select.value = undefined o(select.value).equals("undefined") o(select.selectedIndex).equals(1) if (typeof Symbol === "function") { var threw = false try { select.value = Symbol("x") } catch (e) { threw = true } o(threw).equals(true) o(select.value).equals("undefined") o(select.selectedIndex).equals(1) } }) o("option.value = null is converted to 'null'", function() { var option = $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 option1 = $document.createElement("option") option1.setAttribute("value", "a") var option2 = $document.createElement("option") option2.setAttribute("value", "b") var option3 = $document.createElement("option") option3.setAttribute("value", "c") var optgroup = $document.createElement("optgroup") optgroup.appendChild(option1) optgroup.appendChild(option2) select.appendChild(optgroup) select.appendChild(option3) select.value = "b" o(select.value).equals("b") o(select.selectedIndex).equals(1) }) o("setting valid selectedIndex works", function() { var select = $document.createElement("select") var option1 = $document.createElement("option") option1.setAttribute("value", "a") select.appendChild(option1) var option2 = $document.createElement("option") option2.setAttribute("value", "b") select.appendChild(option2) select.selectedIndex = 1 o(select.value).equals("b") o(select.selectedIndex).equals(1) }) o("setting option[selected] works", function() { var select = $document.createElement("select") var option1 = $document.createElement("option") option1.setAttribute("value", "a") select.appendChild(option1) var option2 = $document.createElement("option") option2.setAttribute("value", "b") select.appendChild(option2) select.childNodes[1].selected = true o(select.value).equals("b") o(select.selectedIndex).equals(1) }) o("unsetting option[selected] works", function() { var select = $document.createElement("select") var option1 = $document.createElement("option") option1.setAttribute("value", "a") select.appendChild(option1) var option2 = $document.createElement("option") option2.setAttribute("value", "b") select.appendChild(option2) select.childNodes[1].selected = true select.childNodes[1].selected = false o(select.value).equals("a") 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 option1 = $document.createElement("option") option1.setAttribute("value", "a") select.appendChild(option1) var option2 = $document.createElement("option") option2.setAttribute("value", "b") select.appendChild(option2) select.value = "c" o(select.value).equals("") 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 option1 = $document.createElement("option") option1.setAttribute("value", "a") select.appendChild(option1) var option2 = $document.createElement("option") option2.setAttribute("value", "b") select.appendChild(option2) select.selectedIndex = -2 o(select.value).equals("") 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 option1 = $document.createElement("option") option1.setAttribute("value", "a") select.appendChild(option1) var option2 = $document.createElement("option") option2.setAttribute("value", "") select.appendChild(option2) select.value = "c" o(select.value).equals("") 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 option1 = $document.createElement("option") option1.setAttribute("value", "a") select.appendChild(option1) var option2 = $document.createElement("option") option2.setAttribute("value", "") select.appendChild(option2) select.selectedIndex = -2 o(select.value).equals("") o(select.selectedIndex).equals(-1) }) }) o.spec("canvas width and height", function() { o("setting property works", function() { var canvas = $document.createElement("canvas") canvas.width = 100 o(canvas.attributes["width"].value).equals("100") o(canvas.width).equals(100) canvas.height = 100 o(canvas.attributes["height"].value).equals("100") o(canvas.height).equals(100) }) o("setting string casts to number", function() { var canvas = $document.createElement("canvas") canvas.width = "100" o(canvas.attributes["width"].value).equals("100") o(canvas.width).equals(100) canvas.height = "100" o(canvas.attributes["height"].value).equals("100") o(canvas.height).equals(100) }) o("setting float casts to int", function() { var canvas = $document.createElement("canvas") canvas.width = 1.2 o(canvas.attributes["width"].value).equals("1") o(canvas.width).equals(1) canvas.height = 1.2 o(canvas.attributes["height"].value).equals("1") o(canvas.height).equals(1) }) o("setting percentage fails", function() { var canvas = $document.createElement("canvas") canvas.width = "100%" o(canvas.attributes["width"].value).equals("0") o(canvas.width).equals(0) canvas.height = "100%" o(canvas.attributes["height"].value).equals("0") o(canvas.height).equals(0) }) o("setting attribute works", function() { var canvas = $document.createElement("canvas") canvas.setAttribute("width", "100%") o(canvas.attributes["width"].value).equals("100%") o(canvas.width).equals(100) canvas.setAttribute("height", "100%") o(canvas.attributes["height"].value).equals("100%") o(canvas.height).equals(100) }) }) }) o.spec("className", function() { o("works", function() { var el = $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") try { el.className = "a" } catch (e) { done() } }) }) o.spec("spies", function() { var $window o.beforeEach(function() { $window = domMock({spy: o.spy}) }) o("basics", function() { o(typeof $window.__getSpies).equals("function") o("__getSpies" in domMock()).equals(false) }) o("input elements have spies on value and type setters", function() { var input = $window.document.createElement("input") var spies = $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 spies = $window.__getSpies(select) o(typeof spies).equals("object") o(spies).notEquals(null) o(typeof spies.valueSetter).equals("function") o(spies.valueSetter.callCount).equals(0) select.value = "aaa" o(spies.valueSetter.callCount).equals(1) o(spies.valueSetter.this).equals(select) o(spies.valueSetter.args[0]).equals("aaa") }) o("option elements have spies on value setters", function() { var option = $window.document.createElement("option") var spies = $window.__getSpies(option) o(typeof spies).equals("object") o(spies).notEquals(null) o(typeof spies.valueSetter).equals("function") o(spies.valueSetter.callCount).equals(0) option.value = "aaa" o(spies.valueSetter.callCount).equals(1) o(spies.valueSetter.this).equals(option) o(spies.valueSetter.args[0]).equals("aaa") }) o("textarea elements have spies on value setters", function() { var textarea = $window.document.createElement("textarea") var spies = $window.__getSpies(textarea) o(typeof spies).equals("object") o(spies).notEquals(null) o(typeof spies.valueSetter).equals("function") o(spies.valueSetter.callCount).equals(0) textarea.value = "aaa" o(spies.valueSetter.callCount).equals(1) o(spies.valueSetter.this).equals(textarea) 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") }) }) }) ================================================ FILE: test-utils/tests/test-parseURL.js ================================================ "use strict" var o = require("ospec") var parseURL = require("../../test-utils/parseURL") o.spec("parseURL", function() { var root = {protocol: "http:", hostname: "localhost", port: "", pathname: "/"} o.spec("full URL", function() { o("parses full http URL", function() { var data = parseURL("http://www.google.com:80/test?a=b#c") o(data.protocol).equals("http:") o(data.hostname).equals("www.google.com") o(data.port).equals("80") o(data.pathname).equals("/test") o(data.search).equals("?a=b") o(data.hash).equals("#c") }) o("parses full websocket URL", function() { var data = parseURL("ws://www.google.com:80/test?a=b#c") o(data.protocol).equals("ws:") o(data.hostname).equals("www.google.com") o(data.port).equals("80") o(data.pathname).equals("/test") o(data.search).equals("?a=b") o(data.hash).equals("#c") }) o("parses full URL omitting optionals", function() { var data = parseURL("http://www.google.com") o(data.protocol).equals("http:") o(data.hostname).equals("www.google.com") o(data.port).equals("") o(data.pathname).equals("/") o(data.search).equals("") o(data.hash).equals("") }) }) o.spec("absolute path", function() { o("parses absolute path", function() { var data = parseURL("/test?a=b#c", root) o(data.protocol).equals(root.protocol) o(data.hostname).equals(root.hostname) o(data.port).equals(root.port) o(data.pathname).equals("/test") o(data.search).equals("?a=b") o(data.hash).equals("#c") }) o("parses absolute path omitting optionals", function() { var data = parseURL("/test?a=b#c", root) o(data.protocol).equals(root.protocol) o(data.hostname).equals(root.hostname) o(data.port).equals(root.port) o(data.pathname).equals("/test") o(data.search).equals("?a=b") o(data.hash).equals("#c") }) }) o.spec("relative path", function() { o("parses relative URL", function() { var data = parseURL("test?a=b#c", root) o(data.protocol).equals(root.protocol) o(data.hostname).equals(root.hostname) o(data.port).equals(root.port) o(data.pathname).equals("/test") o(data.search).equals("?a=b") o(data.hash).equals("#c") }) o("parses relative URL omitting optionals", function() { var data = parseURL("test", root) o(data.protocol).equals(root.protocol) o(data.hostname).equals(root.hostname) o(data.port).equals(root.port) o(data.pathname).equals("/test") o(data.search).equals("") o(data.hash).equals("") }) o("parses relative URL with dot", function() { var data = parseURL("././test?a=b#c", root) o(data.protocol).equals(root.protocol) o(data.hostname).equals(root.hostname) o(data.port).equals(root.port) o(data.pathname).equals("/test") o(data.search).equals("?a=b") o(data.hash).equals("#c") }) o("parses relative URL with dotdot", function() { var data = parseURL("foo/bar/../../test?a=b#c", root) o(data.protocol).equals(root.protocol) o(data.hostname).equals(root.hostname) o(data.port).equals(root.port) o(data.pathname).equals("/test") o(data.search).equals("?a=b") o(data.hash).equals("#c") }) o("clamps invalid dotdot", function() { var data = parseURL("../../test?a=b#c", root) o(data.protocol).equals(root.protocol) o(data.hostname).equals(root.hostname) o(data.port).equals(root.port) o(data.pathname).equals("/test") o(data.search).equals("?a=b") o(data.hash).equals("#c") }) o("clamps invalid dotdot after dot", function() { var data = parseURL("./../../test?a=b#c", root) o(data.protocol).equals(root.protocol) o(data.hostname).equals(root.hostname) o(data.port).equals(root.port) o(data.pathname).equals("/test") o(data.search).equals("?a=b") o(data.hash).equals("#c") }) o("clamps invalid dotdot after valid path", function() { var data = parseURL("a/../../test?a=b#c", root) o(data.protocol).equals(root.protocol) o(data.hostname).equals(root.hostname) o(data.port).equals(root.port) o(data.pathname).equals("/test") o(data.search).equals("?a=b") o(data.hash).equals("#c") }) }) o.spec("edge cases", function() { o("handles hash w/ question mark", function() { var data = parseURL("http://www.google.com/test#a?c") o(data.pathname).equals("/test") o(data.search).equals("") o(data.hash).equals("#a?c") }) o("handles hash w/ slash", function() { var data = parseURL("http://www.google.com/test#a/c") o(data.pathname).equals("/test") o(data.search).equals("") o(data.hash).equals("#a/c") }) o("handles hash w/ colon", function() { var data = parseURL("http://www.google.com/test#a:c") o(data.pathname).equals("/test") o(data.search).equals("") o(data.hash).equals("#a:c") }) o("handles search w/ slash", function() { var data = parseURL("http://www.google.com/test?a/c") o(data.pathname).equals("/test") o(data.search).equals("?a/c") o(data.hash).equals("") }) o("handles search w/ colon", function() { var data = parseURL("http://www.google.com/test?a:c") o(data.pathname).equals("/test") o(data.search).equals("?a:c") o(data.hash).equals("") }) o("handles pathname w/ colon", function() { var data = parseURL("http://www.google.com/a:b") o(data.pathname).equals("/a:b") }) }) }) ================================================ FILE: test-utils/tests/test-pushStateMock.js ================================================ "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" }) }) }) ================================================ FILE: test-utils/tests/test-throttleMock.js ================================================ "use strict" var o = require("ospec") var throttleMocker = require("../../test-utils/throttleMock") o.spec("throttleMock", function() { o("schedules one callback", function() { var throttleMock = throttleMocker() var spy = o.spy() o(throttleMock.queueLength()).equals(0) throttleMock.schedule(spy) o(throttleMock.queueLength()).equals(1) o(spy.callCount).equals(0) throttleMock.fire() o(throttleMock.queueLength()).equals(0) o(spy.callCount).equals(1) }) o("schedules two callbacks", function() { var throttleMock = throttleMocker() var spy1 = o.spy() var spy2 = o.spy() o(throttleMock.queueLength()).equals(0) throttleMock.schedule(spy1) o(throttleMock.queueLength()).equals(1) o(spy1.callCount).equals(0) o(spy2.callCount).equals(0) throttleMock.schedule(spy2) o(throttleMock.queueLength()).equals(2) o(spy1.callCount).equals(0) o(spy2.callCount).equals(0) throttleMock.fire() o(throttleMock.queueLength()).equals(0) o(spy1.callCount).equals(1) o(spy2.callCount).equals(1) }) }) ================================================ FILE: test-utils/tests/test-xhrMock.js ================================================ "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") }) }) }) ================================================ FILE: test-utils/throttleMock.js ================================================ "use strict" module.exports = function() { var queue = [] return { schedule: function(fn) { queue.push(fn) }, fire: function() { var tasks = queue queue = [] tasks.forEach(function(fn) {fn()}) }, queueLength: function(){ return queue.length } } } ================================================ FILE: test-utils/xhrMock.js ================================================ "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 } ================================================ FILE: tests/test-api.js ================================================ "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) }) }) }) }) }) ================================================ FILE: util/censor.js ================================================ "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") var magic = /^(?: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 } ================================================ FILE: util/decodeURIComponentSafe.js ================================================ "use strict" /* Percent encodings encode UTF-8 bytes, so this regexp needs to match that. Here's how UTF-8 encodes stuff: - `00-7F`: 1-byte, for U+0000-U+007F - `C2-DF 80-BF`: 2-byte, for U+0080-U+07FF - `E0-EF 80-BF 80-BF`: 3-byte, encodes U+0800-U+FFFF - `F0-F4 80-BF 80-BF 80-BF`: 4-byte, encodes U+10000-U+10FFFF In this, there's a number of invalid byte sequences: - `80-BF`: Continuation byte, invalid as start - `C0-C1 80-BF`: Overlong encoding for U+0000-U+007F - `E0 80-9F 80-BF`: Overlong encoding for U+0080-U+07FF - `ED A0-BF 80-BF`: Encoding for UTF-16 surrogate U+D800-U+DFFF - `F0 80-8F 80-BF 80-BF`: Overlong encoding for U+0800-U+FFFF - `F4 90-BF`: RFC 3629 restricted UTF-8 to only code points UTF-16 could encode. - `F5-FF`: RFC 3629 restricted UTF-8 to only code points UTF-16 could encode. So in reality, only the following sequences can encode are valid characters: - 00-7F - C2-DF 80-BF - E0 A0-BF 80-BF - E1-EC 80-BF 80-BF - ED 80-9F 80-BF - EE-EF 80-BF 80-BF - F0 90-BF 80-BF 80-BF - F1-F3 80-BF 80-BF 80-BF - F4 80-8F 80-BF 80-BF The regexp just tries to match this as compactly as possible. */ var validUtf8Encodings = /%(?:[0-7]|(?!c[01]|e0%[89]|ed%[ab]|f0%8|f4%[9ab])(?:c|d|(?:e|f[0-4]%[89ab])[\da-f]%[89ab])[\da-f]%[89ab])[\da-f]/gi module.exports = function(str) { return String(str).replace(validUtf8Encodings, decodeURIComponent) } ================================================ FILE: util/hasOwn.js ================================================ // This exists so I'm only saving it once. "use strict" module.exports = {}.hasOwnProperty ================================================ FILE: util/tests/test-censor.js ================================================ "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", }) }) }) }) ================================================ FILE: util/tests/test-decodeURIComponentSafe.js ================================================ "use strict" var o = require("ospec") var decodeURIComponentSafe = require("../../util/decodeURIComponentSafe") o.spec("decodeURIComponentSafe", function() { o("non-string type (compared to decodeURIComponent)", function() { o(decodeURIComponentSafe()).equals(decodeURIComponent()) o(decodeURIComponentSafe(null)).equals(decodeURIComponent(null)) o(decodeURIComponentSafe(0)).equals(decodeURIComponent(0)) o(decodeURIComponentSafe(true)).equals(decodeURIComponent(true)) o(decodeURIComponentSafe(false)).equals(decodeURIComponent(false)) o(decodeURIComponentSafe({})).equals(decodeURIComponent({})) o(decodeURIComponentSafe([])).equals(decodeURIComponent([])) o(decodeURIComponentSafe(function(){})).equals(decodeURIComponent(function(){})) }) o("non-percent-encoded string", function() { o(decodeURIComponentSafe("")).equals("") o(decodeURIComponentSafe("1")).equals("1") o(decodeURIComponentSafe("abc")).equals("abc") o(decodeURIComponentSafe("😃")).equals("😃") }) o("percent-encoded ASCII", function() { for (var i = 0; i < 128; i++) { var char = String.fromCharCode(i) var uenc = "%" + Number(i).toString(16).padStart(2, "0").toUpperCase() var lenc = "%" + Number(i).toString(16).padStart(2, "0").toLowerCase() var uout = decodeURIComponentSafe(uenc) var lout = decodeURIComponentSafe(lenc) o(char).equals(uout) o(char).equals(lout) } }) o("all code points (without surrogates)", function() { var ranges = [ [0x0000, 0xD7FF], /* [0xD800, 0xDFFF], */ [0xE000, 0x10FFFF] ] for (var [lo, hi] of ranges) { for (var cp = lo; cp <= hi; cp++) { var char = String.fromCodePoint(cp) // including ASCII characters not encoded by encodeURIComponent var enc = encodeURIComponent(char) var out = decodeURIComponentSafe(enc) o(char).equals(out) } } }) o("invalid byte sequences", function() { // `80-BF`: Continuation byte, invalid as start o(decodeURIComponentSafe("%7F")).notEquals("%7F") o(decodeURIComponentSafe("%80")).equals("%80") o(decodeURIComponentSafe("%BF")).equals("%BF") // `C0-C1 80-BF`: Overlong encoding for U+0000-U+007F o(decodeURIComponentSafe("%C0%80")).equals("%C0%80") // U+0000 o(decodeURIComponentSafe("%C1%BF")).equals("%C1%BF") // U+007F o(decodeURIComponentSafe("%C2%80")).notEquals("%C2%80") // U+0080 // `E0 80-9F 80-BF`: Overlong encoding for U+0080-U+07FF o(decodeURIComponentSafe("%DF%BF")).notEquals("%DF%BF") // U+07FF o(decodeURIComponentSafe("%E0%80%80")).equals("%E0%80%80") // U+0000 o(decodeURIComponentSafe("%E0%9F%BF")).equals("%E0%9F%BF") // U+07FF o(decodeURIComponentSafe("%E0%A0%80")).notEquals("%E0%A0%80") // U+0800 // `ED A0-BF 80-BF`: Encoding for UTF-16 surrogate U+D800-U+DFFF o(decodeURIComponentSafe("%ED%9F%BF")).notEquals("%ED%9F%BF") // U+D7FF o(decodeURIComponentSafe("%ED%A0%80")).equals("%ED%A0%80") // U+D800 o(decodeURIComponentSafe("%ED%AF%BF")).equals("%ED%AF%BF") // U+DBFF o(decodeURIComponentSafe("%ED%B0%80")).equals("%ED%B0%80") // U+DC00 o(decodeURIComponentSafe("%ED%BF%BF")).equals("%ED%BF%BF") // U+DFFF o(decodeURIComponentSafe("%EE%80%80")).notEquals("%EE%80%80") // U+E000 // `F0 80-8F 80-BF 80-BF`: Overlong encoding for U+0800-U+FFFF o(decodeURIComponentSafe("%EF%BF%BF")).notEquals("%EF%BF%BF") // U+FFFF o(decodeURIComponentSafe("%F0%80%80%80")).equals("%F0%80%80%80") // U+0000 o(decodeURIComponentSafe("%E0%80%9F%BF")).equals("%E0%80%9F%BF") // U+07FF o(decodeURIComponentSafe("%E0%80%A0%80")).equals("%E0%80%A0%80") // U+0800 o(decodeURIComponentSafe("%F0%8F%BF%BF")).equals("%F0%8F%BF%BF") // U+FFFF o(decodeURIComponentSafe("%F0%90%80%80")).notEquals("%F0%90%80%80") // U+10000 // `F4 90-BF`: RFC 3629 restricted UTF-8 to only code points UTF-16 could encode. o(decodeURIComponentSafe("%F4%8F%BF%BF")).notEquals("%F4%8F%BF%BF") // U+10FFFF o(decodeURIComponentSafe("%F4%90%80%80")).equals("%F4%90%80%80") // U+110000 o(decodeURIComponentSafe("%F4%BF%BF%BF")).equals("%F4%BF%BF%BF") // U+13FFFF // `F5-FF`: RFC 3629 restricted UTF-8 to only code points UTF-16 could encode. o(decodeURIComponentSafe("%F5")).equals("%F5") o(decodeURIComponentSafe("%FF")).equals("%FF") o(decodeURIComponentSafe("%F5%80%80%80")).equals("%F5%80%80%80") // U+140000 o(decodeURIComponentSafe("%FF%8F%BF%BF")).equals("%FF%8F%BF%BF") }) o("malformed URI sequence", function() { // "%" only o(() => decodeURIComponent("%")).throws(URIError) o(decodeURIComponentSafe("%")).equals("%") // "%" with one digit o(() => decodeURIComponent("%1")).throws(URIError) o(decodeURIComponentSafe("%1")).equals("%1") // "%" with non-hexadecimal o(() => decodeURIComponent("%G0")).throws(URIError) o(decodeURIComponentSafe("%G0")).equals("%G0") // "%" in string o(() => decodeURIComponent("x%y")).throws(URIError) o(decodeURIComponentSafe("x%y")).equals("x%y") // Overlong encoding o(() => decodeURIComponent("%E0%80%AF")).throws(URIError) o(decodeURIComponentSafe("%E0%80%AF")).equals("%E0%80%AF") // surrogate o(() => decodeURIComponent("%ED%A0%80")).throws(URIError) o(decodeURIComponentSafe("%ED%A0%80")).equals("%ED%A0%80") }) })