Compare commits
90 commits
1.0
...
upstream/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
491acc2d64 | ||
|
|
9365aa2e52 | ||
|
|
b6e979b873 | ||
|
|
19ae86221b | ||
|
|
08c4983721 | ||
|
|
10a3acfb62 | ||
|
|
75cddea7ce | ||
|
|
248bfec3c4 | ||
|
|
24b447eeb3 | ||
|
|
03ea073d46 | ||
|
|
05b6f7a08d | ||
|
|
ab819fa534 | ||
|
|
652fe1bc86 | ||
|
|
9a4cf52cd6 | ||
|
|
e9565fbb1f | ||
|
|
5e6adbfe19 | ||
|
|
bb2ea5f183 | ||
|
|
c1ed921716 | ||
|
|
1d46644e9b | ||
|
|
612f748595 | ||
|
|
619b96f901 | ||
|
|
bd88f95079 | ||
|
|
0744a3eb3a | ||
|
|
d75589873f | ||
|
|
1afa78682d | ||
|
|
2e4baeefc4 | ||
|
|
90562b145f | ||
|
|
6d04874961 | ||
|
|
6c31329c83 | ||
|
|
1933672b81 | ||
|
|
09210bc05e | ||
|
|
6523f66497 | ||
|
|
359605012f | ||
|
|
f16557016d | ||
|
|
354d5039c4 | ||
|
|
2c78996286 | ||
|
|
a20855e4a8 | ||
|
|
52d094dd0e | ||
|
|
ce1b63420f | ||
|
|
ddd75e2206 | ||
|
|
999f4f676b | ||
|
|
33b5e9ae76 | ||
|
|
6f683630ef | ||
|
|
a068b53a7e | ||
|
|
24b094d1cc | ||
|
|
d8bea6b8d4 | ||
|
|
bbec60788f | ||
|
|
e884d518b6 | ||
|
|
0644776001 | ||
|
|
c64ffc8eaa | ||
|
|
661426329a | ||
|
|
cb6cc0652f | ||
|
|
d309edfe9d | ||
|
|
207416957e | ||
|
|
f4eb1ebaa5 | ||
|
|
bf927da6ba | ||
|
|
2fdc20cc2e | ||
|
|
f20363a3c4 | ||
|
|
1b06e553f9 | ||
|
|
7722c8ab06 | ||
|
|
0b592ff361 | ||
|
|
6fcb6e6359 | ||
|
|
76f4bb1e86 | ||
|
|
4c22ba45f6 | ||
|
|
480c9cee0f | ||
|
|
00db41cea3 | ||
|
|
42b6dd8a8d | ||
|
|
dbff607c76 | ||
|
|
9566af6fe6 | ||
|
|
69d5d5bf75 | ||
|
|
c0a1bc139b | ||
|
|
adc3cca860 | ||
|
|
2550aebedb | ||
|
|
ed6143a8cc | ||
|
|
ef8d58c988 | ||
|
|
dce4dfeaa0 | ||
|
|
714b5b4cb2 | ||
|
|
731c9e871d | ||
|
|
e1125d91bc | ||
|
|
52998e962d | ||
|
|
b840b30f7a | ||
|
|
8787833334 | ||
|
|
1e6af3de5f | ||
|
|
281b94e7ae | ||
|
|
694d89c47e | ||
|
|
600ccf0e64 | ||
|
|
9bae5f5ef1 | ||
|
|
3ceae4f0e9 | ||
|
|
6b63846bad | ||
|
|
a1e842164b |
50
.eslintignore
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/node_modules
|
||||
**/dist
|
||||
package-lock.json
|
||||
**/.angular
|
||||
|
||||
/resources/tentative/*
|
||||
|
||||
/resources/todomvc/architecture-examples/*
|
||||
!/resources/todomvc/architecture-examples/angular
|
||||
!/resources/todomvc/architecture-examples/angular-complex
|
||||
!/resources/todomvc/architecture-examples/backbone
|
||||
!/resources/todomvc/architecture-examples/backbone-complex
|
||||
!/resources/todomvc/architecture-examples/lit
|
||||
/resources/todomvc/architecture-examples/lit/.wireit
|
||||
/resources/todomvc/architecture-examples/lit/dist/index.js
|
||||
!/resources/todomvc/architecture-examples/lit-complex
|
||||
!/resources/todomvc/architecture-examples/preact
|
||||
!/resources/todomvc/architecture-examples/preact-complex
|
||||
!/resources/todomvc/architecture-examples/react
|
||||
!/resources/todomvc/architecture-examples/react-complex
|
||||
!/resources/todomvc/architecture-examples/react-redux
|
||||
!/resources/todomvc/architecture-examples/react-redux-complex
|
||||
!/resources/todomvc/architecture-examples/svelte
|
||||
!/resources/todomvc/architecture-examples/svelte-complex
|
||||
!/resources/todomvc/architecture-examples/vue
|
||||
!/resources/todomvc/architecture-examples/vue-complex
|
||||
|
||||
/resources/todomvc/vanilla-examples/*
|
||||
!/resources/todomvc/vanilla-examples/javascript-es5
|
||||
!/resources/todomvc/vanilla-examples/javascript-es5-complex
|
||||
!/resources/todomvc/vanilla-examples/javascript-es6
|
||||
!/resources/todomvc/vanilla-examples/javascript-es6-complex
|
||||
!/resources/todomvc/vanilla-examples/javascript-es6-webpack
|
||||
!/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex
|
||||
!/resources/todomvc/vanilla-examples/javascript-web-components
|
||||
!/resources/todomvc/vanilla-examples/javascript-web-components-complex/
|
||||
|
||||
!/resources/todomvc/big-dom-generator
|
||||
|
||||
!/resources/editors
|
||||
/resources/editors/assets
|
||||
|
||||
!/resources/newssite
|
||||
!/resources/newssite/news-next
|
||||
!/resources/newssite/news-nuxt
|
||||
!/resources/newssite/news-site-css
|
||||
|
||||
/resources/perf.webkit.org
|
||||
|
||||
/resources/react-stockcharts
|
||||
100
.eslintrc
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* Copyright (C) 2014 Apple Inc. All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
||||
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
||||
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
||||
* THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
{
|
||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/base"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"worker": true,
|
||||
"es2022": true,
|
||||
"mocha": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
},
|
||||
"globals": {
|
||||
// unit test specific
|
||||
"sinon": true,
|
||||
"expect": true,
|
||||
// injected dependencies
|
||||
"testRunner": true,
|
||||
"startBenchmark": true,
|
||||
"PerfTestRunner": true,
|
||||
"deltas": true
|
||||
},
|
||||
"rules": {
|
||||
"array-bracket-spacing": [2, "never"],
|
||||
"block-spacing": [2, "always"],
|
||||
"brace-style": 2,
|
||||
"comma-dangle": [2, "only-multiline"],
|
||||
"comma-spacing": [2, { "before": false, "after": true }],
|
||||
"comma-style": [2, "last"],
|
||||
"consistent-return": 2,
|
||||
"curly": ["error", "multi-or-nest", "consistent"],
|
||||
"dot-notation": 0,
|
||||
"eol-last": 2,
|
||||
"eqeqeq": 2,
|
||||
"indent": ["error", 4, { "SwitchCase": 1 }],
|
||||
"func-call-spacing": [2, "never"],
|
||||
"key-spacing": [2, { "beforeColon": false, "afterColon": true }],
|
||||
"keyword-spacing": [2, { "before": true, "after": true }],
|
||||
"new-cap": 0,
|
||||
"new-parens": 0,
|
||||
"no-console": 0,
|
||||
"no-constant-condition": 0,
|
||||
"no-extra-bind": 2,
|
||||
"no-extra-parens": [2, "all", { "nestedBinaryExpressions": false }],
|
||||
"no-extra-semi": 2,
|
||||
"no-global-assign": 2,
|
||||
"no-inner-declarations": 0,
|
||||
"no-multi-spaces": 2,
|
||||
"no-multiple-empty-lines": ["error", { "max": 1 }],
|
||||
"no-new": 0,
|
||||
"no-proto": 0,
|
||||
"no-redeclare": 0,
|
||||
"no-return-assign": 2,
|
||||
"no-return-await": 2,
|
||||
"no-self-compare": 2,
|
||||
"no-shadow": 0,
|
||||
"no-trailing-spaces": 2,
|
||||
"no-undef": 2,
|
||||
"no-underscore-dangle": 0,
|
||||
"no-unused-expressions": 2,
|
||||
"no-unused-vars": [2, { "vars": "all", "args": "none" }],
|
||||
"no-use-before-define": 0,
|
||||
"nonblock-statement-body-position": ["error", "below"],
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"operator-linebreak": ["error", "before"],
|
||||
"prefer-template": 2,
|
||||
"quotes": [2, "double", { "avoidEscape": true }],
|
||||
"semi": 2,
|
||||
"semi-spacing": [2, { "before": false, "after": true }],
|
||||
"space-infix-ops": 2,
|
||||
"spaced-comment": [2, "always"],
|
||||
"strict": 0,
|
||||
"valid-typeof": 2
|
||||
}
|
||||
}
|
||||
50
.github/workflows/test.yml
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
linters:
|
||||
name: Linters
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout Branch
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.13.0
|
||||
- name: Install
|
||||
run: |
|
||||
npm install
|
||||
- name: Run linters
|
||||
run: |
|
||||
npm run format
|
||||
build:
|
||||
name: Build
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
browser: [chrome, firefox, safari]
|
||||
env:
|
||||
BROWSER: ${{ matrix.browser }}
|
||||
steps:
|
||||
- name: Checkout Branch
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.13.0
|
||||
- name: Install
|
||||
run: |
|
||||
npm install
|
||||
- name: Run tests
|
||||
run: |
|
||||
echo "Running in $BROWSER"
|
||||
node tests/run.mjs
|
||||
33
.gitignore
vendored
|
|
@ -2,3 +2,36 @@
|
|||
*.pyc
|
||||
.DS_Store
|
||||
.directory
|
||||
/node_modules
|
||||
|
||||
.vscode
|
||||
.angular
|
||||
|
||||
/resources/todomvc/architecture-examples/angular/node_modules/
|
||||
/resources/todomvc/architecture-examples/angular-complex/node_modules/
|
||||
/resources/todomvc/architecture-examples/backbone/node_modules/
|
||||
/resources/todomvc/architecture-examples/backbone-complex/node_modules/
|
||||
/resources/todomvc/architecture-examples/jquery/node_modules/
|
||||
/resources/todomvc/architecture-examples/jquery-complex/node_modules/
|
||||
/resources/todomvc/architecture-examples/preact/node_modules/
|
||||
/resources/todomvc/architecture-examples/preact-complex/node_modules/
|
||||
/resources/todomvc/architecture-examples/react/node_modules/
|
||||
/resources/todomvc/architecture-examples/react-complex/node_modules/
|
||||
/resources/todomvc/architecture-examples/react-redux/node_modules/
|
||||
/resources/todomvc/architecture-examples/react-redux-complex/node_modules/
|
||||
/resources/todomvc/architecture-examples/svelte/node_modules/
|
||||
/resources/todomvc/architecture-examples/svelte-complex/node_modules/
|
||||
/resources/todomvc/architecture-examples/vue/node_modules/
|
||||
/resources/todomvc/architecture-examples/vue-complex/node_modules/
|
||||
/resources/todomvc/architecture-examples/lit-complex/node_modules/
|
||||
|
||||
/resources/todomvc/vanilla-examples/javascript-es5/node_modules/
|
||||
/resources/todomvc/vanilla-examples/javascript-es5-complex/node_modules/
|
||||
/resources/todomvc/vanilla-examples/javascript-es6/node_modules/
|
||||
/resources/todomvc/vanilla-examples/javascript-es6-complex/node_modules/
|
||||
/resources/todomvc/vanilla-examples/javascript-es6-webpack/node_modules/
|
||||
/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/node_modules/
|
||||
/resources/todomvc/vanilla-examples/javascript-web-components-complex/node_modules/
|
||||
|
||||
/resources/charts/node_modules/
|
||||
/resources/todomvc/big-dom-generator/node_modules/
|
||||
|
|
|
|||
1
.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
engine-strict = true
|
||||
50
.prettierignore
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/node_modules
|
||||
**/dist
|
||||
package-lock.json
|
||||
**/.angular
|
||||
|
||||
/resources/tentative/*
|
||||
|
||||
/resources/todomvc/architecture-examples/*
|
||||
!/resources/todomvc/architecture-examples/angular
|
||||
!/resources/todomvc/architecture-examples/angular-complex
|
||||
!/resources/todomvc/architecture-examples/backbone
|
||||
!/resources/todomvc/architecture-examples/backbone-complex
|
||||
!/resources/todomvc/architecture-examples/lit
|
||||
/resources/todomvc/architecture-examples/lit/dist/index.js
|
||||
/resources/todomvc/architecture-examples/lit/.wireit
|
||||
!/resources/todomvc/architecture-examples/lit-complex
|
||||
!/resources/todomvc/architecture-examples/preact
|
||||
!/resources/todomvc/architecture-examples/preact-complex
|
||||
!/resources/todomvc/architecture-examples/react
|
||||
!/resources/todomvc/architecture-examples/react-complex
|
||||
!/resources/todomvc/architecture-examples/react-redux
|
||||
!/resources/todomvc/architecture-examples/react-redux-complex
|
||||
!/resources/todomvc/architecture-examples/svelte
|
||||
!/resources/todomvc/architecture-examples/svelte-complex
|
||||
!/resources/todomvc/architecture-examples/vue
|
||||
!/resources/todomvc/architecture-examples/vue-complex
|
||||
|
||||
/resources/todomvc/vanilla-examples/*
|
||||
!/resources/todomvc/vanilla-examples/javascript-es5
|
||||
!/resources/todomvc/vanilla-examples/javascript-es5-complex
|
||||
!/resources/todomvc/vanilla-examples/javascript-es6
|
||||
!/resources/todomvc/vanilla-examples/javascript-es6-complex
|
||||
!/resources/todomvc/vanilla-examples/javascript-es6-webpack
|
||||
!/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex
|
||||
!/resources/todomvc/vanilla-examples/javascript-web-components
|
||||
!/resources/todomvc/vanilla-examples/javascript-web-components-complex/
|
||||
|
||||
!/resources/todomvc/big-dom-generator
|
||||
|
||||
!/resources/editors
|
||||
/resources/editors/assets
|
||||
|
||||
!/resources/newssite
|
||||
!/resources/newssite/news-next
|
||||
!/resources/newssite/news-nuxt
|
||||
!/resources/newssite/news-site-css
|
||||
|
||||
/resources/perf.webkit.org
|
||||
|
||||
/resources/react-stockcharts
|
||||
12
.prettierrc
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"arrowParens": "always",
|
||||
"bracketSameLine": false,
|
||||
"bracketSpacing": true,
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"printWidth": 250,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "es5",
|
||||
"useTabs": false
|
||||
}
|
||||
34
Development.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Development Environment
|
||||
|
||||
## Prerequisite
|
||||
|
||||
Node is required for local development.
|
||||
We recommend using a Node version manager ([nvm](https://github.com/nvm-sh/nvm) for example) to ensure a stable development environment is used.
|
||||
Below are the required node / npm versions:
|
||||
|
||||
```sh
|
||||
"node": ">18.13.0",
|
||||
"npm": ">8.19.3"
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
In your working directory, open terminal and paste the following commands:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/WebKit/Speedometer.git
|
||||
cd Speedometer
|
||||
npm install
|
||||
```
|
||||
|
||||
## Run Development Server
|
||||
|
||||
1. In your terminal run:
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
2. Open your browser of choice and navigate to [http://127.0.0.1:7000](http://127.0.0.1:7000)
|
||||
|
||||
## Local Server
|
||||
|
||||
Speedometer uses [http-server](https://github.com/http-party/http-server), which is a static HTTP server. Meaning it does not provide hot-reloading. By default, the dev script disables caching and local changes can be viewed by simply refreshing your browser window. Additional options of the http-server can be found [here](https://github.com/http-party/http-server#available-options).
|
||||
34
Governance.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Speedometer Governance Policy
|
||||
|
||||
Speedometer uses multistakeholder governance.
|
||||
This will allow us to share work, and build a collaborative understanding of performance on the web
|
||||
in order to drive resourcing towards appropriate areas.
|
||||
This also provides a structure that can endure to provide maintenance and adapt to the future web.
|
||||
|
||||
An eligible “browser project” is a core end-to-end web browser engine with an integrated JavaScript engine
|
||||
which distributes implementations widely. The project may delegate decision making within Speedometer
|
||||
to multiple representatives (for example, to review code commits or to provide consensus for major changes).
|
||||
The participating browser projects at this time are Blink/V8, Gecko/SpiderMonkey, and WebKit/JavaScriptCore.
|
||||
|
||||
The intent is that the working team should be able to move quickly for most changes,
|
||||
with a higher level of process and consensus expected based on the impact of the change.
|
||||
|
||||
- **Trivial change** - This is a change that has no effect on the official benchmark and includes changes
|
||||
to whitespaces, comments, documentation outside policies and governance model, and unofficial test cases.
|
||||
A trivial change requires approval by a reviewer, who is not the author of the change,
|
||||
from one of the participating browser projects.
|
||||
The intent is to ensure basic code quality & license compatibility, not to reach agreement.
|
||||
For example, one participating browser project might be both writing and reviewing a new benchmark in
|
||||
a subfolder to test in their own CI, or reviewing code written by an external contributor.
|
||||
- **Non-trivial change** - This is a change that has small impact on the official benchmark and includes
|
||||
changes to official test cases, test runners, bug fixes, and the appearance of the benchmark.
|
||||
A non-trivial change requires approval by at least two of the participating browser projects
|
||||
(including either authoring or reviewing the change) and none other strongly opposed to the change
|
||||
within 10 business days.
|
||||
- **Major change** - This is a change that has major implications on the official benchmark such as
|
||||
releasing of a new version of the benchmark or any revisions to governance policies and processes,
|
||||
including changes to the participating browser projects.
|
||||
A major change requires a consensus, meaning approvals by each of the participating browser projects.
|
||||
|
||||
This governance policy and associated code will be hosted inside the Speedometer repository within
|
||||
the WebKit GitHub organization under the 2-clause BSD license.
|
||||
40
InteractiveRunner.html
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=850" />
|
||||
<title>Speedometer 3.0 Interactive Runner</title>
|
||||
<script src="resources/interactive.mjs" type="module"></script>
|
||||
<link rel="icon" href="resources/favicon.png" />
|
||||
<style>
|
||||
iframe {
|
||||
border: 1px solid black;
|
||||
}
|
||||
ol {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
ol ol {
|
||||
margin-left: 2em;
|
||||
list-style-position: outside;
|
||||
}
|
||||
.running {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.ran {
|
||||
color: grey;
|
||||
}
|
||||
nav {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
height: 600px;
|
||||
}
|
||||
nav > ol {
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
4
LICENSE
|
|
@ -1,4 +1,6 @@
|
|||
Copyright (C) 2022 Apple Inc. All rights reserved.
|
||||
Copyright (C) 2013-2022 Apple Inc. All rights reserved.
|
||||
Copyright (C) 2017 Google Inc. All rights reserved.
|
||||
Copyright (C) 2022-2023 Mozilla Corporation. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
|
|
|
|||
19
README.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
| :warning: **Speedometer 3 is in active development and is unstable**. You can follow along with development in this repository, but see [Speedometer 2.1](https://browserbench.org/Speedometer2.1/) for the latest stable version. |
|
||||
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|
||||
# What is Speedometer?
|
||||
|
||||
Speedometer is a benchmark for web browsers that measures Web application responsiveness
|
||||
by timing simulated user interactions on various workloads. Our primary goal is to make it
|
||||
reflect the real-world Web as much as possible. When a browser improves its score on the
|
||||
benchmark, actual users should benefit. In order to achieve this, it should:
|
||||
|
||||
- Test end-to-end user journeys instead of testing specific features in a tight loop. Each
|
||||
test should exercise the full set of what’s needed from the engine in order for a user to
|
||||
accomplish a task.
|
||||
- Evolve over time, adapting to the present Web on a regular basis. This should be informed
|
||||
by current usage data, and by consensus about features which are important for engines to
|
||||
optimize to provide a consistent experience for users and site authors.
|
||||
- Be accessible to the public and useful to browser engineers. It should run in every modern
|
||||
browser by visiting a normal web page. It should run relatively quickly, while providing
|
||||
enough test coverage to be reflective of the real-world Web.
|
||||
27
Testing.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Testing
|
||||
|
||||
Speedometer uses [Selenium](https://www.selenium.dev/) for testing of the application itself.
|
||||
Tests are located in the `/tests` folder.
|
||||
|
||||
[Sinon](https://sinonjs.org/): Standalone test spies, stubs and mocks for JavaScript.
|
||||
[Mocha](https://mochajs.org/): Testing framework.
|
||||
|
||||
## Local Testing
|
||||
|
||||
To run this locally you'll need the browsers installed along with the corresponding driver:
|
||||
|
||||
- [chromedriver](https://chromedriver.chromium.org/getting-started)
|
||||
- [geckodriver](https://github.com/mozilla/geckodriver/releases)
|
||||
- [safaridriver](https://developer.apple.com/documentation/webkit/testing_with_webdriver_in_safari).
|
||||
|
||||
Once installed you can run the following scripts:
|
||||
|
||||
```bash
|
||||
npm run test:chrome
|
||||
npm run test:firefox
|
||||
npm run test:safari
|
||||
```
|
||||
|
||||
## Automated Testing
|
||||
|
||||
Currently Speedometer's tests run automatically, when pushing to the main branch or when opening a pr.
|
||||
10
babel.config.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"plugins": [
|
||||
[
|
||||
"@babel/plugin-proposal-decorators",
|
||||
{
|
||||
"legacy": true
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
181
index.html
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=850" />
|
||||
<title>Speedometer 3.0</title>
|
||||
<link rel="stylesheet" href="resources/main.css" />
|
||||
<link rel="icon" href="resources/favicon.png" />
|
||||
<script src="resources/main.mjs" type="module"></script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<section id="home">
|
||||
<a href="#home" class="logo">
|
||||
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
|
||||
<div class="version">3.0</div>
|
||||
</a>
|
||||
<div class="content">
|
||||
<p>Speedometer is a browser benchmark that measures the responsiveness of Web applications. It uses demo web applications to simulate user actions such as adding to-do items.</p>
|
||||
<p id="screen-size-warning">
|
||||
<strong>
|
||||
Your browser window is too small. For most accurate results, please make the view port size at least <span id="min-screen-width">850px</span> by <span id="min-screen-height">650px</span>.<br />
|
||||
It's currently <span id="screen-size"></span>.
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<div class="button-row">
|
||||
<button class="start-tests-button">Start Test</button>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<a href="#about">About Speedometer</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="running">
|
||||
<a href="#home" class="logo">
|
||||
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
|
||||
<div class="version">2.1 3.0</div>
|
||||
</a>
|
||||
<div id="testContainer"></div>
|
||||
<div id="progress">
|
||||
<progress aria-label="Progress" id="progress-completed"></progress>
|
||||
</div>
|
||||
<div id="info">
|
||||
<div id="info-label"></div>
|
||||
<div id="info-progress"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="summary">
|
||||
<a href="#home" class="logo">
|
||||
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
|
||||
<div class="version">3.0</div>
|
||||
</a>
|
||||
<h1>Score</h1>
|
||||
<div class="gauge">
|
||||
<div class="window"><div class="needle"></div></div>
|
||||
</div>
|
||||
<hr />
|
||||
<div id="result-number"></div>
|
||||
<div id="confidence-number"></div>
|
||||
<div class="buttons">
|
||||
<div class="button-row">
|
||||
<button class="start-tests-button" title="Discard the current results and re-run all tests.">Test Again</button>
|
||||
<a class="button" href="#details" id="show-details" title="Show detailed results data.">Details</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="details">
|
||||
<a href="#home" class="logo">
|
||||
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
|
||||
<div class="version">3.0</div>
|
||||
</a>
|
||||
<div class="section-grid">
|
||||
<h1 class="section-header">Detailed Results</h1>
|
||||
<div class="section-content all-metric-results">
|
||||
<div class="aggregated-metric-result">
|
||||
<h2>Geomean</h2>
|
||||
<div id="geomean-chart"></div>
|
||||
<h2>Tests</h2>
|
||||
<div id="tests-chart"></div>
|
||||
</div>
|
||||
<br />
|
||||
<h2>Detailed Metrics</h2>
|
||||
<div id="metrics-results"></div>
|
||||
</div>
|
||||
<div class="buttons section-footer">
|
||||
<div class="button-row">
|
||||
<button class="start-tests-button" title="Discard the current results and re-run all tests.">Test Again</button>
|
||||
<a class="button" href="#summary" title="Go back to the simplified summary view.">Summary</a>
|
||||
</div>
|
||||
<div class="button-row export-buttons">
|
||||
<button id="copy-full-json" title="Copy full result metrics as json string.">Copy JSON</button>
|
||||
<a class="button" id="download-full-json" title="Download full result metrics as json string.">Download JSON</a>
|
||||
<a class="button" id="download-classic-json" title="Download backwards-compatible result metrics as json string.">Download Classic JSON</a>
|
||||
<a class="button" id="download-csv" title="Download all result metrics as CSV string.">Download CSV</a>
|
||||
<button id="copy-csv" title="Copy all result metrics as CSV string.">Copy CSV</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="about">
|
||||
<a href="#home" class="logo">
|
||||
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
|
||||
<div class="version">3.0</div>
|
||||
</a>
|
||||
<div class="section-grid">
|
||||
<h1 class="section-header">About Speedometer 3</h1>
|
||||
<div class="section-content">
|
||||
<p>Speedometer 3 is a benchmark for web browsers that measures Web application responsiveness by timing simulated user interactions on various workloads.</p>
|
||||
<p>
|
||||
The following high level user journeys are implemented in the current version. Each of these journeys has one or more workloads which test important aspects of it - for example commonly used patterns, frameworks, or
|
||||
technologies.
|
||||
</p>
|
||||
<ul>
|
||||
<li>Working with a todo list</li>
|
||||
<ul>
|
||||
<li>Measures the time to add, complete, and remove 100 todo items in a basic list.</li>
|
||||
<li>Each example implements the same todo application (TodoMVC) using different techniques and frameworks.</li>
|
||||
<li>
|
||||
Workloads: <a href="resources/todomvc/vanilla-examples/javascript-es5/readme.md" target="_blank">TodoMVC-JavaScript-ES5</a>,
|
||||
<a href="resources/todomvc/vanilla-examples/javascript-web-components/readme.md" target="_blank">TodoMVC-WebComponents</a>,
|
||||
<a href="resources/todomvc/architecture-examples/react-complex/readme.md" target="_blank">TodoMVC-React-Complex-DOM</a>,
|
||||
<a href="resources/todomvc/architecture-examples/backbone/readme.md" target="_blank">TodoMVC-Backbone</a>, <a href="resources/todomvc/architecture-examples/angular/readme.md" target="_blank">TodoMVC-Angular</a>,
|
||||
<a href="resources/todomvc/architecture-examples/vue/readme.md" target="_blank">TodoMVC-Vue</a>, <a href="resources/todomvc/architecture-examples/jquery/readme.md" target="_blank">TodoMVC-jQuery</a>,
|
||||
<a href="resources/todomvc/architecture-examples/preact/readme.md" target="_blank">TodoMVC-Preact</a>, <a href="resources/todomvc/architecture-examples/svelte/readme.md" target="_blank">TodoMVC-Svelte</a>,
|
||||
<a href="resources/todomvc/architecture-examples/lit/readme.md" target="_blank">TodoMVC-Lit</a>
|
||||
</li>
|
||||
</ul>
|
||||
<li>Editing rich text</li>
|
||||
<ul>
|
||||
<li>Loading and styling text inside WYSIWYG and code editors.</li>
|
||||
<li>Workloads: <a href="resources/editors/readme.md" target="_blank">Editor-CodeMirror</a>, <a href="resources/editors/readme.md" target="_blank">Editor-TipTap</a></li>
|
||||
</ul>
|
||||
<li>Rendering charts</li>
|
||||
<ul>
|
||||
<li>Loading and interacting with SVG and canvas charts.</li>
|
||||
<li>
|
||||
Workloads: <a href="resources/charts/readme.md" target="_blank">Charts-observable-plot</a>, <a href="resources/charts/readme.md" target="_blank">Charts-chartjs</a>,
|
||||
<a href="resources/react-stockcharts/readme.md" target="_blank">React-Stockcharts-SVG</a>, <a href="resources/perf.webkit.org/readme.md" target="_blank">Perf-Dashboard</a>
|
||||
</li>
|
||||
</ul>
|
||||
<li>Reading a news site</li>
|
||||
<ul>
|
||||
<li>Navigating across pages and interacting with a typical looking news site.</li>
|
||||
<li>Workloads: <a href="resources/newssite/news-next/readme.md" target="_blank">NewsSite-Next</a>, <a href="resources/newssite/news-nuxt/readme.md" target="_blank">NewsSite-Nuxt</a></li>
|
||||
</ul>
|
||||
</ul>
|
||||
|
||||
<p class="note"><strong>Notes about methodology</strong></p>
|
||||
<ul>
|
||||
<li>Although user-driven actions like mouse movements and keyboard input cannot be fully emulated in JavaScript, Speedometer does its best to faithfully replay a typical workload within the demo applications.</li>
|
||||
<li>To make the run time long enough to measure with the limited precision, we synchronously execute a large number of the operations, such as adding one hundred to-do items.</li>
|
||||
<li>
|
||||
Modern browser engines execute some work asynchronously as an optimization strategy to reduce the run time of synchronous operations. While returning control back to JavaScript execution as soon as possible is worth
|
||||
pursuing, the run time cost of such an asynchronous work should still be taken into a holistic measurement of web application performance. In addition, some JavaScript frameworks call into DOM APIs asynchronously as an
|
||||
optimization technique. Speedometer approximates the run time of this asynchronous work in the UI thread with a zero-second timer that is scheduled immediately after each execution of synchronous operations.
|
||||
</li>
|
||||
<li>Speedometer does not attempt to measure concurrent asynchronous work (e.g. in Web Workers).</li>
|
||||
<li>Speedometer should not be used as a way to compare the performance of different JavaScript frameworks.</li>
|
||||
<li>
|
||||
The goal of all workloads is to represent a scenario that could be found on the Web. Although all workloads strive to use patterns that are commonly used, some implementation details are Speedometer specific and should
|
||||
not be used as a guideline on how to implement and deploy a standalone app. For example, due to constraints within the test harness, workloads must not depend on a server infrastructure to function properly and are
|
||||
built as static files ahead of time.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="buttons section-footer">
|
||||
<div class="button-row">
|
||||
<a class="button" href="#home" title="Show main section.">Home</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
11378
package-lock.json
generated
Normal file
51
package.json
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"name": "speedometer",
|
||||
"version": "3.0.0-alpha",
|
||||
"description": "An open source repository for the Speedometer benchmark.",
|
||||
"engines": {
|
||||
"node": ">=18.13.0",
|
||||
"npm": ">=8.19.3"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/WebKit/Speedometer.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/WebKit/Speedometer/issues"
|
||||
},
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"scripts": {
|
||||
"dev": "http-server ./ -p 7000 -c-1 --cors",
|
||||
"server": "http-server ./ -p 7000 --cors",
|
||||
"lint:check": "eslint **/*.mjs",
|
||||
"lint:fix": "eslint \"**/*.{js,mjs,jsx,ts}\" --fix",
|
||||
"pretty:check": "prettier --check ./",
|
||||
"pretty:fix": "prettier --write ./",
|
||||
"format": "npm run pretty:fix ; npm run lint:fix",
|
||||
"test:chrome": "BROWSER=chrome node tests/run.mjs",
|
||||
"test:firefox": "BROWSER=firefox node tests/run.mjs",
|
||||
"test:safari": "BROWSER=safari node tests/run.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.3",
|
||||
"@babel/eslint-parser": "^7.21.3",
|
||||
"@babel/plugin-proposal-decorators": "^7.21.0",
|
||||
"@nuxt/eslint-config": "^0.1.1",
|
||||
"@next/eslint-plugin-next": "^13.4.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.0",
|
||||
"@typescript-eslint/parser": "^5.59.0",
|
||||
"command-line-args": "^5.2.1",
|
||||
"command-line-usage": "^6.1.3",
|
||||
"eslint": "^8.38.0",
|
||||
"eslint-plugin-ember": "^11.4.8",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-vue": "^9.10.0",
|
||||
"expect.js": "^0.3.1",
|
||||
"http-server": "^14.1.1",
|
||||
"mocha": "^10.2.0",
|
||||
"prettier": "^2.8.3",
|
||||
"selenium-webdriver": "^4.8.0",
|
||||
"sinon": "^15.0.1",
|
||||
"typescript": "^5.0.4"
|
||||
}
|
||||
}
|
||||
597
resources/benchmark-runner.mjs
Normal file
|
|
@ -0,0 +1,597 @@
|
|||
import { Metric } from "./metric.mjs";
|
||||
import { params } from "./params.mjs";
|
||||
|
||||
const performance = globalThis.performance;
|
||||
|
||||
export class BenchmarkTestStep {
|
||||
constructor(testName, testFunction) {
|
||||
this.name = testName;
|
||||
this.run = testFunction;
|
||||
}
|
||||
}
|
||||
|
||||
function getParent(lookupStartNode, path) {
|
||||
const parent = path.reduce((root, selector) => {
|
||||
const node = root.querySelector(selector);
|
||||
return node.shadowRoot ?? node;
|
||||
}, lookupStartNode);
|
||||
|
||||
return parent;
|
||||
}
|
||||
|
||||
class Page {
|
||||
constructor(frame) {
|
||||
this._frame = frame;
|
||||
}
|
||||
|
||||
layout() {
|
||||
const body = this._frame.contentDocument.body.getBoundingClientRect();
|
||||
this.layout.e = document.elementFromPoint((body.width / 2) | 0, (body.height / 2) | 0);
|
||||
}
|
||||
|
||||
async waitForElement(selector) {
|
||||
return new Promise((resolve) => {
|
||||
const resolveIfReady = () => {
|
||||
const element = this.querySelector(selector);
|
||||
let callback = resolveIfReady;
|
||||
if (element)
|
||||
callback = () => resolve(element);
|
||||
window.requestAnimationFrame(callback);
|
||||
};
|
||||
resolveIfReady();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first element within the document that matches the specified selector, or group of selectors.
|
||||
* If no matches are found, null is returned.
|
||||
*
|
||||
* An optional path param is added to be able to target elements within a shadow DOM or nested shadow DOMs.
|
||||
*
|
||||
* @example
|
||||
* // DOM structure: <todo-app> -> #shadow-root -> <todo-list> -> #shadow-root -> <todo-item>
|
||||
* // return PageElement(<todo-item>)
|
||||
* querySelector("todo-item", ["todo-app", "todo-list"]);
|
||||
*
|
||||
* @param {string} selector A string containing one or more selectors to match.
|
||||
* @param {string[]} [path] An array containing a path to the parent element.
|
||||
* @returns PageElement | null
|
||||
*/
|
||||
querySelector(selector, path = []) {
|
||||
const lookupStartNode = this._frame.contentDocument;
|
||||
const element = getParent(lookupStartNode, path).querySelector(selector);
|
||||
|
||||
if (element === null)
|
||||
return null;
|
||||
return this._wrapElement(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all elements within the document that matches the specified selector, or group of selectors.
|
||||
* If no matches are found, null is returned.
|
||||
*
|
||||
* An optional path param is added to be able to target elements within a shadow DOM or nested shadow DOMs.
|
||||
*
|
||||
* @example
|
||||
* // DOM structure: <todo-app> -> #shadow-root -> <todo-list> -> #shadow-root -> <todo-item>
|
||||
* // return [PageElement(<todo-item>), PageElement(<todo-item>)]
|
||||
* querySelectorAll("todo-item", ["todo-app", "todo-list"]);
|
||||
*
|
||||
* @param {string} selector A string containing one or more selectors to match.
|
||||
* @param {string[]} [path] An array containing a path to the parent element.
|
||||
* @returns array
|
||||
*/
|
||||
querySelectorAll(selector, path = []) {
|
||||
const lookupStartNode = this._frame.contentDocument;
|
||||
const elements = Array.from(getParent(lookupStartNode, path).querySelectorAll(selector));
|
||||
for (let i = 0; i < elements.length; i++)
|
||||
elements[i] = this._wrapElement(elements[i]);
|
||||
return elements;
|
||||
}
|
||||
|
||||
getElementById(id) {
|
||||
const element = this._frame.contentDocument.getElementById(id);
|
||||
if (element === null)
|
||||
return null;
|
||||
return this._wrapElement(element);
|
||||
}
|
||||
|
||||
call(functionName) {
|
||||
this._frame.contentWindow[functionName]();
|
||||
return null;
|
||||
}
|
||||
|
||||
callAsync(functionName) {
|
||||
setTimeout(() => {
|
||||
this._frame.contentWindow[functionName]();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
callToGetElement(functionName) {
|
||||
return this._wrapElement(this._frame.contentWindow[functionName]());
|
||||
}
|
||||
|
||||
_wrapElement(element) {
|
||||
return new PageElement(element);
|
||||
}
|
||||
}
|
||||
|
||||
const NATIVE_OPTIONS = {
|
||||
bubbles: true,
|
||||
cancellable: true,
|
||||
};
|
||||
|
||||
class PageElement {
|
||||
#node;
|
||||
|
||||
constructor(node) {
|
||||
this.#node = node;
|
||||
}
|
||||
|
||||
domNode() {
|
||||
return this.#node;
|
||||
}
|
||||
|
||||
setValue(value) {
|
||||
this.#node.value = value;
|
||||
}
|
||||
|
||||
click() {
|
||||
this.#node.click();
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.#node.focus();
|
||||
}
|
||||
|
||||
getElementByMethod(name) {
|
||||
return new PageElement(this.#node[name]());
|
||||
}
|
||||
|
||||
dispatchEvent(eventName, options = NATIVE_OPTIONS, eventType = Event) {
|
||||
if (eventName === "submit")
|
||||
// FIXME FireFox doesn't like `new Event('submit')
|
||||
this._dispatchSubmitEvent();
|
||||
else
|
||||
this.#node.dispatchEvent(new eventType(eventName, options));
|
||||
}
|
||||
|
||||
_dispatchSubmitEvent() {
|
||||
const submitEvent = document.createEvent("Event");
|
||||
submitEvent.initEvent("submit", true, true);
|
||||
this.#node.dispatchEvent(submitEvent);
|
||||
}
|
||||
|
||||
enter(type, options = undefined) {
|
||||
const ENTER_KEY_CODE = 13;
|
||||
return this.dispatchKeyEvent(type, ENTER_KEY_CODE, "Enter", options);
|
||||
}
|
||||
|
||||
dispatchKeyEvent(type, keyCode, key, options) {
|
||||
let eventOptions = { bubbles: true, cancelable: true, keyCode, which: keyCode, key };
|
||||
if (options !== undefined)
|
||||
eventOptions = Object.assign(eventOptions, options);
|
||||
const event = new KeyboardEvent(type, eventOptions);
|
||||
this.#node.dispatchEvent(event);
|
||||
}
|
||||
|
||||
dispatchMouseEvent(type, offsetX, offsetY, options) {
|
||||
const boundingRect = this.#node.getBoundingClientRect();
|
||||
const clientX = offsetX + boundingRect.x;
|
||||
const clientY = offsetY + boundingRect.y;
|
||||
const contentWindow = this.#node.ownerDocument.defaultView;
|
||||
const screenX = clientX + contentWindow.screenX;
|
||||
const screenY = clientY + contentWindow.screenY;
|
||||
let eventOptions = { bubbles: true, cancelable: true, clientX, clientY, screenX, screenY };
|
||||
if (options !== undefined)
|
||||
eventOptions = Object.assign(eventOptions, options);
|
||||
const event = new contentWindow.MouseEvent(type, eventOptions);
|
||||
this.#node.dispatchEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first element found in a node of a PageElement that matches the specified selector, or group of selectors. If a shadow DOM is present in the node, the shadow DOM is used to query.
|
||||
* If no matches are found, null is returned.
|
||||
*
|
||||
* @param {string} selector A string containing one or more selectors to match.
|
||||
* @param {string[]} [path] An array containing a path to the parent element.
|
||||
* @returns PageElement | null
|
||||
*/
|
||||
querySelectorInShadowRoot(selector, path = []) {
|
||||
const lookupStartNode = this.#node.shadowRoot ?? this.#node;
|
||||
const element = getParent(lookupStartNode, path).querySelector(selector);
|
||||
|
||||
if (element === null)
|
||||
return null;
|
||||
return new PageElement(element);
|
||||
}
|
||||
|
||||
querySelector(selector) {
|
||||
const element = this.#node.querySelector(selector);
|
||||
|
||||
if (element === null)
|
||||
return null;
|
||||
return new PageElement(element);
|
||||
}
|
||||
}
|
||||
|
||||
function geomeanToScore(geomean) {
|
||||
return 1000 / geomean;
|
||||
}
|
||||
|
||||
// The WarmupSuite is used to make sure all runner helper functions and
|
||||
// classes are compiled, to avoid unnecessary pauses due to delayed
|
||||
// compilation of runner methods in the middle of the measuring cycle.
|
||||
const WarmupSuite = {
|
||||
name: "Warmup",
|
||||
url: "warmup/index.html",
|
||||
async prepare(page) {
|
||||
await page.waitForElement("#testItem");
|
||||
},
|
||||
tests: [
|
||||
// Make sure to run ever page.method once at least
|
||||
new BenchmarkTestStep("WarmingUpPageMethods", (page) => {
|
||||
let results = [];
|
||||
results.push(page.querySelector(".testItem"));
|
||||
results.push(page.querySelectorAll(".item"));
|
||||
results.push(page.getElementById("testItem"));
|
||||
}),
|
||||
new BenchmarkTestStep("WarmingUpPageElementMethods", (page) => {
|
||||
const item = page.getElementById("testItem");
|
||||
item.setValue("value");
|
||||
item.click();
|
||||
item.focus();
|
||||
item.dispatchEvent("change");
|
||||
item.enter("keypress");
|
||||
item.dispatchEvent("input");
|
||||
item.enter("keyup");
|
||||
}),
|
||||
new BenchmarkTestStep("WarmingUpPageElementMouseMethods", (page) => {
|
||||
const item = page.getElementById("testItem");
|
||||
const mouseEventOptions = { clientX: 100, clientY: 100, bubbles: true, cancelable: true };
|
||||
const wheelEventOptions = {
|
||||
clientX: 200,
|
||||
clientY: 200,
|
||||
deltaMode: 0,
|
||||
delta: -10,
|
||||
deltaY: -10,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
};
|
||||
item.dispatchEvent("mousedown", mouseEventOptions, MouseEvent);
|
||||
item.dispatchEvent("mousemove", mouseEventOptions, MouseEvent);
|
||||
item.dispatchEvent("mouseup", mouseEventOptions, MouseEvent);
|
||||
item.dispatchEvent("wheel", wheelEventOptions, WheelEvent);
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
class TestInvoker {
|
||||
constructor(syncCallback, asyncCallback, reportCallback) {
|
||||
this._syncCallback = syncCallback;
|
||||
this._asyncCallback = asyncCallback;
|
||||
this._reportCallback = reportCallback;
|
||||
}
|
||||
}
|
||||
|
||||
class TimerTestInvoker extends TestInvoker {
|
||||
start() {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
this._syncCallback();
|
||||
setTimeout(() => {
|
||||
this._asyncCallback();
|
||||
requestAnimationFrame(async () => {
|
||||
await this._reportCallback();
|
||||
resolve();
|
||||
});
|
||||
}, 0);
|
||||
}, params.waitBeforeSync);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class RAFTestInvoker extends TestInvoker {
|
||||
start() {
|
||||
return new Promise((resolve) => {
|
||||
if (params.waitBeforeSync)
|
||||
setTimeout(() => this._scheduleCallbacks(resolve), params.waitBeforeSync);
|
||||
else
|
||||
this._scheduleCallbacks(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
_scheduleCallbacks(resolve) {
|
||||
requestAnimationFrame(() => this._syncCallback());
|
||||
requestAnimationFrame(() => {
|
||||
setTimeout(() => {
|
||||
this._asyncCallback();
|
||||
setTimeout(async () => {
|
||||
await this._reportCallback();
|
||||
resolve();
|
||||
}, 0);
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/47593316
|
||||
function seededHashRandomNumberGenerator(a) {
|
||||
return function () {
|
||||
var t = a += 0x6d2b79f5;
|
||||
t = Math.imul(t ^ (t >>> 15), t | 1);
|
||||
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
||||
return (t ^ (t >>> 14)) >>> 0;
|
||||
};
|
||||
}
|
||||
|
||||
export class BenchmarkRunner {
|
||||
constructor(suites, client) {
|
||||
this._suites = suites;
|
||||
if (params.useWarmupSuite)
|
||||
this._suites = [WarmupSuite, ...suites];
|
||||
this._client = client;
|
||||
this._page = null;
|
||||
this._metrics = null;
|
||||
this._iterationCount = params.iterationCount;
|
||||
if (params.shuffleSeed !== "off")
|
||||
this._suiteOrderRandomNumberGenerator = seededHashRandomNumberGenerator(params.shuffleSeed);
|
||||
}
|
||||
|
||||
async runMultipleIterations(iterationCount) {
|
||||
this._iterationCount = iterationCount;
|
||||
if (this._client?.willStartFirstIteration)
|
||||
await this._client.willStartFirstIteration(iterationCount);
|
||||
|
||||
const iterationStartLabel = "iteration-start";
|
||||
const iterationEndLabel = "iteration-end";
|
||||
for (let i = 0; i < iterationCount; i++) {
|
||||
performance.mark(iterationStartLabel);
|
||||
await this._runAllSuites();
|
||||
performance.mark(iterationEndLabel);
|
||||
performance.measure(`iteration-${i}`, iterationStartLabel, iterationEndLabel);
|
||||
}
|
||||
|
||||
if (this._client?.didFinishLastIteration)
|
||||
await this._client.didFinishLastIteration(this._metrics);
|
||||
}
|
||||
|
||||
_removeFrame() {
|
||||
if (this._frame) {
|
||||
this._frame.parentNode.removeChild(this._frame);
|
||||
this._frame = null;
|
||||
}
|
||||
}
|
||||
|
||||
async _appendFrame(src) {
|
||||
const frame = document.createElement("iframe");
|
||||
const style = frame.style;
|
||||
style.width = `${params.viewport.width}px`;
|
||||
style.height = `${params.viewport.height}px`;
|
||||
style.border = "0px none";
|
||||
style.position = "absolute";
|
||||
frame.setAttribute("scrolling", "no");
|
||||
frame.className = "test-runner";
|
||||
style.left = "50%";
|
||||
style.top = "50%";
|
||||
style.transform = "translate(-50%, -50%)";
|
||||
|
||||
if (this._client?.willAddTestFrame)
|
||||
await this._client.willAddTestFrame(frame);
|
||||
|
||||
document.body.insertBefore(frame, document.body.firstChild);
|
||||
this._frame = frame;
|
||||
return frame;
|
||||
}
|
||||
|
||||
async _runAllSuites() {
|
||||
this._measuredValues = { tests: {}, total: 0, mean: NaN, geomean: NaN, score: NaN };
|
||||
|
||||
const prepareStartLabel = "runner-prepare-start";
|
||||
const prepareEndLabel = "runner-prepare-end";
|
||||
|
||||
performance.mark(prepareStartLabel);
|
||||
this._removeFrame();
|
||||
await this._appendFrame();
|
||||
this._page = new Page(this._frame);
|
||||
|
||||
let suites = [...this._suites];
|
||||
if (this._suiteOrderRandomNumberGenerator) {
|
||||
// We just do a simple Fisher-Yates shuffle based on the repeated hash of the
|
||||
// seed. This is not a high quality RNG, but it's plenty good enough.
|
||||
for (let i = 0; i < suites.length - 1; i++) {
|
||||
let j = i + (this._suiteOrderRandomNumberGenerator() % (suites.length - i));
|
||||
let tmp = suites[i];
|
||||
suites[i] = suites[j];
|
||||
suites[j] = tmp;
|
||||
}
|
||||
}
|
||||
|
||||
performance.mark(prepareEndLabel);
|
||||
performance.measure("runner-prepare", prepareStartLabel, prepareEndLabel);
|
||||
|
||||
for (const suite of suites) {
|
||||
if (!suite.disabled)
|
||||
await this._runSuite(suite);
|
||||
}
|
||||
|
||||
const finalizeStartLabel = "runner-finalize-start";
|
||||
const finalizeEndLabel = "runner-finalize-end";
|
||||
|
||||
performance.mark(finalizeStartLabel);
|
||||
// Remove frame to clear the view for displaying the results.
|
||||
this._removeFrame();
|
||||
await this._finalize();
|
||||
performance.mark(finalizeEndLabel);
|
||||
performance.measure("runner-finalize", finalizeStartLabel, finalizeEndLabel);
|
||||
}
|
||||
|
||||
async _runSuite(suite) {
|
||||
const suitePrepareStartLabel = `suite-${suite.name}-prepare-start`;
|
||||
const suitePrepareEndLabel = `suite-${suite.name}-prepare-end`;
|
||||
const suiteStartLabel = `suite-${suite.name}-start`;
|
||||
const suiteEndLabel = `suite-${suite.name}-end`;
|
||||
|
||||
performance.mark(suitePrepareStartLabel);
|
||||
await this._prepareSuite(suite);
|
||||
performance.mark(suitePrepareEndLabel);
|
||||
|
||||
performance.mark(suiteStartLabel);
|
||||
for (const test of suite.tests)
|
||||
await this._runTestAndRecordResults(suite, test);
|
||||
performance.mark(suiteEndLabel);
|
||||
|
||||
performance.measure(`suite-${suite.name}-prepare`, suitePrepareStartLabel, suitePrepareEndLabel);
|
||||
performance.measure(`suite-${suite.name}`, suiteStartLabel, suiteEndLabel);
|
||||
}
|
||||
|
||||
async _prepareSuite(suite) {
|
||||
return new Promise((resolve) => {
|
||||
const frame = this._page._frame;
|
||||
frame.onload = async () => {
|
||||
this._prepareValue = await suite.prepare(this._page, frame.contentWindow, frame.contentDocument);
|
||||
resolve();
|
||||
};
|
||||
frame.src = `resources/${suite.url}`;
|
||||
});
|
||||
}
|
||||
|
||||
async _runTestAndRecordResults(suite, test) {
|
||||
/* eslint-disable-next-line no-async-promise-executor */
|
||||
if (this._client?.willRunTest)
|
||||
await this._client.willRunTest(suite, test);
|
||||
|
||||
// Prepare all mark labels outside the measuring loop.
|
||||
const startLabel = `${suite.name}.${test.name}-start`;
|
||||
const syncEndLabel = `${suite.name}.${test.name}-sync-end`;
|
||||
const asyncStartLabel = `${suite.name}.${test.name}-async-start`;
|
||||
const asyncEndLabel = `${suite.name}.${test.name}-async-end`;
|
||||
|
||||
let syncTime;
|
||||
let asyncStartTime;
|
||||
let asyncTime;
|
||||
const runSync = () => {
|
||||
if (params.warmupBeforeSync) {
|
||||
performance.mark("warmup-start");
|
||||
const startTime = performance.now();
|
||||
// Infinite loop for the specified ms.
|
||||
while (performance.now() - startTime < params.warmupBeforeSync)
|
||||
continue;
|
||||
performance.mark("warmup-end");
|
||||
}
|
||||
performance.mark(startLabel);
|
||||
const syncStartTime = performance.now();
|
||||
test.run(this._prepareValue, this._page._frame.contentWindow, this._page._frame.contentDocument);
|
||||
const syncEndTime = performance.now();
|
||||
performance.mark(syncEndLabel);
|
||||
|
||||
syncTime = syncEndTime - syncStartTime;
|
||||
|
||||
performance.mark(asyncStartLabel);
|
||||
asyncStartTime = performance.now();
|
||||
};
|
||||
const measureAsync = () => {
|
||||
// Some browsers don't immediately update the layout for paint.
|
||||
// Force the layout here to ensure we're measuring the layout time.
|
||||
const height = this._frame.contentDocument.body.getBoundingClientRect().height;
|
||||
const asyncEndTime = performance.now();
|
||||
asyncTime = asyncEndTime - asyncStartTime;
|
||||
this._frame.contentWindow._unusedHeightValue = height; // Prevent dead code elimination.
|
||||
performance.mark(asyncEndLabel);
|
||||
if (params.warmupBeforeSync)
|
||||
performance.measure("warmup", "warmup-start", "warmup-end");
|
||||
performance.measure(`${suite.name}.${test.name}-sync`, startLabel, syncEndLabel);
|
||||
performance.measure(`${suite.name}.${test.name}-async`, asyncStartLabel, asyncEndLabel);
|
||||
};
|
||||
const report = () => this._recordTestResults(suite, test, syncTime, asyncTime);
|
||||
const invokerClass = params.measurementMethod === "raf" ? RAFTestInvoker : TimerTestInvoker;
|
||||
const invoker = new invokerClass(runSync, measureAsync, report);
|
||||
|
||||
return invoker.start();
|
||||
}
|
||||
|
||||
async _recordTestResults(suite, test, syncTime, asyncTime) {
|
||||
// Skip reporting updates for the warmup suite.
|
||||
if (suite === WarmupSuite)
|
||||
return;
|
||||
|
||||
const suiteResults = this._measuredValues.tests[suite.name] || { tests: {}, total: 0 };
|
||||
const total = syncTime + asyncTime;
|
||||
this._measuredValues.tests[suite.name] = suiteResults;
|
||||
suiteResults.tests[test.name] = { tests: { Sync: syncTime, Async: asyncTime }, total: total };
|
||||
suiteResults.total += total;
|
||||
|
||||
if (this._client?.didRunTest)
|
||||
await this._client.didRunTest(suite, test);
|
||||
}
|
||||
|
||||
async _finalize() {
|
||||
this._appendIterationMetrics();
|
||||
if (this._client?.didRunSuites) {
|
||||
let product = 1;
|
||||
const values = [];
|
||||
for (const suiteName in this._measuredValues.tests) {
|
||||
const suiteTotal = this._measuredValues.tests[suiteName].total;
|
||||
product *= suiteTotal;
|
||||
values.push(suiteTotal);
|
||||
}
|
||||
|
||||
values.sort((a, b) => a - b); // Avoid the loss of significance for the sum.
|
||||
const total = values.reduce((a, b) => a + b);
|
||||
const geomean = Math.pow(product, 1 / values.length);
|
||||
|
||||
this._measuredValues.total = total;
|
||||
this._measuredValues.mean = total / values.length;
|
||||
this._measuredValues.geomean = geomean;
|
||||
this._measuredValues.score = geomeanToScore(geomean);
|
||||
await this._client.didRunSuites(this._measuredValues);
|
||||
}
|
||||
}
|
||||
|
||||
_appendIterationMetrics() {
|
||||
const getMetric = (name, unit = "ms") => this._metrics[name] || (this._metrics[name] = new Metric(name, unit));
|
||||
const iterationTotalMetric = (i) => {
|
||||
if (i >= params.iterationCount)
|
||||
throw new Error(`Requested iteration=${i} does not exist.`);
|
||||
return getMetric(`Iteration-${i}-Total`);
|
||||
};
|
||||
|
||||
const collectSubMetrics = (prefix, items, parent) => {
|
||||
for (let name in items) {
|
||||
const results = items[name];
|
||||
const metric = getMetric(prefix + name);
|
||||
metric.add(results.total ?? results);
|
||||
if (metric.parent !== parent)
|
||||
parent.addChild(metric);
|
||||
if (results.tests)
|
||||
collectSubMetrics(`${metric.name}${Metric.separator}`, results.tests, metric);
|
||||
}
|
||||
};
|
||||
const initializeMetrics = this._metrics === null;
|
||||
if (initializeMetrics)
|
||||
this._metrics = { __proto__: null };
|
||||
|
||||
const iterationResults = this._measuredValues.tests;
|
||||
collectSubMetrics("", iterationResults);
|
||||
|
||||
if (initializeMetrics) {
|
||||
// Prepare all iteration metrics so they are listed at the end of
|
||||
// of the _metrics object, before "Total" and "Score".
|
||||
for (let i = 0; i < this._iterationCount; i++)
|
||||
iterationTotalMetric(i);
|
||||
getMetric("Geomean");
|
||||
getMetric("Score", "score");
|
||||
}
|
||||
|
||||
const geomean = getMetric("Geomean");
|
||||
const iterationTotal = iterationTotalMetric(geomean.length);
|
||||
for (const results of Object.values(iterationResults))
|
||||
iterationTotal.add(results.total);
|
||||
iterationTotal.computeAggregatedMetrics();
|
||||
geomean.add(iterationTotal.geomean);
|
||||
getMetric("Score").add(geomeanToScore(iterationTotal.geomean));
|
||||
|
||||
for (const metric of Object.values(this._metrics))
|
||||
metric.computeAggregatedMetrics();
|
||||
}
|
||||
}
|
||||
253
resources/developer-mode.mjs
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
import { Suites, Tags } from "./tests.mjs";
|
||||
import { params, defaultParams } from "./params.mjs";
|
||||
|
||||
export function createDeveloperModeContainer() {
|
||||
const container = document.createElement("div");
|
||||
container.className = "developer-mode";
|
||||
|
||||
const details = document.createElement("details");
|
||||
const summary = document.createElement("summary");
|
||||
summary.textContent = "Developer Mode";
|
||||
details.append(summary);
|
||||
|
||||
const content = document.createElement("div");
|
||||
content.className = "developer-mode-content";
|
||||
content.append(createUIForSuites());
|
||||
|
||||
content.append(document.createElement("hr"));
|
||||
content.append(createUIForMeasurementMethod());
|
||||
content.append(document.createElement("br"));
|
||||
content.append(createUIForWarmupSuite());
|
||||
content.append(document.createElement("br"));
|
||||
content.append(createUIForIterationCount());
|
||||
|
||||
content.append(document.createElement("hr"));
|
||||
content.append(createUIForRun());
|
||||
|
||||
details.append(content);
|
||||
container.append(details);
|
||||
return container;
|
||||
}
|
||||
|
||||
export function createUIForMeasurementMethod() {
|
||||
let check = document.createElement("input");
|
||||
check.type = "checkbox";
|
||||
check.id = "measurement-method";
|
||||
check.checked = params.measurementMethod === "raf";
|
||||
|
||||
check.onchange = () => {
|
||||
params.measurementMethod = check.checked ? "raf" : "timer";
|
||||
updateURL();
|
||||
};
|
||||
|
||||
let label = document.createElement("label");
|
||||
label.append(check, " ", "rAF timing");
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
export function createUIForWarmupSuite() {
|
||||
let check = document.createElement("input");
|
||||
check.type = "checkbox";
|
||||
check.id = "warmup-suite";
|
||||
check.checked = !!params.useWarmupSuite;
|
||||
|
||||
check.onchange = () => {
|
||||
params.useWarmupSuite = check.checked;
|
||||
updateURL();
|
||||
};
|
||||
|
||||
let label = document.createElement("label");
|
||||
label.append(check, " ", "warmup suite");
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
export function createUIForIterationCount() {
|
||||
let range = document.createElement("input");
|
||||
range.type = "range";
|
||||
range.id = "iteration-count";
|
||||
range.min = 1;
|
||||
range.max = 20;
|
||||
range.value = params.iterationCount;
|
||||
|
||||
let label = document.createElement("label");
|
||||
let countLabel = document.createElement("span");
|
||||
countLabel.textContent = params.iterationCount;
|
||||
label.append("iterations: ", countLabel, document.createElement("br"), range);
|
||||
|
||||
range.oninput = () => {
|
||||
countLabel.textContent = range.value;
|
||||
};
|
||||
|
||||
range.onchange = () => {
|
||||
params.iterationCount = parseInt(range.value);
|
||||
updateURL();
|
||||
};
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
|
||||
export function createUIForSuites() {
|
||||
const control = document.createElement("nav");
|
||||
const ol = document.createElement("ol");
|
||||
const checkboxes = [];
|
||||
const setSuiteEnabled = (suiteIndex, enabled) => {
|
||||
Suites[suiteIndex].disabled = !enabled;
|
||||
checkboxes[suiteIndex].checked = enabled;
|
||||
};
|
||||
|
||||
for (const suite of Suites) {
|
||||
const li = document.createElement("li");
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.id = suite.name;
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.checked = !suite.disabled;
|
||||
checkbox.onchange = () => {
|
||||
suite.disabled = !checkbox.checked;
|
||||
updateURL();
|
||||
};
|
||||
checkboxes.push(checkbox);
|
||||
|
||||
const label = document.createElement("label");
|
||||
label.append(checkbox, " ", suite.name);
|
||||
li.appendChild(label);
|
||||
label.onclick = (event) => {
|
||||
if (event?.ctrlKey || event?.metaKey) {
|
||||
for (let suiteIndex = 0; suiteIndex < Suites.length; suiteIndex++) {
|
||||
if (Suites[suiteIndex] !== suite)
|
||||
setSuiteEnabled(suiteIndex, false);
|
||||
else
|
||||
setSuiteEnabled(suiteIndex, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ol.appendChild(li);
|
||||
}
|
||||
control.appendChild(ol);
|
||||
let buttons = control.appendChild(document.createElement("div"));
|
||||
buttons.className = "button-bar";
|
||||
|
||||
let button = document.createElement("button");
|
||||
button.textContent = "Select all";
|
||||
button.onclick = () => {
|
||||
for (let suiteIndex = 0; suiteIndex < Suites.length; suiteIndex++)
|
||||
setSuiteEnabled(suiteIndex, true);
|
||||
|
||||
updateURL();
|
||||
};
|
||||
buttons.appendChild(button);
|
||||
|
||||
button = document.createElement("button");
|
||||
button.textContent = "Unselect all";
|
||||
button.onclick = () => {
|
||||
for (let suiteIndex = 0; suiteIndex < Suites.length; suiteIndex++)
|
||||
setSuiteEnabled(suiteIndex, false);
|
||||
|
||||
updateURL();
|
||||
};
|
||||
buttons.appendChild(button);
|
||||
|
||||
let i = 0;
|
||||
const kTagsPerLine = 3;
|
||||
for (const tag of Tags) {
|
||||
if (tag === "all")
|
||||
continue;
|
||||
if (!(i % kTagsPerLine)) {
|
||||
buttons = control.appendChild(document.createElement("div"));
|
||||
buttons.className = "button-bar";
|
||||
}
|
||||
i++;
|
||||
button = document.createElement("button");
|
||||
button.className = "tag";
|
||||
button.textContent = `#${tag}`;
|
||||
button.dataTag = tag;
|
||||
button.onclick = (event) => {
|
||||
const extendSelection = event?.shiftKey;
|
||||
const invertSelection = event?.ctrlKey || event?.metaKey;
|
||||
const selectedTag = event.target.dataTag;
|
||||
for (let suiteIndex = 0; suiteIndex < Suites.length; suiteIndex++) {
|
||||
let enabled = Suites[suiteIndex].tags.includes(selectedTag);
|
||||
if (invertSelection)
|
||||
enabled = !enabled;
|
||||
if (extendSelection && !enabled)
|
||||
continue;
|
||||
setSuiteEnabled(suiteIndex, enabled);
|
||||
}
|
||||
updateURL();
|
||||
};
|
||||
buttons.appendChild(button);
|
||||
}
|
||||
|
||||
return control;
|
||||
}
|
||||
|
||||
function createUIForRun() {
|
||||
let button = document.createElement("button");
|
||||
button.textContent = `Start Test`;
|
||||
button.onclick = (event) => {
|
||||
globalThis.benchmarkClient.start();
|
||||
}
|
||||
let buttons = document.createElement("div");
|
||||
buttons.className = "button-bar";
|
||||
buttons.appendChild(button);
|
||||
return buttons
|
||||
}
|
||||
|
||||
function updateURL() {
|
||||
const url = new URL(window.location.href);
|
||||
|
||||
// If less than all suites are selected then change the URL "Suites" GET parameter
|
||||
// to comma separate only the selected
|
||||
const selectedSuites = Suites.filter((suite) => !suite.disabled);
|
||||
|
||||
if (!selectedSuites.length) {
|
||||
url.searchParams.delete("tags");
|
||||
url.searchParams.delete("suites");
|
||||
url.searchParams.delete("suite");
|
||||
} else {
|
||||
url.searchParams.delete("tags");
|
||||
url.searchParams.delete("suite");
|
||||
// Try finding common tags that would result in the current suite selection.
|
||||
let commonTags = new Set(selectedSuites[0].tags);
|
||||
for (const suite of Suites) {
|
||||
if (suite.disabled)
|
||||
suite.tags.forEach((tag) => commonTags.delete(tag));
|
||||
else
|
||||
commonTags = new Set(suite.tags.filter((tag) => commonTags.has(tag)));
|
||||
}
|
||||
if (commonTags.size) {
|
||||
const tags = [...commonTags][0];
|
||||
if (tags === "default")
|
||||
url.searchParams.delete("tags");
|
||||
else
|
||||
url.searchParams.set("tags", tags);
|
||||
url.searchParams.delete("suites");
|
||||
} else {
|
||||
url.searchParams.delete("tags");
|
||||
url.searchParams.set("suites", selectedSuites.map((suite) => suite.name).join(","));
|
||||
}
|
||||
}
|
||||
|
||||
if (params.measurementMethod !== "raf")
|
||||
url.searchParams.set("measurementMethod", "timer");
|
||||
else
|
||||
url.searchParams.delete("measurementMethod");
|
||||
|
||||
if (params.iterationCount !== defaultParams.iterationCount)
|
||||
url.searchParams.set("iterationCount", params.iterationCount);
|
||||
else
|
||||
url.searchParams.delete("iterationCount");
|
||||
|
||||
if (params.useWarmupSuite !== defaultParams.useWarmupSuite)
|
||||
url.searchParams.set("useWarmupSuite", params.useWarmupSuite);
|
||||
else
|
||||
url.searchParams.delete("useWarmupSuite");
|
||||
|
||||
// Only push state if changed
|
||||
url.search = decodeURIComponent(url.search);
|
||||
if (url.href !== window.location.href)
|
||||
window.history.pushState({}, "", url);
|
||||
}
|
||||
BIN
resources/favicon.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
19
resources/flightjs-example-app/LICENSE.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) Twitter Inc
|
||||
|
||||
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.
|
||||
5
resources/flightjs-example-app/README.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Flight example app
|
||||
|
||||
[](http://travis-ci.org/flightjs/example-app)
|
||||
|
||||
An example Flight application.
|
||||
47
resources/flightjs-example-app/app/boot/page.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
'use strict';
|
||||
|
||||
define(
|
||||
|
||||
[
|
||||
'app/component_data/mail_items',
|
||||
'app/component_data/compose_box',
|
||||
'app/component_data/move_to',
|
||||
'app/component_ui/mail_items',
|
||||
'app/component_ui/mail_controls',
|
||||
'app/component_ui/compose_box',
|
||||
'app/component_ui/folders',
|
||||
'app/component_ui/move_to_selector'
|
||||
],
|
||||
|
||||
function(
|
||||
MailItemsData,
|
||||
ComposeBoxData,
|
||||
MoveToData,
|
||||
MailItemsUI,
|
||||
MailControlsUI,
|
||||
ComposeBoxUI,
|
||||
FoldersUI,
|
||||
MoveToSelectorUI) {
|
||||
|
||||
function initialize() {
|
||||
MailItemsData.attachTo(document);
|
||||
ComposeBoxData.attachTo(document, {
|
||||
selectedFolders: ['inbox']
|
||||
});
|
||||
MoveToData.attachTo(document);
|
||||
MailItemsUI.attachTo('#mail_items', {
|
||||
itemContainerSelector: '#mail_items_TB',
|
||||
selectedFolders: ['inbox']
|
||||
});
|
||||
MailControlsUI.attachTo('#mail_controls');
|
||||
ComposeBoxUI.attachTo('#compose_box');
|
||||
FoldersUI.attachTo('#folders');
|
||||
MoveToSelectorUI.attachTo('#move_to_selector', {
|
||||
moveActionSelector: '#move_mail',
|
||||
selectedFolders: ['inbox']
|
||||
});
|
||||
}
|
||||
|
||||
return initialize;
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
'use strict';
|
||||
|
||||
define(
|
||||
|
||||
[
|
||||
'flight/lib/component',
|
||||
'components/mustache/mustache',
|
||||
'app/data',
|
||||
'app/templates'
|
||||
],
|
||||
|
||||
function(defineComponent, Mustache, dataStore, templates) {
|
||||
return defineComponent(composeBox);
|
||||
|
||||
function composeBox() {
|
||||
|
||||
this.defaultAttrs({
|
||||
dataStore: dataStore,
|
||||
recipientHintId: 'recipient_hint',
|
||||
subjectHint: 'Subject',
|
||||
messageHint: 'Message',
|
||||
toHint: 'To',
|
||||
forwardPrefix: 'Fw',
|
||||
replyPrefix: 'Re'
|
||||
});
|
||||
|
||||
this.serveComposeBox = function(ev, data) {
|
||||
this.trigger("dataComposeBoxServed", {
|
||||
markup: this.renderComposeBox(data.type, data.relatedMailId),
|
||||
type: data.type});
|
||||
};
|
||||
|
||||
this.getSubject = function(type, relatedMailId) {
|
||||
var relatedMail = this.attr.dataStore.mail.filter(function(each) {
|
||||
return each.id == relatedMailId;
|
||||
})[0];
|
||||
|
||||
var subject = relatedMail && relatedMail.subject;
|
||||
|
||||
var subjectLookup = {
|
||||
newMail: this.attr.subjectHint,
|
||||
forward: this.attr.forwardPrefix + ": " + subject,
|
||||
reply: this.attr.replyPrefix + ": " + subject
|
||||
}
|
||||
|
||||
return subjectLookup[type];
|
||||
};
|
||||
|
||||
this.renderComposeBox = function(type, relatedMailId) {
|
||||
var recipientId = this.getRecipientId(type, relatedMailId);
|
||||
var contacts = this.attr.dataStore.contacts.map(function(contact) {
|
||||
contact.recipient = (contact.id == recipientId);
|
||||
return contact;
|
||||
});
|
||||
|
||||
return Mustache.render(templates.composeBox, {
|
||||
newMail: type == 'newMail',
|
||||
reply: type == 'reply',
|
||||
subject: this.getSubject(type, relatedMailId),
|
||||
message: this.attr.messageHint,
|
||||
contacts: contacts
|
||||
});
|
||||
};
|
||||
|
||||
this.getRecipientId = function(type, relatedMailId) {
|
||||
var relatedMail = (type == 'reply') && this.attr.dataStore.mail.filter(function(each) {
|
||||
return each.id == relatedMailId;
|
||||
})[0];
|
||||
|
||||
return relatedMail && relatedMail.contact_id || this.attr.recipientHintId;
|
||||
};
|
||||
|
||||
|
||||
this.send = function(ev, data) {
|
||||
this.attr.dataStore.mail.push({
|
||||
id: String(Date.now()),
|
||||
contact_id: data.to_id,
|
||||
folders: ["sent"],
|
||||
time: Date.now(),
|
||||
subject: data.subject,
|
||||
message: data.message
|
||||
});
|
||||
this.trigger('dataMailItemsRefreshRequested', {folder: data.currentFolder});
|
||||
};
|
||||
|
||||
this.after("initialize", function() {
|
||||
this.on("uiComposeBoxRequested", this.serveComposeBox);
|
||||
this.on("uiSendRequested", this.send);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
'use strict';
|
||||
|
||||
define(
|
||||
|
||||
[
|
||||
'flight/lib/component',
|
||||
'components/mustache/mustache',
|
||||
'app/data',
|
||||
'app/templates'
|
||||
],
|
||||
|
||||
function(defineComponent, Mustache, dataStore, templates) {
|
||||
return defineComponent(mailItems);
|
||||
|
||||
function mailItems() {
|
||||
|
||||
this.defaultAttrs({
|
||||
folder: 'inbox',
|
||||
dataStore: dataStore
|
||||
});
|
||||
|
||||
this.serveMailItems = function(ev, data) {
|
||||
var folder = (data && data.folder) || this.attr.folder;
|
||||
this.trigger("dataMailItemsServed", {markup: this.renderItems(this.assembleItems(folder))})
|
||||
};
|
||||
|
||||
this.renderItems = function(items) {
|
||||
return Mustache.render(templates.mailItem, {mailItems: items});
|
||||
};
|
||||
|
||||
this.assembleItems = function(folder) {
|
||||
var items = [];
|
||||
|
||||
this.attr.dataStore.mail.forEach(function(each) {
|
||||
if (each.folders && each.folders.indexOf(folder) > -1) {
|
||||
items.push(this.getItemForView(each));
|
||||
}
|
||||
}, this);
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
this.getItemForView = function(itemData) {
|
||||
var thisItem, thisContact, msg
|
||||
|
||||
thisItem = {id: itemData.id, important: itemData.important};
|
||||
|
||||
thisContact = this.attr.dataStore.contacts.filter(function(contact) {
|
||||
return contact.id == itemData.contact_id
|
||||
})[0];
|
||||
thisItem.name = [thisContact.firstName, thisContact.lastName].join(' ');
|
||||
|
||||
var subj = itemData.subject;
|
||||
thisItem.formattedSubject = subj.length > 70 ? subj.slice(0, 70) + "..." : subj;
|
||||
|
||||
var msg = itemData.message;
|
||||
thisItem.formattedMessage = msg.length > 70 ? msg.slice(0, 70) + "..." : msg;
|
||||
|
||||
return thisItem;
|
||||
};
|
||||
|
||||
this.after("initialize", function() {
|
||||
this.on("uiMailItemsRequested", this.serveMailItems);
|
||||
this.on("dataMailItemsRefreshRequested", this.serveMailItems);
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
52
resources/flightjs-example-app/app/component_data/move_to.js
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
'use strict';
|
||||
|
||||
define(
|
||||
|
||||
[
|
||||
'flight/lib/component',
|
||||
'components/mustache/mustache',
|
||||
'app/data',
|
||||
'app/templates'
|
||||
],
|
||||
|
||||
function(defineComponent, Mustache, dataStore, templates) {
|
||||
return defineComponent(moveTo);
|
||||
|
||||
function moveTo() {
|
||||
|
||||
this.defaultAttrs({
|
||||
dataStore: dataStore
|
||||
});
|
||||
|
||||
this.serveAvailableFolders = function(ev, data) {
|
||||
this.trigger("dataMoveToItemsServed", {
|
||||
markup: this.renderFolderSelector(this.getOtherFolders(data.folder))
|
||||
})
|
||||
};
|
||||
|
||||
this.renderFolderSelector = function(items) {
|
||||
return Mustache.render(templates.moveToSelector, {moveToItems: items});
|
||||
};
|
||||
|
||||
this.moveItems = function(ev, data) {
|
||||
var itemsToMoveIds = data.itemIds
|
||||
this.attr.dataStore.mail.forEach(function(item) {
|
||||
if (itemsToMoveIds.indexOf(item.id) > -1) {
|
||||
item.folders = [data.toFolder];
|
||||
}
|
||||
});
|
||||
this.trigger('dataMailItemsRefreshRequested', {folder: data.fromFolder});
|
||||
};
|
||||
|
||||
this.getOtherFolders = function(folder) {
|
||||
return this.attr.dataStore.folders.filter(function(e) {return e != folder});
|
||||
};
|
||||
|
||||
this.after("initialize", function() {
|
||||
this.on("uiAvailableFoldersRequested", this.serveAvailableFolders);
|
||||
this.on("uiMoveItemsRequested", this.moveItems);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
);
|
||||
113
resources/flightjs-example-app/app/component_ui/compose_box.js
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
'use strict';
|
||||
|
||||
define(
|
||||
|
||||
[
|
||||
'flight/lib/component'
|
||||
],
|
||||
|
||||
function(defineComponent) {
|
||||
|
||||
return defineComponent(composeBox);
|
||||
|
||||
function composeBox() {
|
||||
|
||||
this.defaultAttrs({
|
||||
newMailType: 'newMail',
|
||||
forwardMailType: 'forward',
|
||||
replyMailType: 'reply',
|
||||
hintClass: 'hint',
|
||||
selectedFolders: [],
|
||||
selectedMailItems: [],
|
||||
|
||||
//selectors
|
||||
composeControl: '.compose',
|
||||
newControlSelector: '#new_mail',
|
||||
cancelSelector: '#cancel_composed',
|
||||
sendSelector: '#send_composed',
|
||||
toSelector: '#compose_to',
|
||||
subjectSelector: '#compose_subject',
|
||||
messageSelector: '#compose_message',
|
||||
recipientSelector: '#recipient_select',
|
||||
recipientHintSelector: '#recipient_hint',
|
||||
selectedRecipientSelector: '#recipient_select :selected',
|
||||
hintSelector: 'div.hint'
|
||||
});
|
||||
|
||||
this.newMail = function() {
|
||||
this.requestComposeBox(this.attr.newMailType);
|
||||
};
|
||||
|
||||
this.forward = function() {
|
||||
this.requestComposeBox(this.attr.forwardMailType, this.attr.selectedMailItems);
|
||||
};
|
||||
|
||||
this.reply = function() {
|
||||
this.requestComposeBox(this.attr.replyMailType, this.attr.selectedMailItems);
|
||||
};
|
||||
|
||||
this.requestComposeBox = function(type, relatedMailId) {
|
||||
this.trigger('uiComposeBoxRequested', {type: type, relatedMailId: relatedMailId});
|
||||
};
|
||||
|
||||
this.launchComposeBox = function(ev, data) {
|
||||
var focusSelector = (data.type == this.attr.replyMailType) ? 'messageSelector' : 'toSelector';
|
||||
this.$node.html(data.markup).show();
|
||||
this.select(focusSelector).focus();
|
||||
};
|
||||
|
||||
this.cancel = function() {
|
||||
this.$node.html('').hide();
|
||||
};
|
||||
|
||||
this.requestSend = function() {
|
||||
var data = {
|
||||
to_id: this.select('selectedRecipientSelector').attr('id'),
|
||||
subject: this.select('subjectSelector').text(),
|
||||
message: this.select('messageSelector').text(),
|
||||
currentFolder: this.attr.selectedFolders[0]
|
||||
};
|
||||
this.trigger('uiSendRequested', data);
|
||||
this.$node.hide();
|
||||
};
|
||||
|
||||
this.enableSend = function() {
|
||||
this.select('recipientHintSelector').attr('disabled', 'disabled');
|
||||
this.select('sendSelector').removeAttr('disabled');
|
||||
};
|
||||
|
||||
this.removeHint = function(ev, data) {
|
||||
$(ev.target).html('').removeClass(this.attr.hintClass);
|
||||
};
|
||||
|
||||
this.updateMailItemSelections = function(ev, data) {
|
||||
this.attr.selectedMailItems = data.selectedIds;
|
||||
}
|
||||
|
||||
this.updateFolderSelections = function(ev, data) {
|
||||
this.attr.selectedFolders = data.selectedIds;
|
||||
}
|
||||
|
||||
this.after('initialize', function() {
|
||||
this.on(document, 'dataComposeBoxServed', this.launchComposeBox);
|
||||
this.on(document, 'uiForwardMail', this.forward);
|
||||
this.on(document, 'uiReplyToMail', this.reply);
|
||||
this.on(document, 'uiMailItemSelectionChanged', this.updateMailItemSelections);
|
||||
this.on(document, 'uiFolderSelectionChanged', this.updateFolderSelections);
|
||||
|
||||
//the following bindings use delegation so that the event target is read at event time
|
||||
this.on(document, "click", {
|
||||
'cancelSelector': this.cancel,
|
||||
'sendSelector': this.requestSend,
|
||||
'newControlSelector': this.newMail
|
||||
});
|
||||
this.on('change', {
|
||||
'recipientSelector': this.enableSend
|
||||
});
|
||||
this.on('keydown', {
|
||||
'hintSelector': this.removeHint
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
34
resources/flightjs-example-app/app/component_ui/folders.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
'use strict';
|
||||
|
||||
define(
|
||||
|
||||
[
|
||||
'flight/lib/component',
|
||||
'./with_select'
|
||||
],
|
||||
|
||||
function(defineComponent, withSelect) {
|
||||
|
||||
return defineComponent(folders, withSelect);
|
||||
|
||||
function folders() {
|
||||
|
||||
this.defaultAttrs({
|
||||
selectedClass: 'selected',
|
||||
selectionChangedEvent: 'uiFolderSelectionChanged',
|
||||
|
||||
//selectors
|
||||
itemSelector: 'li.folder-item',
|
||||
selectedItemSelector: 'li.folder-item.selected'
|
||||
});
|
||||
|
||||
this.fetchMailItems = function(ev, data) {
|
||||
this.trigger('uiMailItemsRequested', {folder: data.selectedIds[0]});
|
||||
}
|
||||
|
||||
this.after('initialize', function() {
|
||||
this.on('uiFolderSelectionChanged', this.fetchMailItems);
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
'use strict';
|
||||
|
||||
define(
|
||||
[
|
||||
'flight/lib/component'
|
||||
],
|
||||
|
||||
function(defineComponent) {
|
||||
|
||||
return defineComponent(mailControls);
|
||||
|
||||
function mailControls() {
|
||||
this.defaultAttrs({
|
||||
//selectors
|
||||
actionControlsSelector: 'button.mail-action',
|
||||
deleteControlSelector: '#delete_mail',
|
||||
moveControlSelector: '#move_mail',
|
||||
forwardControlSelector: '#forward',
|
||||
replyControlSelector: '#reply',
|
||||
singleItemActionSelector: 'button.single-item'
|
||||
});
|
||||
|
||||
this.disableAll = function() {
|
||||
this.select('actionControlsSelector').attr('disabled', 'disabled');
|
||||
};
|
||||
|
||||
this.restyleOnSelectionChange = function(ev, data) {
|
||||
if (data.selectedIds.length > 1) {
|
||||
this.select('actionControlsSelector').not('button.single-item').removeAttr('disabled');
|
||||
this.select('singleItemActionSelector').attr('disabled', 'disabled');
|
||||
} else if (data.selectedIds.length == 1) {
|
||||
this.select('actionControlsSelector').removeAttr('disabled');
|
||||
} else {
|
||||
this.disableAll();
|
||||
}
|
||||
};
|
||||
|
||||
this.deleteMail = function(ev, data) {
|
||||
this.trigger('uiDeleteMail');
|
||||
};
|
||||
|
||||
this.moveMail = function(ev, data) {
|
||||
this.trigger('uiMoveMail');
|
||||
};
|
||||
|
||||
this.forwardMail = function(ev, data) {
|
||||
this.trigger('uiForwardMail');
|
||||
};
|
||||
|
||||
this.replyToMail = function(ev, data) {
|
||||
this.trigger('uiReplyToMail');
|
||||
};
|
||||
|
||||
this.after('initialize', function() {
|
||||
this.on('.mail-action', 'click', {
|
||||
'deleteControlSelector': this.deleteMail,
|
||||
'moveControlSelector': this.moveMail,
|
||||
'forwardControlSelector': this.forwardMail,
|
||||
'replyControlSelector': this.replyToMail
|
||||
});
|
||||
this.on(document, 'uiMailItemSelectionChanged', this.restyleOnSelectionChange);
|
||||
this.on(document, 'uiFolderSelectionChanged', this.disableAll);
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
'use strict';
|
||||
|
||||
define(
|
||||
|
||||
[
|
||||
'flight/lib/component',
|
||||
'./with_select'
|
||||
],
|
||||
|
||||
function(defineComponent, withSelect) {
|
||||
|
||||
return defineComponent(mailItems, withSelect);
|
||||
|
||||
function mailItems() {
|
||||
|
||||
this.defaultAttrs({
|
||||
deleteFolder: 'trash',
|
||||
selectedClass: 'selected',
|
||||
allowMultiSelect: true,
|
||||
selectionChangedEvent: 'uiMailItemSelectionChanged',
|
||||
selectedMailItems: [],
|
||||
selectedFolders: [],
|
||||
//selectors
|
||||
itemSelector: 'tr.mail-item',
|
||||
selectedItemSelector: 'tr.mail-item.selected'
|
||||
});
|
||||
|
||||
this.renderItems = function(ev, data) {
|
||||
this.select('itemContainerSelector').html(data.markup);
|
||||
//new items, so no selections
|
||||
this.trigger('uiMailItemSelectionChanged', {selectedIds: []});
|
||||
}
|
||||
|
||||
this.updateMailItemSelections = function(ev, data) {
|
||||
this.attr.selectedMailItems = data.selectedIds;
|
||||
}
|
||||
|
||||
this.updateFolderSelections = function(ev, data) {
|
||||
this.attr.selectedFolders = data.selectedIds;
|
||||
}
|
||||
|
||||
this.requestDeletion = function() {
|
||||
this.trigger('uiMoveItemsRequested', {
|
||||
itemIds: this.attr.selectedMailItems,
|
||||
fromFolder: this.attr.selectedFolders[0],
|
||||
toFolder: this.attr.deleteFolder
|
||||
});
|
||||
};
|
||||
|
||||
this.after('initialize', function() {
|
||||
this.on(document, 'dataMailItemsServed', this.renderItems);
|
||||
this.on(document, 'uiDeleteMail', this.requestDeletion);
|
||||
|
||||
this.on('uiMailItemSelectionChanged', this.updateMailItemSelections);
|
||||
this.on(document, 'uiFolderSelectionChanged', this.updateFolderSelections);
|
||||
|
||||
this.trigger('uiMailItemsRequested');
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
'use strict';
|
||||
|
||||
define(
|
||||
|
||||
[
|
||||
'flight/lib/component',
|
||||
'./with_select'
|
||||
],
|
||||
|
||||
function(defineComponent, withSelect) {
|
||||
|
||||
return defineComponent(moveToSelector, withSelect);
|
||||
|
||||
function moveToSelector() {
|
||||
|
||||
this.defaultAttrs({
|
||||
selectionChangedEvent: 'uiMoveToSelectionChanged',
|
||||
selectedMailItems: [],
|
||||
selectedFolders: [],
|
||||
//selectors
|
||||
itemSelector: 'li.move-to-item',
|
||||
selectedItemSelector: 'li.move-to-item.selected'
|
||||
});
|
||||
|
||||
this.requestSelectorWidget = function(ev, data) {
|
||||
this.trigger('uiAvailableFoldersRequested', {
|
||||
folder: this.attr.selectedFolders[0]
|
||||
})
|
||||
};
|
||||
|
||||
this.launchSelector = function(ev, data) {
|
||||
var controlPosition = $(this.attr.moveActionSelector).offset();
|
||||
this.$node.html(data.markup).show().css({
|
||||
left: controlPosition.left,
|
||||
top: controlPosition.top + $(this.attr.moveActionSelector).outerHeight(),
|
||||
width: $(this.attr.moveActionSelector).outerWidth()
|
||||
});
|
||||
window.setTimeout(
|
||||
(function() {
|
||||
this.on(document, 'click', this.hideSelector)
|
||||
}).bind(this), 0);
|
||||
};
|
||||
|
||||
this.hideSelector = function() {
|
||||
this.off(document, 'click', this.hideSelector);
|
||||
this.$node.hide();
|
||||
}
|
||||
|
||||
this.updateMailItemSelections = function(ev, data) {
|
||||
this.attr.selectedMailItems = data.selectedIds;
|
||||
}
|
||||
|
||||
this.updateFolderSelections = function(ev, data) {
|
||||
this.attr.selectedFolders = data.selectedIds;
|
||||
}
|
||||
|
||||
this.requestMoveTo = function(ev, data) {
|
||||
this.trigger('uiMoveItemsRequested', {
|
||||
itemIds: this.attr.selectedMailItems,
|
||||
fromFolder: this.attr.selectedFolders[0],
|
||||
toFolder: data.selectedIds[0]
|
||||
});
|
||||
this.$node.hide();
|
||||
};
|
||||
|
||||
this.after('initialize', function() {
|
||||
//show selector widget
|
||||
this.on(document, 'uiMoveMail', this.requestSelectorWidget);
|
||||
this.on(document, 'dataMoveToItemsServed', this.launchSelector);
|
||||
//listen for other selections
|
||||
this.on(document, 'uiMailItemSelectionChanged', this.updateMailItemSelections);
|
||||
this.on(document, 'uiFolderSelectionChanged', this.updateFolderSelections);
|
||||
//move items
|
||||
this.on('uiMoveToSelectionChanged', this.requestMoveTo);
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
'use strict';
|
||||
|
||||
define(
|
||||
|
||||
function() {
|
||||
|
||||
return withSelect;
|
||||
|
||||
function withSelect() {
|
||||
|
||||
this.defaultAttrs({
|
||||
selectedIds: []
|
||||
});
|
||||
|
||||
this.initializeSelections = function() {
|
||||
this.select('selectedItemSelector').toArray().forEach(function(el) {
|
||||
this.attr.selectedIds.push(el.getAttribute('id'));
|
||||
}, this);
|
||||
};
|
||||
|
||||
this.getSelectedIds = function() {
|
||||
return this.attr.selectedIds;
|
||||
};
|
||||
|
||||
this.toggleItemSelect = function(ev, data) {
|
||||
var $item = $(data.el), append;
|
||||
|
||||
if ($item.hasClass(this.attr.selectedClass)) {
|
||||
this.unselectItem($item);
|
||||
} else {
|
||||
append = this.attr.allowMultiSelect && (ev.metaKey || ev.ctrlKey || ev.shiftKey);
|
||||
this.selectItem($item, append);
|
||||
}
|
||||
};
|
||||
|
||||
this.selectItem = function($item, append) {
|
||||
if (!append) {
|
||||
this.select('selectedItemSelector').removeClass(this.attr.selectedClass);
|
||||
this.attr.selectedIds = [];
|
||||
}
|
||||
$item.addClass(this.attr.selectedClass);
|
||||
|
||||
this.attr.selectedIds.push($item.attr('id'));
|
||||
this.trigger(this.attr.selectionChangedEvent, {selectedIds: this.attr.selectedIds});
|
||||
};
|
||||
|
||||
this.unselectItem = function($item) {
|
||||
$item.removeClass(this.attr.selectedClass);
|
||||
|
||||
var thisIdIndex = this.attr.selectedIds.indexOf($item.attr('id'));
|
||||
this.attr.selectedIds.splice(thisIdIndex, 1);
|
||||
this.trigger(this.attr.selectionChangedEvent, {selectedIds: this.attr.selectedIds});
|
||||
};
|
||||
|
||||
this.after("initialize", function() {
|
||||
this.on('click', {
|
||||
'itemSelector': this.toggleItemSelect
|
||||
});
|
||||
|
||||
this.initializeSelections();
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
102
resources/flightjs-example-app/app/css/custom.css
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
.modal.fade.in {
|
||||
left: 5%;
|
||||
top: 10%;
|
||||
margin: auto auto auto auto;
|
||||
}
|
||||
|
||||
table {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
td.mailContact,
|
||||
span.mailSubject,
|
||||
span.mailMessage {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
td.mailContact,
|
||||
span.mailSubject {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
tr.mail-item.selected td,
|
||||
li.folder-item.selected,
|
||||
li.move-to-item.selected {
|
||||
background-color: #D9EDF7;
|
||||
}
|
||||
|
||||
tr.mail-item.selected:hover td,
|
||||
li.folder-item.selected:hover,
|
||||
li.move-to-item.selected:hover {
|
||||
background-color: #C1E1FF;
|
||||
}
|
||||
|
||||
li.folder-item:hover,
|
||||
li.move-to-item:hover {
|
||||
background-color: #EDEDED;
|
||||
}
|
||||
|
||||
li.folder-item,
|
||||
li.move-to-item {
|
||||
padding: 3px 0;
|
||||
margin-left: -15px;
|
||||
font-size: 15px;
|
||||
cursor:pointer;
|
||||
}
|
||||
|
||||
li.move-to-item {
|
||||
text-align: center;
|
||||
margin-right: -15px;
|
||||
}
|
||||
|
||||
#new_mail {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
div.compose-box {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
background-color: #FFFFFF;
|
||||
width: 350px;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
div.compose-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.compose-header {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
#recipient_select {
|
||||
width: 90%;
|
||||
margin-bottom: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div.hint {
|
||||
border: 1px solid #EAEAEA;
|
||||
color:#CACACA;
|
||||
}
|
||||
|
||||
#compose_message {
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
#compose_subject,
|
||||
#compose_message {
|
||||
font-size: 15px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
#move_to_selector {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid;
|
||||
}
|
||||
110
resources/flightjs-example-app/app/data.js
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
'use strict';
|
||||
|
||||
define(
|
||||
function() {
|
||||
return {
|
||||
folders: ["inbox", "later", "sent", "trash"],
|
||||
contacts: [
|
||||
{
|
||||
"id": "contact_342",
|
||||
"firstName": "Michael",
|
||||
"lastName": "Smith",
|
||||
"email": "ms@proxyweb.com"
|
||||
},
|
||||
{
|
||||
"id": "contact_377",
|
||||
"firstName": "Mary",
|
||||
"lastName": "Jones",
|
||||
"email": "mary@jones.net"
|
||||
},
|
||||
{
|
||||
"id": "contact_398",
|
||||
"firstName": "Billy",
|
||||
"lastName": "Idiot",
|
||||
"email": "william_idiot@gmail.com"
|
||||
}
|
||||
],
|
||||
mail: [
|
||||
{
|
||||
"id": "mail_2139",
|
||||
"contact_id": "contact_342",
|
||||
"folders": ["inbox"],
|
||||
"time": 1334891976104,
|
||||
"subject": "Consectetur adipiscing elit",
|
||||
"message": "Vestibulum vestibulum varius diam in iaculis. Praesent ultrices dui vitae nibh malesuada non iaculis ante vulputate. Suspendisse feugiat ultricies egestas. Aenean a odio libero. Quisque mollis leo et est euismod sit amet dignissim sapien venenatis. Morbi interdum adipiscing massa"
|
||||
},
|
||||
{
|
||||
"id": "mail_2143",
|
||||
"contact_id": "contact_377",
|
||||
"folders": ["inbox", "later"],
|
||||
"important": "true",
|
||||
"time": 1334884976104,
|
||||
"subject": "Neque porro quisquam velit!!",
|
||||
"message": "Curabitur sollicitudin mi eget sapien posuere semper. Fusce at neque et lacus luctus vulputate vehicula ac enim"
|
||||
},
|
||||
{
|
||||
"id": "mail_2154",
|
||||
"contact_id": "contact_398",
|
||||
"folders": ["inbox"],
|
||||
"important": "true",
|
||||
"unread": "true",
|
||||
"time": 1334874976199,
|
||||
"subject": "Proin egestas aliquam :)",
|
||||
"message": "Aenean nec erat id ipsum faucibus tristique. Nam blandit est lacinia turpis consectetur elementum. Nulla in risus ut sapien dignissim feugiat. Proin ultrices sodales imperdiet. Vestibulum vehicula blandit tincidunt. Vivamus posuere rhoncus orci, porta commodo mauris aliquam nec"
|
||||
},
|
||||
{
|
||||
"id": "mail_2176",
|
||||
"contact_id": "contact_377",
|
||||
"folders": ["inbox"],
|
||||
"time": 1334884976104,
|
||||
"subject": "Sed ut perspiciatis unde omnis?",
|
||||
"message": "laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem."
|
||||
},
|
||||
{
|
||||
"id": "mail_2191",
|
||||
"contact_id": "contact_398",
|
||||
"folders": ["inbox"],
|
||||
"unread": "true",
|
||||
"time": 1334874976199,
|
||||
"subject": "At vero eos et accusamus!",
|
||||
"message": "Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat"
|
||||
},
|
||||
{
|
||||
"id": "mail_2203",
|
||||
"contact_id": "contact_377",
|
||||
"folders": ["later"],
|
||||
"important": "true",
|
||||
"time": 1334874576199,
|
||||
"subject": "Mi netus convallis",
|
||||
"message": "Egestas morbi at. Curabitur aliquet et commodo nonummy, aliquam quis arcu, sed pellentesque vitae molestie mattis magna, in eget, risus nulla vivamus vulputate"
|
||||
},
|
||||
{
|
||||
"id": "mail_2212",
|
||||
"contact_id": "contact_398",
|
||||
"folders": ["sent"],
|
||||
"time": 1334874579867,
|
||||
"subject": "Fusce tristique pretium eros a gravida",
|
||||
"message": "Proin malesuada"
|
||||
},
|
||||
{
|
||||
"id": "mail_2021",
|
||||
"contact_id": "contact_342",
|
||||
"folders": ["trash"],
|
||||
"time": 1134874579824,
|
||||
"subject": "Phasellus vitae interdum nulla.",
|
||||
"message": "Pellentesque quam eros, mollis quis vulputate eget, pellentesque nec ipsum. Cras dignissim fringilla ligula, ac ullamcorper dui convallis blandit. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Etiam id nunc ac orci hendrerit faucibus vel in ante. Mauris nec est turpis, ut fringilla mi. Suspendisse vel tortor at nulla facilisis venenatis in sit amet ligula."
|
||||
},
|
||||
{
|
||||
"id": "mail_1976",
|
||||
"contact_id": "contact_377",
|
||||
"folders": ["trash"],
|
||||
"time": 1034874579813,
|
||||
"subject": "Fusce tristique pretium :(",
|
||||
"message": "aliquam quis arcu."
|
||||
}
|
||||
]
|
||||
};
|
||||
return data;
|
||||
}
|
||||
);
|
||||
|
||||
62
resources/flightjs-example-app/app/templates.js
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
'use strict';
|
||||
|
||||
define(
|
||||
function() {
|
||||
var mailItem =
|
||||
'{{#mailItems}}\
|
||||
<tr id="{{id}}" class="mail-item">\
|
||||
{{#important}}\
|
||||
<td class="span1"><span class="label label-important">Important</span></td>\
|
||||
{{/important}}\
|
||||
{{^important}}\
|
||||
<td class="span1"><span> </span></td>\
|
||||
{{/important}}\
|
||||
<td class="span2 mailContact">{{name}}</td>\
|
||||
<td class="span8">\
|
||||
<span class="mailSubject">\
|
||||
{{formattedSubject}}\
|
||||
</span>\
|
||||
<span class="mailMessage">\
|
||||
- <a href="#">{{formattedMessage}}</a>\
|
||||
</span>\
|
||||
</td>\
|
||||
</tr>\
|
||||
{{/mailItems}}';
|
||||
|
||||
var composeBox =
|
||||
'<div id="compose_to" class="modal-header compose-header">\
|
||||
To: <select id="recipient_select">\
|
||||
{{^reply}}<option id="recipient_hint" class="hint" style="color:#CACACA" >[Select Recipient]</option>{{/reply}}\
|
||||
{{#contacts}}\
|
||||
<option id="{{id}}"{{#recipient}} selected{{/recipient}}>{{firstName}} {{lastName}}</option>\
|
||||
{{/contacts}}\
|
||||
</select>\
|
||||
</div>\
|
||||
<div class="modal-body compose-body">\
|
||||
<div id="compose_subject" class="{{#newMail}}hint{{/newMail}}{{^newMail}}compose-header{{/newMail}}" contentEditable="true">\
|
||||
{{subject}}\
|
||||
</div>\
|
||||
<div id="compose_message" class="hint" contentEditable="true">\
|
||||
{{message}}\
|
||||
</div>\
|
||||
</div>\
|
||||
<div class="modal-footer">\
|
||||
<button id="send_composed" {{^reply}}disabled="disabled"{{/reply}} class="btn btn-primary">Send</button>\
|
||||
<button id="cancel_composed" class="btn">Cancel</button>\
|
||||
</div>';
|
||||
|
||||
var moveToSelector =
|
||||
'<ul class="nav nav-list">\
|
||||
{{#moveToItems}}\
|
||||
<li id="{{.}}" class="move-to-item">{{.}}</li>\
|
||||
{{/moveToItems}}\
|
||||
</ul>';
|
||||
|
||||
return {
|
||||
mailItem: mailItem,
|
||||
composeBox: composeBox,
|
||||
moveToSelector: moveToSelector
|
||||
}
|
||||
}
|
||||
|
||||
);
|
||||
1092
resources/flightjs-example-app/components/bootstrap/css/bootstrap-responsive.css
vendored
Normal file
9
resources/flightjs-example-app/components/bootstrap/css/bootstrap-responsive.min.css
vendored
Normal file
2470
resources/flightjs-example-app/components/bootstrap/css/bootstrap.css
vendored
Normal file
9
resources/flightjs-example-app/components/bootstrap/css/bootstrap.min.css
vendored
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 12 KiB |
2159
resources/flightjs-example-app/components/bootstrap/js/bootstrap.js
vendored
Normal file
6
resources/flightjs-example-app/components/bootstrap/js/bootstrap.min.js
vendored
Normal file
2
resources/flightjs-example-app/components/es5-shim/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
node_modules
|
||||
.DS_Store
|
||||
93
resources/flightjs-example-app/components/es5-shim/CHANGES
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
|
||||
2.0.0
|
||||
- Separate reliable shims from dubious shims (shams).
|
||||
|
||||
1.2.10
|
||||
- Group-effort Style Cleanup
|
||||
- Took a stab at fixing Object.defineProperty on IE8 without
|
||||
bad side-effects. (@hax)
|
||||
- Object.isExtensible no longer fakes it. (@xavierm)
|
||||
- Date.prototype.toISOString no longer deals with partial
|
||||
ISO dates, per spec (@kitcambridge)
|
||||
- More (mostly from @bryanforbes)
|
||||
|
||||
1.2.9
|
||||
- Corrections to toISOString by @kitcambridge
|
||||
- Fixed three bugs in array methods revealed by Jasmine tests.
|
||||
- Cleaned up Function.prototype.bind with more fixes and tests from
|
||||
@bryanforbes.
|
||||
|
||||
1.2.8
|
||||
- Actually fixed problems with Function.prototype.bind, and regressions
|
||||
from 1.2.7 (@bryanforbes, @jdalton #36)
|
||||
|
||||
1.2.7 - REGRESSED
|
||||
- Fixed problems with Function.prototype.bind when called as a constructor.
|
||||
(@jdalton #36)
|
||||
|
||||
1.2.6
|
||||
- Revised Date.parse to match ES 5.1 (kitcambridge)
|
||||
|
||||
1.2.5
|
||||
- Fixed a bug for padding it Date..toISOString (tadfisher issue #33)
|
||||
|
||||
1.2.4
|
||||
- Fixed a descriptor bug in Object.defineProperty (raynos)
|
||||
|
||||
1.2.3
|
||||
- Cleaned up RequireJS and <script> boilerplate
|
||||
|
||||
1.2.2
|
||||
- Changed reduce to follow the letter of the spec with regard to having and
|
||||
owning properties.
|
||||
- Fixed a bug where RegExps pass as Functions in some engines in reduce.
|
||||
|
||||
1.2.1
|
||||
- Adding few fixes to make jshint happy.
|
||||
- Fix for issue #12, function expressions can cause scoping issues in IE.
|
||||
- NPM will minify on install or when `npm run-script install` is executed.
|
||||
- Adding .gitignore to avoid publishing dev dependencies.
|
||||
|
||||
1.2.0
|
||||
- Making script loadable as AMD module.
|
||||
- Adding `indexOf` to the list of safe shims.
|
||||
|
||||
1.1.0
|
||||
- Added support for accessor properties where possible (which is all browsers
|
||||
except IE).
|
||||
- Stop exposing bound function's (that are returned by
|
||||
`Function.prototype.bind`) internal properties (`bound, boundTo, boundArgs`)
|
||||
as in some cases (when using facade objects for example) capabilities of the
|
||||
enclosed functions will be leaked.
|
||||
- `Object.create` now explicitly sets `__proto__` property to guarantee
|
||||
correct behavior of `Object.getPrototypeOf`'s on all objects created using
|
||||
`Object.create`.
|
||||
- Switched to `===` from `==` where possible as it's slightly faster on older
|
||||
browsers that are target of this lib.
|
||||
- Added names to all anonymous functions to have a better stack traces.
|
||||
|
||||
1.0.0
|
||||
- fixed Date.toISODate, using UTC accessors, as in
|
||||
http://code.google.com/p/v8/source/browse/trunk/src/date.js?r=6120#986
|
||||
(arian)
|
||||
|
||||
0.0.4
|
||||
- Revised Object.getPrototypeOf to work in more cases
|
||||
in response to http://ejohn.org/blog/objectgetprototypeof/
|
||||
[issue #2] (fschaefer)
|
||||
|
||||
0.0.3
|
||||
- Fixed typos in Object.keys (samsonjs)
|
||||
|
||||
0.0.2
|
||||
Per kangax's recommendations:
|
||||
- faster Object.create(null)
|
||||
- fixed a function-scope function declaration statement in Object.create
|
||||
|
||||
0.0.1
|
||||
- fixed Object.create(null), in so far as that's possible
|
||||
- reworked Rhino Object.freeze(Function) bug detector and patcher
|
||||
|
||||
0.0.0
|
||||
- forked from narwhal-lib
|
||||
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
|
||||
- kriskowal Kris Kowal Copyright (C) 2009-2011 MIT License
|
||||
- tlrobinson Tom Robinson Copyright (C) 2009-2010 MIT License (Narwhal
|
||||
Project)
|
||||
- dantman Daniel Friesen Copyright (C) 2010 XXX TODO License or CLA
|
||||
- fschaefer Florian Schäfer Copyright (C) 2010 MIT License
|
||||
- Gozala Irakli Gozalishvili Copyright (C) 2010 MIT License
|
||||
- kitcambridge Kit Cambridge Copyright (C) 2011 MIT License
|
||||
- kossnocorp Sasha Koss XXX TODO License or CLA
|
||||
- bryanforbes Bryan Forbes XXX TODO License or CLA
|
||||
- killdream Quildreen Motta Copyright (C) 2011 MIT Licence
|
||||
- michaelficarra Michael Ficarra Copyright (C) 2011 3-clause BSD
|
||||
License
|
||||
- sharkbrainguy Gerard Paapu Copyright (C) 2011 MIT License
|
||||
- bbqsrc Brendan Molloy (C) 2011 Creative Commons Zero (public domain)
|
||||
- iwyg XXX TODO License or CLA
|
||||
- DomenicDenicola Domenic Denicola Copyright (C) 2011 MIT License
|
||||
- xavierm02 Montillet Xavier Copyright (C) 2011 MIT License
|
||||
- Raynos Jake Verbaten Copyright (C) 2011 MIT Licence
|
||||
- samsonjs Sami Samhuri Copyright (C) 2010 MIT License
|
||||
- rwldrn Rick Waldron Copyright (C) 2011 MIT License
|
||||
- lexer Alexey Zakharov XXX TODO License or CLA
|
||||
- 280 North Inc. (Now Motorola LLC, a subsidiary of Google Inc.)
|
||||
Copyright (C) 2009 MIT License
|
||||
|
||||
19
resources/flightjs-example-app/components/es5-shim/LICENSE
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
|
||||
Copyright 2009, 2010 Kristopher Michael Kowal. All rights reserved.
|
||||
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.
|
||||
161
resources/flightjs-example-app/components/es5-shim/README.md
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
|
||||
`es5-shim.js` and `es5-shim.min.js` monkey-patch a JavaScript context to
|
||||
contain all EcmaScript 5 methods that can be faithfully emulated with a
|
||||
legacy JavaScript engine.
|
||||
|
||||
`es5-sham.js` and `es5-sham.min.js` monkey-patch other ES5 methods as
|
||||
closely as possible. For these methods, as closely as possible to ES5
|
||||
is not very close. Many of these shams are intended only to allow code
|
||||
to be written to ES5 without causing run-time errors in older engines.
|
||||
In many cases, this means that these shams cause many ES5 methods to
|
||||
silently fail. Decide carefully whether this is what you want.
|
||||
|
||||
|
||||
## Tests
|
||||
|
||||
The tests are written with the Jasmine BDD test framework.
|
||||
To run the tests, navigate to <root-folder>/tests/.
|
||||
|
||||
In order to run against the shim-code, the tests attempt to kill the current
|
||||
implementation of the missing methods. This happens in <root-folder>/tests/helpers/h-kill.js.
|
||||
So in order to run the tests against the build-in methods, invalidate that file somehow
|
||||
(comment-out, delete the file, delete the script-tag, etc.).
|
||||
|
||||
## Shims
|
||||
|
||||
### Complete tests ###
|
||||
|
||||
* Array.prototype.every
|
||||
* Array.prototype.filter
|
||||
* Array.prototype.forEach
|
||||
* Array.prototype.indexOf
|
||||
* Array.prototype.lastIndexOf
|
||||
* Array.prototype.map
|
||||
* Array.prototype.some
|
||||
* Array.prototype.reduce
|
||||
* Array.prototype.reduceRight
|
||||
* Array.isArray
|
||||
* Date.now
|
||||
* Date.prototype.toJSON
|
||||
* Function.prototype.bind
|
||||
* /!\ Caveat: the bound function's length is always 0.
|
||||
* /!\ Caveat: the bound function has a prototype property.
|
||||
* /!\ Caveat: bound functions do not try too hard to keep you
|
||||
from manipulating their ``arguments`` and ``caller`` properties.
|
||||
* /!\ Caveat: bound functions don't have checks in ``call`` and
|
||||
``apply`` to avoid executing as a constructor.
|
||||
* Object.keys
|
||||
* String.prototype.trim
|
||||
|
||||
### Untested ###
|
||||
|
||||
* Date.parse (for ISO parsing)
|
||||
* Date.prototype.toISOString
|
||||
|
||||
## Shams
|
||||
|
||||
* /?\ Object.create
|
||||
|
||||
For the case of simply "begetting" an object that
|
||||
inherits prototypically from another, this should work
|
||||
fine across legacy engines.
|
||||
|
||||
/!\ Object.create(null) will work only in browsers that
|
||||
support prototype assignment. This creates an object
|
||||
that does not have any properties inherited from
|
||||
Object.prototype. It will silently fail otherwise.
|
||||
|
||||
/!\ The second argument is passed to
|
||||
Object.defineProperties which will probably fail
|
||||
silently.
|
||||
|
||||
* /?\ Object.getPrototypeOf
|
||||
|
||||
This will return "undefined" in some cases. It uses
|
||||
__proto__ if it's available. Failing that, it uses
|
||||
constructor.prototype, which depends on the constructor
|
||||
property of the object's prototype having not been
|
||||
replaced. If your object was created like this, it
|
||||
won't work:
|
||||
|
||||
function Foo() {
|
||||
}
|
||||
Foo.prototype = {};
|
||||
|
||||
Because the prototype reassignment destroys the
|
||||
constructor property.
|
||||
|
||||
This will work for all objects that were created using
|
||||
`Object.create` implemented with this library.
|
||||
|
||||
* /!\ Object.getOwnPropertyNames
|
||||
|
||||
This method uses Object.keys, so it will not be accurate
|
||||
on legacy engines.
|
||||
|
||||
* Object.isSealed
|
||||
|
||||
Returns "false" in all legacy engines for all objects,
|
||||
which is conveniently guaranteed to be accurate.
|
||||
|
||||
* Object.isFrozen
|
||||
|
||||
Returns "false" in all legacy engines for all objects,
|
||||
which is conveniently guaranteed to be accurate.
|
||||
|
||||
* Object.isExtensible
|
||||
|
||||
Works like a charm, by trying very hard to extend the
|
||||
object then redacting the extension.
|
||||
|
||||
### Fail silently
|
||||
|
||||
* /!\ Object.getOwnPropertyDescriptor
|
||||
|
||||
The behavior of this shim does not conform to ES5. It
|
||||
should probably not be used at this time, until its
|
||||
behavior has been reviewed and been confirmed to be
|
||||
useful in legacy engines.
|
||||
|
||||
* /!\ Object.defineProperty
|
||||
|
||||
This method will silently fail to set "writable",
|
||||
"enumerable", and "configurable" properties.
|
||||
|
||||
Providing a getter or setter with "get" or "set" on a
|
||||
descriptor will silently fail on engines that lack
|
||||
"__defineGetter__" and "__defineSetter__", which include
|
||||
all versions of IE up to version 8 so far.
|
||||
|
||||
IE 8 provides a version of this method but it only works
|
||||
on DOM objects. Thus, the shim will not get installed
|
||||
and attempts to set "value" properties will fail
|
||||
silently on non-DOM objects.
|
||||
|
||||
https://github.com/kriskowal/es5-shim/issues#issue/5
|
||||
|
||||
* /!\ Object.defineProperties
|
||||
|
||||
This uses the Object.defineProperty shim
|
||||
|
||||
* Object.seal
|
||||
|
||||
Silently fails on all legacy engines. This should be
|
||||
fine unless you are depending on the safety and security
|
||||
provisions of this method, which you cannot possibly
|
||||
obtain in legacy engines.
|
||||
|
||||
* Object.freeze
|
||||
|
||||
Silently fails on all legacy engines. This should be
|
||||
fine unless you are depending on the safety and security
|
||||
provisions of this method, which you cannot possibly
|
||||
obtain in legacy engines.
|
||||
|
||||
* Object.preventExtensions
|
||||
|
||||
Silently fails on all legacy engines. This should be
|
||||
fine unless you are depending on the safety and security
|
||||
provisions of this method, which you cannot possibly
|
||||
obtain in legacy engines.
|
||||
|
||||
326
resources/flightjs-example-app/components/es5-shim/es5-sham.js
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
// Copyright 2009-2012 by contributors, MIT License
|
||||
// vim: ts=4 sts=4 sw=4 expandtab
|
||||
|
||||
// Module systems magic dance
|
||||
(function (definition) {
|
||||
// RequireJS
|
||||
if (typeof define == "function") {
|
||||
define(definition);
|
||||
// YUI3
|
||||
} else if (typeof YUI == "function") {
|
||||
YUI.add("es5-sham", definition);
|
||||
// CommonJS and <script>
|
||||
} else {
|
||||
definition();
|
||||
}
|
||||
})(function () {
|
||||
|
||||
// ES5 15.2.3.2
|
||||
// http://es5.github.com/#x15.2.3.2
|
||||
if (!Object.getPrototypeOf) {
|
||||
// https://github.com/kriskowal/es5-shim/issues#issue/2
|
||||
// http://ejohn.org/blog/objectgetprototypeof/
|
||||
// recommended by fschaefer on github
|
||||
Object.getPrototypeOf = function getPrototypeOf(object) {
|
||||
return object.__proto__ || (
|
||||
object.constructor
|
||||
? object.constructor.prototype
|
||||
: prototypeOfObject
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// ES5 15.2.3.3
|
||||
// http://es5.github.com/#x15.2.3.3
|
||||
if (!Object.getOwnPropertyDescriptor) {
|
||||
var ERR_NON_OBJECT = "Object.getOwnPropertyDescriptor called on a non-object: ";
|
||||
|
||||
Object.getOwnPropertyDescriptor = function getOwnPropertyDescriptor(object, property) {
|
||||
if ((typeof object != "object" && typeof object != "function") || object === null) {
|
||||
throw new TypeError(ERR_NON_OBJECT + object);
|
||||
}
|
||||
// If object does not owns property return undefined immediately.
|
||||
if (!owns(object, property)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If object has a property then it's for sure both `enumerable` and
|
||||
// `configurable`.
|
||||
var descriptor = { enumerable: true, configurable: true };
|
||||
|
||||
// If JS engine supports accessor properties then property may be a
|
||||
// getter or setter.
|
||||
if (supportsAccessors) {
|
||||
// Unfortunately `__lookupGetter__` will return a getter even
|
||||
// if object has own non getter property along with a same named
|
||||
// inherited getter. To avoid misbehavior we temporary remove
|
||||
// `__proto__` so that `__lookupGetter__` will return getter only
|
||||
// if it's owned by an object.
|
||||
var prototype = object.__proto__;
|
||||
object.__proto__ = prototypeOfObject;
|
||||
|
||||
var getter = lookupGetter(object, property);
|
||||
var setter = lookupSetter(object, property);
|
||||
|
||||
// Once we have getter and setter we can put values back.
|
||||
object.__proto__ = prototype;
|
||||
|
||||
if (getter || setter) {
|
||||
if (getter) {
|
||||
descriptor.get = getter;
|
||||
}
|
||||
if (setter) {
|
||||
descriptor.set = setter;
|
||||
}
|
||||
// If it was accessor property we're done and return here
|
||||
// in order to avoid adding `value` to the descriptor.
|
||||
return descriptor;
|
||||
}
|
||||
}
|
||||
|
||||
// If we got this far we know that object has an own property that is
|
||||
// not an accessor so we set it as a value and return descriptor.
|
||||
descriptor.value = object[property];
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
|
||||
// ES5 15.2.3.4
|
||||
// http://es5.github.com/#x15.2.3.4
|
||||
if (!Object.getOwnPropertyNames) {
|
||||
Object.getOwnPropertyNames = function getOwnPropertyNames(object) {
|
||||
return Object.keys(object);
|
||||
};
|
||||
}
|
||||
|
||||
// ES5 15.2.3.5
|
||||
// http://es5.github.com/#x15.2.3.5
|
||||
if (!Object.create) {
|
||||
Object.create = function create(prototype, properties) {
|
||||
var object;
|
||||
if (prototype === null) {
|
||||
object = { "__proto__": null };
|
||||
} else {
|
||||
if (typeof prototype != "object") {
|
||||
throw new TypeError("typeof prototype["+(typeof prototype)+"] != 'object'");
|
||||
}
|
||||
var Type = function () {};
|
||||
Type.prototype = prototype;
|
||||
object = new Type();
|
||||
// IE has no built-in implementation of `Object.getPrototypeOf`
|
||||
// neither `__proto__`, but this manually setting `__proto__` will
|
||||
// guarantee that `Object.getPrototypeOf` will work as expected with
|
||||
// objects created using `Object.create`
|
||||
object.__proto__ = prototype;
|
||||
}
|
||||
if (properties !== void 0) {
|
||||
Object.defineProperties(object, properties);
|
||||
}
|
||||
return object;
|
||||
};
|
||||
}
|
||||
|
||||
// ES5 15.2.3.6
|
||||
// http://es5.github.com/#x15.2.3.6
|
||||
|
||||
// Patch for WebKit and IE8 standard mode
|
||||
// Designed by hax <hax.github.com>
|
||||
// related issue: https://github.com/kriskowal/es5-shim/issues#issue/5
|
||||
// IE8 Reference:
|
||||
// http://msdn.microsoft.com/en-us/library/dd282900.aspx
|
||||
// http://msdn.microsoft.com/en-us/library/dd229916.aspx
|
||||
// WebKit Bugs:
|
||||
// https://bugs.webkit.org/show_bug.cgi?id=36423
|
||||
|
||||
function doesDefinePropertyWork(object) {
|
||||
try {
|
||||
Object.defineProperty(object, "sentinel", {});
|
||||
return "sentinel" in object;
|
||||
} catch (exception) {
|
||||
// returns falsy
|
||||
}
|
||||
}
|
||||
|
||||
// check whether defineProperty works if it's given. Otherwise,
|
||||
// shim partially.
|
||||
if (Object.defineProperty) {
|
||||
var definePropertyWorksOnObject = doesDefinePropertyWork({});
|
||||
var definePropertyWorksOnDom = typeof document == "undefined" ||
|
||||
doesDefinePropertyWork(document.createElement("div"));
|
||||
if (!definePropertyWorksOnObject || !definePropertyWorksOnDom) {
|
||||
var definePropertyFallback = Object.defineProperty;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Object.defineProperty || definePropertyFallback) {
|
||||
var ERR_NON_OBJECT_DESCRIPTOR = "Property description must be an object: ";
|
||||
var ERR_NON_OBJECT_TARGET = "Object.defineProperty called on non-object: "
|
||||
var ERR_ACCESSORS_NOT_SUPPORTED = "getters & setters can not be defined " +
|
||||
"on this javascript engine";
|
||||
|
||||
Object.defineProperty = function defineProperty(object, property, descriptor) {
|
||||
if ((typeof object != "object" && typeof object != "function") || object === null) {
|
||||
throw new TypeError(ERR_NON_OBJECT_TARGET + object);
|
||||
}
|
||||
if ((typeof descriptor != "object" && typeof descriptor != "function") || descriptor === null) {
|
||||
throw new TypeError(ERR_NON_OBJECT_DESCRIPTOR + descriptor);
|
||||
}
|
||||
// make a valiant attempt to use the real defineProperty
|
||||
// for I8's DOM elements.
|
||||
if (definePropertyFallback) {
|
||||
try {
|
||||
return definePropertyFallback.call(Object, object, property, descriptor);
|
||||
} catch (exception) {
|
||||
// try the shim if the real one doesn't work
|
||||
}
|
||||
}
|
||||
|
||||
// If it's a data property.
|
||||
if (owns(descriptor, "value")) {
|
||||
// fail silently if "writable", "enumerable", or "configurable"
|
||||
// are requested but not supported
|
||||
/*
|
||||
// alternate approach:
|
||||
if ( // can't implement these features; allow false but not true
|
||||
!(owns(descriptor, "writable") ? descriptor.writable : true) ||
|
||||
!(owns(descriptor, "enumerable") ? descriptor.enumerable : true) ||
|
||||
!(owns(descriptor, "configurable") ? descriptor.configurable : true)
|
||||
)
|
||||
throw new RangeError(
|
||||
"This implementation of Object.defineProperty does not " +
|
||||
"support configurable, enumerable, or writable."
|
||||
);
|
||||
*/
|
||||
|
||||
if (supportsAccessors && (lookupGetter(object, property) ||
|
||||
lookupSetter(object, property)))
|
||||
{
|
||||
// As accessors are supported only on engines implementing
|
||||
// `__proto__` we can safely override `__proto__` while defining
|
||||
// a property to make sure that we don't hit an inherited
|
||||
// accessor.
|
||||
var prototype = object.__proto__;
|
||||
object.__proto__ = prototypeOfObject;
|
||||
// Deleting a property anyway since getter / setter may be
|
||||
// defined on object itself.
|
||||
delete object[property];
|
||||
object[property] = descriptor.value;
|
||||
// Setting original `__proto__` back now.
|
||||
object.__proto__ = prototype;
|
||||
} else {
|
||||
object[property] = descriptor.value;
|
||||
}
|
||||
} else {
|
||||
if (!supportsAccessors) {
|
||||
throw new TypeError(ERR_ACCESSORS_NOT_SUPPORTED);
|
||||
}
|
||||
// If we got that far then getters and setters can be defined !!
|
||||
if (owns(descriptor, "get")) {
|
||||
defineGetter(object, property, descriptor.get);
|
||||
}
|
||||
if (owns(descriptor, "set")) {
|
||||
defineSetter(object, property, descriptor.set);
|
||||
}
|
||||
}
|
||||
return object;
|
||||
};
|
||||
}
|
||||
|
||||
// ES5 15.2.3.7
|
||||
// http://es5.github.com/#x15.2.3.7
|
||||
if (!Object.defineProperties) {
|
||||
Object.defineProperties = function defineProperties(object, properties) {
|
||||
for (var property in properties) {
|
||||
if (owns(properties, property) && property != "__proto__") {
|
||||
Object.defineProperty(object, property, properties[property]);
|
||||
}
|
||||
}
|
||||
return object;
|
||||
};
|
||||
}
|
||||
|
||||
// ES5 15.2.3.8
|
||||
// http://es5.github.com/#x15.2.3.8
|
||||
if (!Object.seal) {
|
||||
Object.seal = function seal(object) {
|
||||
// this is misleading and breaks feature-detection, but
|
||||
// allows "securable" code to "gracefully" degrade to working
|
||||
// but insecure code.
|
||||
return object;
|
||||
};
|
||||
}
|
||||
|
||||
// ES5 15.2.3.9
|
||||
// http://es5.github.com/#x15.2.3.9
|
||||
if (!Object.freeze) {
|
||||
Object.freeze = function freeze(object) {
|
||||
// this is misleading and breaks feature-detection, but
|
||||
// allows "securable" code to "gracefully" degrade to working
|
||||
// but insecure code.
|
||||
return object;
|
||||
};
|
||||
}
|
||||
|
||||
// detect a Rhino bug and patch it
|
||||
try {
|
||||
Object.freeze(function () {});
|
||||
} catch (exception) {
|
||||
Object.freeze = (function freeze(freezeObject) {
|
||||
return function freeze(object) {
|
||||
if (typeof object == "function") {
|
||||
return object;
|
||||
} else {
|
||||
return freezeObject(object);
|
||||
}
|
||||
};
|
||||
})(Object.freeze);
|
||||
}
|
||||
|
||||
// ES5 15.2.3.10
|
||||
// http://es5.github.com/#x15.2.3.10
|
||||
if (!Object.preventExtensions) {
|
||||
Object.preventExtensions = function preventExtensions(object) {
|
||||
// this is misleading and breaks feature-detection, but
|
||||
// allows "securable" code to "gracefully" degrade to working
|
||||
// but insecure code.
|
||||
return object;
|
||||
};
|
||||
}
|
||||
|
||||
// ES5 15.2.3.11
|
||||
// http://es5.github.com/#x15.2.3.11
|
||||
if (!Object.isSealed) {
|
||||
Object.isSealed = function isSealed(object) {
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
// ES5 15.2.3.12
|
||||
// http://es5.github.com/#x15.2.3.12
|
||||
if (!Object.isFrozen) {
|
||||
Object.isFrozen = function isFrozen(object) {
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
// ES5 15.2.3.13
|
||||
// http://es5.github.com/#x15.2.3.13
|
||||
if (!Object.isExtensible) {
|
||||
Object.isExtensible = function isExtensible(object) {
|
||||
// 1. If Type(O) is not Object throw a TypeError exception.
|
||||
if (Object(object) !== object) {
|
||||
throw new TypeError(); // TODO message
|
||||
}
|
||||
// 2. Return the Boolean value of the [[Extensible]] internal property of O.
|
||||
var name = '';
|
||||
while (owns(object, name)) {
|
||||
name += '?';
|
||||
}
|
||||
object[name] = true;
|
||||
var returnValue = owns(object, name);
|
||||
delete object[name];
|
||||
return returnValue;
|
||||
};
|
||||
}
|
||||
|
||||
});
|
||||
6
resources/flightjs-example-app/components/es5-shim/es5-sham.min.js
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
(function(d){"function"==typeof define?define(d):"function"==typeof YUI?YUI.add("es5-sham",d):d()})(function(){function d(a){try{return Object.defineProperty(a,"sentinel",{}),"sentinel"in a}catch(c){}}Object.getPrototypeOf||(Object.getPrototypeOf=function(a){return a.__proto__||(a.constructor?a.constructor.prototype:prototypeOfObject)});Object.getOwnPropertyDescriptor||(Object.getOwnPropertyDescriptor=function(a,c){if(typeof a!="object"&&typeof a!="function"||a===null)throw new TypeError("Object.getOwnPropertyDescriptor called on a non-object: "+
|
||||
a);if(owns(a,c)){var b={enumerable:true,configurable:true};if(supportsAccessors){var d=a.__proto__;a.__proto__=prototypeOfObject;var f=lookupGetter(a,c),e=lookupSetter(a,c);a.__proto__=d;if(f||e){if(f)b.get=f;if(e)b.set=e;return b}}b.value=a[c];return b}});Object.getOwnPropertyNames||(Object.getOwnPropertyNames=function(a){return Object.keys(a)});Object.create||(Object.create=function(a,c){var b;if(a===null)b={__proto__:null};else{if(typeof a!="object")throw new TypeError("typeof prototype["+typeof a+
|
||||
"] != 'object'");b=function(){};b.prototype=a;b=new b;b.__proto__=a}c!==void 0&&Object.defineProperties(b,c);return b});if(Object.defineProperty){var g=d({}),h="undefined"==typeof document||d(document.createElement("div"));if(!g||!h)var e=Object.defineProperty}if(!Object.defineProperty||e)Object.defineProperty=function(a,c,b){if(typeof a!="object"&&typeof a!="function"||a===null)throw new TypeError("Object.defineProperty called on non-object: "+a);if(typeof b!="object"&&typeof b!="function"||b===
|
||||
null)throw new TypeError("Property description must be an object: "+b);if(e)try{return e.call(Object,a,c,b)}catch(d){}if(owns(b,"value"))if(supportsAccessors&&(lookupGetter(a,c)||lookupSetter(a,c))){var f=a.__proto__;a.__proto__=prototypeOfObject;delete a[c];a[c]=b.value;a.__proto__=f}else a[c]=b.value;else{if(!supportsAccessors)throw new TypeError("getters & setters can not be defined on this javascript engine");owns(b,"get")&&defineGetter(a,c,b.get);owns(b,"set")&&defineSetter(a,c,b.set)}return a};
|
||||
Object.defineProperties||(Object.defineProperties=function(a,c){for(var b in c)owns(c,b)&&b!="__proto__"&&Object.defineProperty(a,b,c[b]);return a});Object.seal||(Object.seal=function(a){return a});Object.freeze||(Object.freeze=function(a){return a});try{Object.freeze(function(){})}catch(j){var i=Object.freeze;Object.freeze=function(a){return typeof a=="function"?a:i(a)}}Object.preventExtensions||(Object.preventExtensions=function(a){return a});Object.isSealed||(Object.isSealed=function(){return false});
|
||||
Object.isFrozen||(Object.isFrozen=function(){return false});Object.isExtensible||(Object.isExtensible=function(a){if(Object(a)!==a)throw new TypeError;for(var c="";owns(a,c);)c=c+"?";a[c]=true;var b=owns(a,c);delete a[c];return b})});
|
||||
778
resources/flightjs-example-app/components/es5-shim/es5-shim.js
Normal file
|
|
@ -0,0 +1,778 @@
|
|||
// Copyright 2009-2012 by contributors, MIT License
|
||||
// vim: ts=4 sts=4 sw=4 expandtab
|
||||
|
||||
// Module systems magic dance
|
||||
(function (definition) {
|
||||
// RequireJS
|
||||
if (typeof define == "function") {
|
||||
define(definition);
|
||||
// YUI3
|
||||
} else if (typeof YUI == "function") {
|
||||
YUI.add("es5", definition);
|
||||
// CommonJS and <script>
|
||||
} else {
|
||||
definition();
|
||||
}
|
||||
})(function () {
|
||||
|
||||
/**
|
||||
* Brings an environment as close to ECMAScript 5 compliance
|
||||
* as is possible with the facilities of erstwhile engines.
|
||||
*
|
||||
* Annotated ES5: http://es5.github.com/ (specific links below)
|
||||
* ES5 Spec: http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf
|
||||
* Required reading: http://javascriptweblog.wordpress.com/2011/12/05/extending-javascript-natives/
|
||||
*/
|
||||
|
||||
//
|
||||
// Function
|
||||
// ========
|
||||
//
|
||||
|
||||
// ES-5 15.3.4.5
|
||||
// http://es5.github.com/#x15.3.4.5
|
||||
|
||||
if (!Function.prototype.bind) {
|
||||
Function.prototype.bind = function bind(that) { // .length is 1
|
||||
// 1. Let Target be the this value.
|
||||
var target = this;
|
||||
// 2. If IsCallable(Target) is false, throw a TypeError exception.
|
||||
if (typeof target != "function") {
|
||||
throw new TypeError("Function.prototype.bind called on incompatible " + target);
|
||||
}
|
||||
// 3. Let A be a new (possibly empty) internal list of all of the
|
||||
// argument values provided after thisArg (arg1, arg2 etc), in order.
|
||||
// XXX slicedArgs will stand in for "A" if used
|
||||
var args = slice.call(arguments, 1); // for normal call
|
||||
// 4. Let F be a new native ECMAScript object.
|
||||
// 11. Set the [[Prototype]] internal property of F to the standard
|
||||
// built-in Function prototype object as specified in 15.3.3.1.
|
||||
// 12. Set the [[Call]] internal property of F as described in
|
||||
// 15.3.4.5.1.
|
||||
// 13. Set the [[Construct]] internal property of F as described in
|
||||
// 15.3.4.5.2.
|
||||
// 14. Set the [[HasInstance]] internal property of F as described in
|
||||
// 15.3.4.5.3.
|
||||
var bound = function () {
|
||||
|
||||
if (this instanceof bound) {
|
||||
// 15.3.4.5.2 [[Construct]]
|
||||
// When the [[Construct]] internal method of a function object,
|
||||
// F that was created using the bind function is called with a
|
||||
// list of arguments ExtraArgs, the following steps are taken:
|
||||
// 1. Let target be the value of F's [[TargetFunction]]
|
||||
// internal property.
|
||||
// 2. If target has no [[Construct]] internal method, a
|
||||
// TypeError exception is thrown.
|
||||
// 3. Let boundArgs be the value of F's [[BoundArgs]] internal
|
||||
// property.
|
||||
// 4. Let args be a new list containing the same values as the
|
||||
// list boundArgs in the same order followed by the same
|
||||
// values as the list ExtraArgs in the same order.
|
||||
// 5. Return the result of calling the [[Construct]] internal
|
||||
// method of target providing args as the arguments.
|
||||
|
||||
var F = function(){};
|
||||
F.prototype = target.prototype;
|
||||
var self = new F;
|
||||
|
||||
var result = target.apply(
|
||||
self,
|
||||
args.concat(slice.call(arguments))
|
||||
);
|
||||
if (Object(result) === result) {
|
||||
return result;
|
||||
}
|
||||
return self;
|
||||
|
||||
} else {
|
||||
// 15.3.4.5.1 [[Call]]
|
||||
// When the [[Call]] internal method of a function object, F,
|
||||
// which was created using the bind function is called with a
|
||||
// this value and a list of arguments ExtraArgs, the following
|
||||
// steps are taken:
|
||||
// 1. Let boundArgs be the value of F's [[BoundArgs]] internal
|
||||
// property.
|
||||
// 2. Let boundThis be the value of F's [[BoundThis]] internal
|
||||
// property.
|
||||
// 3. Let target be the value of F's [[TargetFunction]] internal
|
||||
// property.
|
||||
// 4. Let args be a new list containing the same values as the
|
||||
// list boundArgs in the same order followed by the same
|
||||
// values as the list ExtraArgs in the same order.
|
||||
// 5. Return the result of calling the [[Call]] internal method
|
||||
// of target providing boundThis as the this value and
|
||||
// providing args as the arguments.
|
||||
|
||||
// equiv: target.call(this, ...boundArgs, ...args)
|
||||
return target.apply(
|
||||
that,
|
||||
args.concat(slice.call(arguments))
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
// XXX bound.length is never writable, so don't even try
|
||||
//
|
||||
// 15. If the [[Class]] internal property of Target is "Function", then
|
||||
// a. Let L be the length property of Target minus the length of A.
|
||||
// b. Set the length own property of F to either 0 or L, whichever is
|
||||
// larger.
|
||||
// 16. Else set the length own property of F to 0.
|
||||
// 17. Set the attributes of the length own property of F to the values
|
||||
// specified in 15.3.5.1.
|
||||
|
||||
// TODO
|
||||
// 18. Set the [[Extensible]] internal property of F to true.
|
||||
|
||||
// TODO
|
||||
// 19. Let thrower be the [[ThrowTypeError]] function Object (13.2.3).
|
||||
// 20. Call the [[DefineOwnProperty]] internal method of F with
|
||||
// arguments "caller", PropertyDescriptor {[[Get]]: thrower, [[Set]]:
|
||||
// thrower, [[Enumerable]]: false, [[Configurable]]: false}, and
|
||||
// false.
|
||||
// 21. Call the [[DefineOwnProperty]] internal method of F with
|
||||
// arguments "arguments", PropertyDescriptor {[[Get]]: thrower,
|
||||
// [[Set]]: thrower, [[Enumerable]]: false, [[Configurable]]: false},
|
||||
// and false.
|
||||
|
||||
// TODO
|
||||
// NOTE Function objects created using Function.prototype.bind do not
|
||||
// have a prototype property or the [[Code]], [[FormalParameters]], and
|
||||
// [[Scope]] internal properties.
|
||||
// XXX can't delete prototype in pure-js.
|
||||
|
||||
// 22. Return F.
|
||||
return bound;
|
||||
};
|
||||
}
|
||||
|
||||
// Shortcut to an often accessed properties, in order to avoid multiple
|
||||
// dereference that costs universally.
|
||||
// _Please note: Shortcuts are defined after `Function.prototype.bind` as we
|
||||
// us it in defining shortcuts.
|
||||
var call = Function.prototype.call;
|
||||
var prototypeOfArray = Array.prototype;
|
||||
var prototypeOfObject = Object.prototype;
|
||||
var slice = prototypeOfArray.slice;
|
||||
// Having a toString local variable name breaks in Opera so use _toString.
|
||||
var _toString = call.bind(prototypeOfObject.toString);
|
||||
var owns = call.bind(prototypeOfObject.hasOwnProperty);
|
||||
|
||||
// If JS engine supports accessors creating shortcuts.
|
||||
var defineGetter;
|
||||
var defineSetter;
|
||||
var lookupGetter;
|
||||
var lookupSetter;
|
||||
var supportsAccessors;
|
||||
if ((supportsAccessors = owns(prototypeOfObject, "__defineGetter__"))) {
|
||||
defineGetter = call.bind(prototypeOfObject.__defineGetter__);
|
||||
defineSetter = call.bind(prototypeOfObject.__defineSetter__);
|
||||
lookupGetter = call.bind(prototypeOfObject.__lookupGetter__);
|
||||
lookupSetter = call.bind(prototypeOfObject.__lookupSetter__);
|
||||
}
|
||||
|
||||
//
|
||||
// Array
|
||||
// =====
|
||||
//
|
||||
|
||||
// ES5 15.4.3.2
|
||||
// http://es5.github.com/#x15.4.3.2
|
||||
// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/isArray
|
||||
if (!Array.isArray) {
|
||||
Array.isArray = function isArray(obj) {
|
||||
return _toString(obj) == "[object Array]";
|
||||
};
|
||||
}
|
||||
|
||||
// The IsCallable() check in the Array functions
|
||||
// has been replaced with a strict check on the
|
||||
// internal class of the object to trap cases where
|
||||
// the provided function was actually a regular
|
||||
// expression literal, which in V8 and
|
||||
// JavaScriptCore is a typeof "function". Only in
|
||||
// V8 are regular expression literals permitted as
|
||||
// reduce parameters, so it is desirable in the
|
||||
// general case for the shim to match the more
|
||||
// strict and common behavior of rejecting regular
|
||||
// expressions.
|
||||
|
||||
// ES5 15.4.4.18
|
||||
// http://es5.github.com/#x15.4.4.18
|
||||
// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/forEach
|
||||
if (!Array.prototype.forEach) {
|
||||
Array.prototype.forEach = function forEach(fun /*, thisp*/) {
|
||||
var self = toObject(this),
|
||||
thisp = arguments[1],
|
||||
i = -1,
|
||||
length = self.length >>> 0;
|
||||
|
||||
// If no callback function or if callback is not a callable function
|
||||
if (_toString(fun) != "[object Function]") {
|
||||
throw new TypeError(); // TODO message
|
||||
}
|
||||
|
||||
while (++i < length) {
|
||||
if (i in self) {
|
||||
// Invoke the callback function with call, passing arguments:
|
||||
// context, property value, property key, thisArg object context
|
||||
fun.call(thisp, self[i], i, self);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ES5 15.4.4.19
|
||||
// http://es5.github.com/#x15.4.4.19
|
||||
// https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/map
|
||||
if (!Array.prototype.map) {
|
||||
Array.prototype.map = function map(fun /*, thisp*/) {
|
||||
var self = toObject(this),
|
||||
length = self.length >>> 0,
|
||||
result = Array(length),
|
||||
thisp = arguments[1];
|
||||
|
||||
// If no callback function or if callback is not a callable function
|
||||
if (_toString(fun) != "[object Function]") {
|
||||
throw new TypeError(fun + " is not a function");
|
||||
}
|
||||
|
||||
for (var i = 0; i < length; i++) {
|
||||
if (i in self)
|
||||
result[i] = fun.call(thisp, self[i], i, self);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
// ES5 15.4.4.20
|
||||
// http://es5.github.com/#x15.4.4.20
|
||||
// https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/filter
|
||||
if (!Array.prototype.filter) {
|
||||
Array.prototype.filter = function filter(fun /*, thisp */) {
|
||||
var self = toObject(this),
|
||||
length = self.length >>> 0,
|
||||
result = [],
|
||||
value,
|
||||
thisp = arguments[1];
|
||||
|
||||
// If no callback function or if callback is not a callable function
|
||||
if (_toString(fun) != "[object Function]") {
|
||||
throw new TypeError(fun + " is not a function");
|
||||
}
|
||||
|
||||
for (var i = 0; i < length; i++) {
|
||||
if (i in self) {
|
||||
value = self[i];
|
||||
if (fun.call(thisp, value, i, self)) {
|
||||
result.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
// ES5 15.4.4.16
|
||||
// http://es5.github.com/#x15.4.4.16
|
||||
// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/every
|
||||
if (!Array.prototype.every) {
|
||||
Array.prototype.every = function every(fun /*, thisp */) {
|
||||
var self = toObject(this),
|
||||
length = self.length >>> 0,
|
||||
thisp = arguments[1];
|
||||
|
||||
// If no callback function or if callback is not a callable function
|
||||
if (_toString(fun) != "[object Function]") {
|
||||
throw new TypeError(fun + " is not a function");
|
||||
}
|
||||
|
||||
for (var i = 0; i < length; i++) {
|
||||
if (i in self && !fun.call(thisp, self[i], i, self)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
// ES5 15.4.4.17
|
||||
// http://es5.github.com/#x15.4.4.17
|
||||
// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/some
|
||||
if (!Array.prototype.some) {
|
||||
Array.prototype.some = function some(fun /*, thisp */) {
|
||||
var self = toObject(this),
|
||||
length = self.length >>> 0,
|
||||
thisp = arguments[1];
|
||||
|
||||
// If no callback function or if callback is not a callable function
|
||||
if (_toString(fun) != "[object Function]") {
|
||||
throw new TypeError(fun + " is not a function");
|
||||
}
|
||||
|
||||
for (var i = 0; i < length; i++) {
|
||||
if (i in self && fun.call(thisp, self[i], i, self)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
// ES5 15.4.4.21
|
||||
// http://es5.github.com/#x15.4.4.21
|
||||
// https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/reduce
|
||||
if (!Array.prototype.reduce) {
|
||||
Array.prototype.reduce = function reduce(fun /*, initial*/) {
|
||||
var self = toObject(this),
|
||||
length = self.length >>> 0;
|
||||
|
||||
// If no callback function or if callback is not a callable function
|
||||
if (_toString(fun) != "[object Function]") {
|
||||
throw new TypeError(fun + " is not a function");
|
||||
}
|
||||
|
||||
// no value to return if no initial value and an empty array
|
||||
if (!length && arguments.length == 1) {
|
||||
throw new TypeError('reduce of empty array with no initial value');
|
||||
}
|
||||
|
||||
var i = 0;
|
||||
var result;
|
||||
if (arguments.length >= 2) {
|
||||
result = arguments[1];
|
||||
} else {
|
||||
do {
|
||||
if (i in self) {
|
||||
result = self[i++];
|
||||
break;
|
||||
}
|
||||
|
||||
// if array contains no values, no initial value to return
|
||||
if (++i >= length) {
|
||||
throw new TypeError('reduce of empty array with no initial value');
|
||||
}
|
||||
} while (true);
|
||||
}
|
||||
|
||||
for (; i < length; i++) {
|
||||
if (i in self) {
|
||||
result = fun.call(void 0, result, self[i], i, self);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
// ES5 15.4.4.22
|
||||
// http://es5.github.com/#x15.4.4.22
|
||||
// https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/reduceRight
|
||||
if (!Array.prototype.reduceRight) {
|
||||
Array.prototype.reduceRight = function reduceRight(fun /*, initial*/) {
|
||||
var self = toObject(this),
|
||||
length = self.length >>> 0;
|
||||
|
||||
// If no callback function or if callback is not a callable function
|
||||
if (_toString(fun) != "[object Function]") {
|
||||
throw new TypeError(fun + " is not a function");
|
||||
}
|
||||
|
||||
// no value to return if no initial value, empty array
|
||||
if (!length && arguments.length == 1) {
|
||||
throw new TypeError('reduceRight of empty array with no initial value');
|
||||
}
|
||||
|
||||
var result, i = length - 1;
|
||||
if (arguments.length >= 2) {
|
||||
result = arguments[1];
|
||||
} else {
|
||||
do {
|
||||
if (i in self) {
|
||||
result = self[i--];
|
||||
break;
|
||||
}
|
||||
|
||||
// if array contains no values, no initial value to return
|
||||
if (--i < 0) {
|
||||
throw new TypeError('reduceRight of empty array with no initial value');
|
||||
}
|
||||
} while (true);
|
||||
}
|
||||
|
||||
do {
|
||||
if (i in this) {
|
||||
result = fun.call(void 0, result, self[i], i, self);
|
||||
}
|
||||
} while (i--);
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
// ES5 15.4.4.14
|
||||
// http://es5.github.com/#x15.4.4.14
|
||||
// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/indexOf
|
||||
if (!Array.prototype.indexOf) {
|
||||
Array.prototype.indexOf = function indexOf(sought /*, fromIndex */ ) {
|
||||
var self = toObject(this),
|
||||
length = self.length >>> 0;
|
||||
|
||||
if (!length) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
var i = 0;
|
||||
if (arguments.length > 1) {
|
||||
i = toInteger(arguments[1]);
|
||||
}
|
||||
|
||||
// handle negative indices
|
||||
i = i >= 0 ? i : Math.max(0, length + i);
|
||||
for (; i < length; i++) {
|
||||
if (i in self && self[i] === sought) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
}
|
||||
|
||||
// ES5 15.4.4.15
|
||||
// http://es5.github.com/#x15.4.4.15
|
||||
// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/lastIndexOf
|
||||
if (!Array.prototype.lastIndexOf) {
|
||||
Array.prototype.lastIndexOf = function lastIndexOf(sought /*, fromIndex */) {
|
||||
var self = toObject(this),
|
||||
length = self.length >>> 0;
|
||||
|
||||
if (!length) {
|
||||
return -1;
|
||||
}
|
||||
var i = length - 1;
|
||||
if (arguments.length > 1) {
|
||||
i = Math.min(i, toInteger(arguments[1]));
|
||||
}
|
||||
// handle negative indices
|
||||
i = i >= 0 ? i : length - Math.abs(i);
|
||||
for (; i >= 0; i--) {
|
||||
if (i in self && sought === self[i]) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Object
|
||||
// ======
|
||||
//
|
||||
|
||||
// ES5 15.2.3.14
|
||||
// http://es5.github.com/#x15.2.3.14
|
||||
if (!Object.keys) {
|
||||
// http://whattheheadsaid.com/2010/10/a-safer-object-keys-compatibility-implementation
|
||||
var hasDontEnumBug = true,
|
||||
dontEnums = [
|
||||
"toString",
|
||||
"toLocaleString",
|
||||
"valueOf",
|
||||
"hasOwnProperty",
|
||||
"isPrototypeOf",
|
||||
"propertyIsEnumerable",
|
||||
"constructor"
|
||||
],
|
||||
dontEnumsLength = dontEnums.length;
|
||||
|
||||
for (var key in {"toString": null}) {
|
||||
hasDontEnumBug = false;
|
||||
}
|
||||
|
||||
Object.keys = function keys(object) {
|
||||
|
||||
if ((typeof object != "object" && typeof object != "function") || object === null) {
|
||||
throw new TypeError("Object.keys called on a non-object");
|
||||
}
|
||||
|
||||
var keys = [];
|
||||
for (var name in object) {
|
||||
if (owns(object, name)) {
|
||||
keys.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasDontEnumBug) {
|
||||
for (var i = 0, ii = dontEnumsLength; i < ii; i++) {
|
||||
var dontEnum = dontEnums[i];
|
||||
if (owns(object, dontEnum)) {
|
||||
keys.push(dontEnum);
|
||||
}
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
//
|
||||
// Date
|
||||
// ====
|
||||
//
|
||||
|
||||
// ES5 15.9.5.43
|
||||
// http://es5.github.com/#x15.9.5.43
|
||||
// This function returns a String value represent the instance in time
|
||||
// represented by this Date object. The format of the String is the Date Time
|
||||
// string format defined in 15.9.1.15. All fields are present in the String.
|
||||
// The time zone is always UTC, denoted by the suffix Z. If the time value of
|
||||
// this object is not a finite Number a RangeError exception is thrown.
|
||||
if (!Date.prototype.toISOString || (new Date(-62198755200000).toISOString().indexOf('-000001') === -1)) {
|
||||
Date.prototype.toISOString = function toISOString() {
|
||||
var result, length, value, year;
|
||||
if (!isFinite(this)) {
|
||||
throw new RangeError("Date.prototype.toISOString called on non-finite value.");
|
||||
}
|
||||
|
||||
// the date time string format is specified in 15.9.1.15.
|
||||
result = [this.getUTCMonth() + 1, this.getUTCDate(),
|
||||
this.getUTCHours(), this.getUTCMinutes(), this.getUTCSeconds()];
|
||||
year = this.getUTCFullYear();
|
||||
year = (year < 0 ? '-' : (year > 9999 ? '+' : '')) + ('00000' + Math.abs(year)).slice(0 <= year && year <= 9999 ? -4 : -6);
|
||||
|
||||
length = result.length;
|
||||
while (length--) {
|
||||
value = result[length];
|
||||
// pad months, days, hours, minutes, and seconds to have two digits.
|
||||
if (value < 10) {
|
||||
result[length] = "0" + value;
|
||||
}
|
||||
}
|
||||
// pad milliseconds to have three digits.
|
||||
return year + "-" + result.slice(0, 2).join("-") + "T" + result.slice(2).join(":") + "." +
|
||||
("000" + this.getUTCMilliseconds()).slice(-3) + "Z";
|
||||
}
|
||||
}
|
||||
|
||||
// ES5 15.9.4.4
|
||||
// http://es5.github.com/#x15.9.4.4
|
||||
if (!Date.now) {
|
||||
Date.now = function now() {
|
||||
return new Date().getTime();
|
||||
};
|
||||
}
|
||||
|
||||
// ES5 15.9.5.44
|
||||
// http://es5.github.com/#x15.9.5.44
|
||||
// This function provides a String representation of a Date object for use by
|
||||
// JSON.stringify (15.12.3).
|
||||
if (!Date.prototype.toJSON) {
|
||||
Date.prototype.toJSON = function toJSON(key) {
|
||||
// When the toJSON method is called with argument key, the following
|
||||
// steps are taken:
|
||||
|
||||
// 1. Let O be the result of calling ToObject, giving it the this
|
||||
// value as its argument.
|
||||
// 2. Let tv be ToPrimitive(O, hint Number).
|
||||
// 3. If tv is a Number and is not finite, return null.
|
||||
// XXX
|
||||
// 4. Let toISO be the result of calling the [[Get]] internal method of
|
||||
// O with argument "toISOString".
|
||||
// 5. If IsCallable(toISO) is false, throw a TypeError exception.
|
||||
if (typeof this.toISOString != "function") {
|
||||
throw new TypeError('toISOString property is not callable');
|
||||
}
|
||||
// 6. Return the result of calling the [[Call]] internal method of
|
||||
// toISO with O as the this value and an empty argument list.
|
||||
return this.toISOString();
|
||||
|
||||
// NOTE 1 The argument is ignored.
|
||||
|
||||
// NOTE 2 The toJSON function is intentionally generic; it does not
|
||||
// require that its this value be a Date object. Therefore, it can be
|
||||
// transferred to other kinds of objects for use as a method. However,
|
||||
// it does require that any such object have a toISOString method. An
|
||||
// object is free to use the argument key to filter its
|
||||
// stringification.
|
||||
};
|
||||
}
|
||||
|
||||
// ES5 15.9.4.2
|
||||
// http://es5.github.com/#x15.9.4.2
|
||||
// based on work shared by Daniel Friesen (dantman)
|
||||
// http://gist.github.com/303249
|
||||
if (!Date.parse || Date.parse("+275760-09-13T00:00:00.000Z") !== 8.64e15) {
|
||||
// XXX global assignment won't work in embeddings that use
|
||||
// an alternate object for the context.
|
||||
Date = (function(NativeDate) {
|
||||
|
||||
// Date.length === 7
|
||||
var Date = function Date(Y, M, D, h, m, s, ms) {
|
||||
var length = arguments.length;
|
||||
if (this instanceof NativeDate) {
|
||||
var date = length == 1 && String(Y) === Y ? // isString(Y)
|
||||
// We explicitly pass it through parse:
|
||||
new NativeDate(Date.parse(Y)) :
|
||||
// We have to manually make calls depending on argument
|
||||
// length here
|
||||
length >= 7 ? new NativeDate(Y, M, D, h, m, s, ms) :
|
||||
length >= 6 ? new NativeDate(Y, M, D, h, m, s) :
|
||||
length >= 5 ? new NativeDate(Y, M, D, h, m) :
|
||||
length >= 4 ? new NativeDate(Y, M, D, h) :
|
||||
length >= 3 ? new NativeDate(Y, M, D) :
|
||||
length >= 2 ? new NativeDate(Y, M) :
|
||||
length >= 1 ? new NativeDate(Y) :
|
||||
new NativeDate();
|
||||
// Prevent mixups with unfixed Date object
|
||||
date.constructor = Date;
|
||||
return date;
|
||||
}
|
||||
return NativeDate.apply(this, arguments);
|
||||
};
|
||||
|
||||
// 15.9.1.15 Date Time String Format.
|
||||
var isoDateExpression = new RegExp("^" +
|
||||
"(\\d{4}|[\+\-]\\d{6})" + // four-digit year capture or sign + 6-digit extended year
|
||||
"(?:-(\\d{2})" + // optional month capture
|
||||
"(?:-(\\d{2})" + // optional day capture
|
||||
"(?:" + // capture hours:minutes:seconds.milliseconds
|
||||
"T(\\d{2})" + // hours capture
|
||||
":(\\d{2})" + // minutes capture
|
||||
"(?:" + // optional :seconds.milliseconds
|
||||
":(\\d{2})" + // seconds capture
|
||||
"(?:\\.(\\d{3}))?" + // milliseconds capture
|
||||
")?" +
|
||||
"(?:" + // capture UTC offset component
|
||||
"Z|" + // UTC capture
|
||||
"(?:" + // offset specifier +/-hours:minutes
|
||||
"([-+])" + // sign capture
|
||||
"(\\d{2})" + // hours offset capture
|
||||
":(\\d{2})" + // minutes offset capture
|
||||
")" +
|
||||
")?)?)?)?" +
|
||||
"$");
|
||||
|
||||
// Copy any custom methods a 3rd party library may have added
|
||||
for (var key in NativeDate) {
|
||||
Date[key] = NativeDate[key];
|
||||
}
|
||||
|
||||
// Copy "native" methods explicitly; they may be non-enumerable
|
||||
Date.now = NativeDate.now;
|
||||
Date.UTC = NativeDate.UTC;
|
||||
Date.prototype = NativeDate.prototype;
|
||||
Date.prototype.constructor = Date;
|
||||
|
||||
// Upgrade Date.parse to handle simplified ISO 8601 strings
|
||||
Date.parse = function parse(string) {
|
||||
var match = isoDateExpression.exec(string);
|
||||
if (match) {
|
||||
match.shift(); // kill match[0], the full match
|
||||
// parse months, days, hours, minutes, seconds, and milliseconds
|
||||
for (var i = 1; i < 7; i++) {
|
||||
// provide default values if necessary
|
||||
match[i] = +(match[i] || (i < 3 ? 1 : 0));
|
||||
// match[1] is the month. Months are 0-11 in JavaScript
|
||||
// `Date` objects, but 1-12 in ISO notation, so we
|
||||
// decrement.
|
||||
if (i == 1) {
|
||||
match[i]--;
|
||||
}
|
||||
}
|
||||
|
||||
// parse the UTC offset component
|
||||
var minuteOffset = +match.pop(), hourOffset = +match.pop(), sign = match.pop();
|
||||
|
||||
// compute the explicit time zone offset if specified
|
||||
var offset = 0;
|
||||
if (sign) {
|
||||
// detect invalid offsets and return early
|
||||
if (hourOffset > 23 || minuteOffset > 59) {
|
||||
return NaN;
|
||||
}
|
||||
|
||||
// express the provided time zone offset in minutes. The offset is
|
||||
// negative for time zones west of UTC; positive otherwise.
|
||||
offset = (hourOffset * 60 + minuteOffset) * 6e4 * (sign == "+" ? -1 : 1);
|
||||
}
|
||||
|
||||
// Date.UTC for years between 0 and 99 converts year to 1900 + year
|
||||
// The Gregorian calendar has a 400-year cycle, so
|
||||
// to Date.UTC(year + 400, .... ) - 12622780800000 == Date.UTC(year, ...),
|
||||
// where 12622780800000 - number of milliseconds in Gregorian calendar 400 years
|
||||
var year = +match[0];
|
||||
if (0 <= year && year <= 99) {
|
||||
match[0] = year + 400;
|
||||
return NativeDate.UTC.apply(this, match) + offset - 12622780800000;
|
||||
}
|
||||
|
||||
// compute a new UTC date value, accounting for the optional offset
|
||||
return NativeDate.UTC.apply(this, match) + offset;
|
||||
}
|
||||
return NativeDate.parse.apply(this, arguments);
|
||||
};
|
||||
|
||||
return Date;
|
||||
})(Date);
|
||||
}
|
||||
|
||||
//
|
||||
// String
|
||||
// ======
|
||||
//
|
||||
|
||||
// ES5 15.5.4.20
|
||||
// http://es5.github.com/#x15.5.4.20
|
||||
var ws = "\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003" +
|
||||
"\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028" +
|
||||
"\u2029\uFEFF";
|
||||
if (!String.prototype.trim || ws.trim()) {
|
||||
// http://blog.stevenlevithan.com/archives/faster-trim-javascript
|
||||
// http://perfectionkills.com/whitespace-deviations/
|
||||
ws = "[" + ws + "]";
|
||||
var trimBeginRegexp = new RegExp("^" + ws + ws + "*"),
|
||||
trimEndRegexp = new RegExp(ws + ws + "*$");
|
||||
String.prototype.trim = function trim() {
|
||||
if (this === undefined || this === null) {
|
||||
throw new TypeError("can't convert "+this+" to object");
|
||||
}
|
||||
return String(this).replace(trimBeginRegexp, "").replace(trimEndRegexp, "");
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Util
|
||||
// ======
|
||||
//
|
||||
|
||||
// ES5 9.4
|
||||
// http://es5.github.com/#x9.4
|
||||
// http://jsperf.com/to-integer
|
||||
var toInteger = function (n) {
|
||||
n = +n;
|
||||
if (n !== n) { // isNaN
|
||||
n = 0;
|
||||
} else if (n !== 0 && n !== (1/0) && n !== -(1/0)) {
|
||||
n = (n > 0 || -1) * Math.floor(Math.abs(n));
|
||||
}
|
||||
return n;
|
||||
};
|
||||
|
||||
var prepareString = "a"[0] != "a";
|
||||
// ES5 9.9
|
||||
// http://es5.github.com/#x9.9
|
||||
var toObject = function (o) {
|
||||
if (o == null) { // this matches both null and undefined
|
||||
throw new TypeError("can't convert "+o+" to object");
|
||||
}
|
||||
// If the implementation doesn't support by-index access of
|
||||
// string characters (ex. IE < 9), split the string
|
||||
if (prepareString && typeof o == "string" && o) {
|
||||
return o.split("");
|
||||
}
|
||||
return Object(o);
|
||||
};
|
||||
|
||||
});
|
||||
13
resources/flightjs-example-app/components/es5-shim/es5-shim.min.js
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
(function(f){"function"==typeof define?define(f):"function"==typeof YUI?YUI.add("es5",f):f()})(function(){Function.prototype.bind||(Function.prototype.bind=function(d){var c=this;if("function"!=typeof c)throw new TypeError("Function.prototype.bind called on incompatible "+c);var a=n.call(arguments,1),b=function(){if(this instanceof b){var e=function(){};e.prototype=c.prototype;var e=new e,i=c.apply(e,a.concat(n.call(arguments)));return Object(i)===i?i:e}return c.apply(d,a.concat(n.call(arguments)))};
|
||||
return b});var f=Function.prototype.call,m=Object.prototype,n=Array.prototype.slice,l=f.bind(m.toString),o=f.bind(m.hasOwnProperty);o(m,"__defineGetter__")&&(f.bind(m.__defineGetter__),f.bind(m.__defineSetter__),f.bind(m.__lookupGetter__),f.bind(m.__lookupSetter__));Array.isArray||(Array.isArray=function(d){return l(d)=="[object Array]"});Array.prototype.forEach||(Array.prototype.forEach=function(d,c){var a=j(this),b=-1,e=a.length>>>0;if(l(d)!="[object Function]")throw new TypeError;for(;++b<e;)b in
|
||||
a&&d.call(c,a[b],b,a)});Array.prototype.map||(Array.prototype.map=function(d,c){var a=j(this),b=a.length>>>0,e=Array(b);if(l(d)!="[object Function]")throw new TypeError(d+" is not a function");for(var i=0;i<b;i++)i in a&&(e[i]=d.call(c,a[i],i,a));return e});Array.prototype.filter||(Array.prototype.filter=function(d,c){var a=j(this),b=a.length>>>0,e=[],i;if(l(d)!="[object Function]")throw new TypeError(d+" is not a function");for(var f=0;f<b;f++)if(f in a){i=a[f];d.call(c,i,f,a)&&e.push(i)}return e});
|
||||
Array.prototype.every||(Array.prototype.every=function(d,c){var a=j(this),b=a.length>>>0;if(l(d)!="[object Function]")throw new TypeError(d+" is not a function");for(var e=0;e<b;e++)if(e in a&&!d.call(c,a[e],e,a))return false;return true});Array.prototype.some||(Array.prototype.some=function(d,c){var a=j(this),b=a.length>>>0;if(l(d)!="[object Function]")throw new TypeError(d+" is not a function");for(var e=0;e<b;e++)if(e in a&&d.call(c,a[e],e,a))return true;return false});Array.prototype.reduce||
|
||||
(Array.prototype.reduce=function(d){var c=j(this),a=c.length>>>0;if(l(d)!="[object Function]")throw new TypeError(d+" is not a function");if(!a&&arguments.length==1)throw new TypeError("reduce of empty array with no initial value");var b=0,e;if(arguments.length>=2)e=arguments[1];else{do{if(b in c){e=c[b++];break}if(++b>=a)throw new TypeError("reduce of empty array with no initial value");}while(1)}for(;b<a;b++)b in c&&(e=d.call(void 0,e,c[b],b,c));return e});Array.prototype.reduceRight||(Array.prototype.reduceRight=
|
||||
function(d){var c=j(this),a=c.length>>>0;if(l(d)!="[object Function]")throw new TypeError(d+" is not a function");if(!a&&arguments.length==1)throw new TypeError("reduceRight of empty array with no initial value");var b,a=a-1;if(arguments.length>=2)b=arguments[1];else{do{if(a in c){b=c[a--];break}if(--a<0)throw new TypeError("reduceRight of empty array with no initial value");}while(1)}do a in this&&(b=d.call(void 0,b,c[a],a,c));while(a--);return b});Array.prototype.indexOf||(Array.prototype.indexOf=
|
||||
function(d){var c=j(this),a=c.length>>>0;if(!a)return-1;var b=0;arguments.length>1&&(b=p(arguments[1]));for(b=b>=0?b:Math.max(0,a+b);b<a;b++)if(b in c&&c[b]===d)return b;return-1});Array.prototype.lastIndexOf||(Array.prototype.lastIndexOf=function(d){var c=j(this),a=c.length>>>0;if(!a)return-1;var b=a-1;arguments.length>1&&(b=Math.min(b,p(arguments[1])));for(b=b>=0?b:a-Math.abs(b);b>=0;b--)if(b in c&&d===c[b])return b;return-1});if(!Object.keys){var q=!0,r="toString toLocaleString valueOf hasOwnProperty isPrototypeOf propertyIsEnumerable constructor".split(" "),
|
||||
s=r.length,t;for(t in{toString:null})q=!1;Object.keys=function(d){if(typeof d!="object"&&typeof d!="function"||d===null)throw new TypeError("Object.keys called on a non-object");var c=[],a;for(a in d)o(d,a)&&c.push(a);if(q)for(a=0;a<s;a++){var b=r[a];o(d,b)&&c.push(b)}return c}}if(!Date.prototype.toISOString||-1===(new Date(-621987552E5)).toISOString().indexOf("-000001"))Date.prototype.toISOString=function(){var d,c,a,b;if(!isFinite(this))throw new RangeError("Date.prototype.toISOString called on non-finite value.");
|
||||
d=[this.getUTCMonth()+1,this.getUTCDate(),this.getUTCHours(),this.getUTCMinutes(),this.getUTCSeconds()];b=this.getUTCFullYear();b=(b<0?"-":b>9999?"+":"")+("00000"+Math.abs(b)).slice(0<=b&&b<=9999?-4:-6);for(c=d.length;c--;){a=d[c];a<10&&(d[c]="0"+a)}return b+"-"+d.slice(0,2).join("-")+"T"+d.slice(2).join(":")+"."+("000"+this.getUTCMilliseconds()).slice(-3)+"Z"};Date.now||(Date.now=function(){return(new Date).getTime()});Date.prototype.toJSON||(Date.prototype.toJSON=function(){if(typeof this.toISOString!=
|
||||
"function")throw new TypeError("toISOString property is not callable");return this.toISOString()});if(!Date.parse||864E13!==Date.parse("+275760-09-13T00:00:00.000Z")){var g=Date,f=function c(a,b,e,f,h,j,l){var k=arguments.length;if(this instanceof g){k=k==1&&String(a)===a?new g(c.parse(a)):k>=7?new g(a,b,e,f,h,j,l):k>=6?new g(a,b,e,f,h,j):k>=5?new g(a,b,e,f,h):k>=4?new g(a,b,e,f):k>=3?new g(a,b,e):k>=2?new g(a,b):k>=1?new g(a):new g;k.constructor=c;return k}return g.apply(this,arguments)},u=RegExp("^(\\d{4}|[+-]\\d{6})(?:-(\\d{2})(?:-(\\d{2})(?:T(\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.(\\d{3}))?)?(?:Z|(?:([-+])(\\d{2}):(\\d{2})))?)?)?)?$"),
|
||||
h;for(h in g)f[h]=g[h];f.now=g.now;f.UTC=g.UTC;f.prototype=g.prototype;f.prototype.constructor=f;f.parse=function(c){var a=u.exec(c);if(a){a.shift();for(var b=1;b<7;b++){a[b]=+(a[b]||(b<3?1:0));b==1&&a[b]--}var e=+a.pop(),f=+a.pop(),h=a.pop(),b=0;if(h){if(f>23||e>59)return NaN;b=(f*60+e)*6E4*(h=="+"?-1:1)}e=+a[0];if(0<=e&&e<=99){a[0]=e+400;return g.UTC.apply(this,a)+b-126227808E5}return g.UTC.apply(this,a)+b}return g.parse.apply(this,arguments)};Date=f}h="\t\n\x0B\f\r \u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\ufeff";
|
||||
if(!String.prototype.trim||h.trim()){h="["+h+"]";var v=RegExp("^"+h+h+"*"),w=RegExp(h+h+"*$");String.prototype.trim=function(){if(this===void 0||this===null)throw new TypeError("can't convert "+this+" to object");return String(this).replace(v,"").replace(w,"")}}var p=function(c){c=+c;c!==c?c=0:c!==0&&(c!==1/0&&c!==-(1/0))&&(c=(c>0||-1)*Math.floor(Math.abs(c)));return c},x="a"!="a"[0],j=function(c){if(c==null)throw new TypeError("can't convert "+c+" to object");return x&&typeof c=="string"&&c?c.split(""):
|
||||
Object(c)}});
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "es5-shim",
|
||||
"version": "2.0.0",
|
||||
"description": "ES5 as implementable on previous engines",
|
||||
"homepage": "http://github.com/kriskowal/es5-shim/",
|
||||
"contributors": [
|
||||
"Kris Kowal <kris@cixar.com> (http://github.com/kriskowal/)",
|
||||
"Sami Samhuri <sami.samhuri@gmail.com> (http://samhuri.net/)",
|
||||
"Florian Schäfer <florian.schaefer@gmail.com> (http://github.com/fschaefer)",
|
||||
"Irakli Gozalishvili <rfobic@gmail.com> (http://jeditoolkit.com)",
|
||||
"Kit Cambridge <kitcambridge@gmail.com> (http://kitcambridge.github.com)"
|
||||
],
|
||||
"bugs": {
|
||||
"mail": "kris@cixar.com",
|
||||
"url": "http://github.com/kriskowal/es5-shim/issues"
|
||||
},
|
||||
"licenses": [
|
||||
{
|
||||
"type": "MIT",
|
||||
"url": "http://github.com/kriskowal/es5-shim/raw/master/LICENSE"
|
||||
}
|
||||
],
|
||||
"main": "es5-shim.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "http://github.com/kriskowal/es5-shim.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.2.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
// This methods allows the killing of built-in functions,
|
||||
// so the shim can take over with that implementation
|
||||
var HLP = (function() {
|
||||
"use strict";
|
||||
var kill;
|
||||
|
||||
kill = function(_class, methods) {
|
||||
/*if(!Array.isArray(methods))
|
||||
return;*/
|
||||
if(!_class.originals)
|
||||
_class.originals = {};
|
||||
|
||||
for (var i = 0, len = methods.length; i < len; i++) {
|
||||
var obj = methods[i];
|
||||
_class.originals[obj] = _class[obj];
|
||||
delete _class[obj];
|
||||
if (obj in _class) {
|
||||
// try something more aggressive since V8 at least
|
||||
// appears to ignore the delete.
|
||||
_class[obj] = null;
|
||||
if (_class[obj]) {
|
||||
console.log("Couln't overwrite", obj, "of", _class);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return { kill: kill };
|
||||
}());
|
||||
|
||||
HLP.kill(Function.prototype, [
|
||||
'bind'
|
||||
]);
|
||||
|
||||
HLP.kill(Array, [
|
||||
'isArray'
|
||||
]);
|
||||
|
||||
HLP.kill(String.prototype, [
|
||||
"trim"
|
||||
]);
|
||||
|
||||
HLP.kill(Object, [
|
||||
'keys'
|
||||
]);
|
||||
|
||||
HLP.kill(Date, [
|
||||
'now', 'parse'
|
||||
]);
|
||||
|
||||
HLP.kill(Date.prototype, [
|
||||
"toJSON", "toISOString"
|
||||
]);
|
||||
|
||||
HLP.kill(Array.prototype, [
|
||||
'forEach', 'some', 'every',
|
||||
'indexOf', 'lastIndexOf',
|
||||
'map', 'filter',
|
||||
'reduce', 'reduceRight'
|
||||
]);
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
beforeEach(function() {
|
||||
this.addMatchers({
|
||||
toExactlyMatch: function(expected) {
|
||||
var a1, a2,
|
||||
l, i,
|
||||
key,
|
||||
actual = this.actual;
|
||||
|
||||
var getKeys = function(o) {
|
||||
var a = [];
|
||||
for(key in o) {
|
||||
if(o.hasOwnProperty(key)) {
|
||||
a.push(key);
|
||||
}
|
||||
}
|
||||
return a;
|
||||
}
|
||||
a1 = getKeys(actual);
|
||||
a2 = getKeys(expected);
|
||||
|
||||
l = a1.length;
|
||||
if(l !== a2.length) {
|
||||
return false;
|
||||
}
|
||||
for(i = 0; i < l; i++) {
|
||||
key = a1[i];
|
||||
expect(key).toEqual(a2[i]);
|
||||
expect(actual[key]).toEqual(expected[key]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
function implement() {
|
||||
throw 'Not implemented';
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>Jasmine Spec Runner</title>
|
||||
|
||||
<link rel="shortcut icon" type="image/png" href="lib/jasmine_favicon.png">
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="lib/jasmine.css">
|
||||
<script type="text/javascript" src="lib/jasmine.js"></script>
|
||||
<script type="text/javascript" src="lib/jasmine-html.js"></script>
|
||||
<script type="text/javascript" src="lib/json2.js"></script>
|
||||
|
||||
<!-- include helper files here... -->
|
||||
<script src="helpers/h.js"></script>
|
||||
<script src="helpers/h-kill.js"></script>
|
||||
<script src="helpers/h-matchers.js"></script>
|
||||
|
||||
<!-- include source files here... -->
|
||||
<script src="../es5-shim.js"></script>
|
||||
|
||||
<!-- include spec files here... -->
|
||||
<script src="spec/s-array.js"></script>
|
||||
<script src="spec/s-function.js"></script>
|
||||
<script src="spec/s-string.js"></script>
|
||||
<script src="spec/s-object.js"></script>
|
||||
<script src="spec/s-date.js"></script>
|
||||
|
||||
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
var jasmineEnv = jasmine.getEnv();
|
||||
jasmineEnv.updateInterval = 1000;
|
||||
|
||||
var trivialReporter = new jasmine.TrivialReporter();
|
||||
|
||||
jasmineEnv.addReporter(trivialReporter);
|
||||
|
||||
jasmineEnv.specFilter = function(spec) {
|
||||
return trivialReporter.specFilter(spec);
|
||||
};
|
||||
|
||||
var currentWindowOnload = window.onload;
|
||||
|
||||
window.onload = function() {
|
||||
if (currentWindowOnload) {
|
||||
currentWindowOnload();
|
||||
}
|
||||
execJasmine();
|
||||
};
|
||||
|
||||
function execJasmine() {
|
||||
jasmineEnv.execute();
|
||||
}
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
jasmine.TrivialReporter = function(doc) {
|
||||
this.document = doc || document;
|
||||
this.suiteDivs = {};
|
||||
this.logRunningSpecs = false;
|
||||
};
|
||||
|
||||
jasmine.TrivialReporter.prototype.createDom = function(type, attrs, childrenVarArgs) {
|
||||
var el = document.createElement(type);
|
||||
|
||||
for (var i = 2; i < arguments.length; i++) {
|
||||
var child = arguments[i];
|
||||
|
||||
if (typeof child === 'string') {
|
||||
el.appendChild(document.createTextNode(child));
|
||||
} else {
|
||||
if (child) { el.appendChild(child); }
|
||||
}
|
||||
}
|
||||
|
||||
for (var attr in attrs) {
|
||||
if (attr == "className") {
|
||||
el[attr] = attrs[attr];
|
||||
} else {
|
||||
el.setAttribute(attr, attrs[attr]);
|
||||
}
|
||||
}
|
||||
|
||||
return el;
|
||||
};
|
||||
|
||||
jasmine.TrivialReporter.prototype.reportRunnerStarting = function(runner) {
|
||||
var showPassed, showSkipped;
|
||||
|
||||
this.outerDiv = this.createDom('div', { className: 'jasmine_reporter' },
|
||||
this.createDom('div', { className: 'banner' },
|
||||
this.createDom('div', { className: 'logo' },
|
||||
this.createDom('span', { className: 'title' }, "Jasmine"),
|
||||
this.createDom('span', { className: 'version' }, runner.env.versionString())),
|
||||
this.createDom('div', { className: 'options' },
|
||||
"Show ",
|
||||
showPassed = this.createDom('input', { id: "__jasmine_TrivialReporter_showPassed__", type: 'checkbox' }),
|
||||
this.createDom('label', { "for": "__jasmine_TrivialReporter_showPassed__" }, " passed "),
|
||||
showSkipped = this.createDom('input', { id: "__jasmine_TrivialReporter_showSkipped__", type: 'checkbox' }),
|
||||
this.createDom('label', { "for": "__jasmine_TrivialReporter_showSkipped__" }, " skipped")
|
||||
)
|
||||
),
|
||||
|
||||
this.runnerDiv = this.createDom('div', { className: 'runner running' },
|
||||
this.createDom('a', { className: 'run_spec', href: '?' }, "run all"),
|
||||
this.runnerMessageSpan = this.createDom('span', {}, "Running..."),
|
||||
this.finishedAtSpan = this.createDom('span', { className: 'finished-at' }, ""))
|
||||
);
|
||||
|
||||
this.document.body.appendChild(this.outerDiv);
|
||||
|
||||
var suites = runner.suites();
|
||||
for (var i = 0; i < suites.length; i++) {
|
||||
var suite = suites[i];
|
||||
var suiteDiv = this.createDom('div', { className: 'suite' },
|
||||
this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, "run"),
|
||||
this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, suite.description));
|
||||
this.suiteDivs[suite.id] = suiteDiv;
|
||||
var parentDiv = this.outerDiv;
|
||||
if (suite.parentSuite) {
|
||||
parentDiv = this.suiteDivs[suite.parentSuite.id];
|
||||
}
|
||||
parentDiv.appendChild(suiteDiv);
|
||||
}
|
||||
|
||||
this.startedAt = new Date();
|
||||
|
||||
var self = this;
|
||||
showPassed.onclick = function(evt) {
|
||||
if (showPassed.checked) {
|
||||
self.outerDiv.className += ' show-passed';
|
||||
} else {
|
||||
self.outerDiv.className = self.outerDiv.className.replace(/ show-passed/, '');
|
||||
}
|
||||
};
|
||||
|
||||
showSkipped.onclick = function(evt) {
|
||||
if (showSkipped.checked) {
|
||||
self.outerDiv.className += ' show-skipped';
|
||||
} else {
|
||||
self.outerDiv.className = self.outerDiv.className.replace(/ show-skipped/, '');
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
jasmine.TrivialReporter.prototype.reportRunnerResults = function(runner) {
|
||||
var results = runner.results();
|
||||
var className = (results.failedCount > 0) ? "runner failed" : "runner passed";
|
||||
this.runnerDiv.setAttribute("class", className);
|
||||
//do it twice for IE
|
||||
this.runnerDiv.setAttribute("className", className);
|
||||
var specs = runner.specs();
|
||||
var specCount = 0;
|
||||
for (var i = 0; i < specs.length; i++) {
|
||||
if (this.specFilter(specs[i])) {
|
||||
specCount++;
|
||||
}
|
||||
}
|
||||
var message = "" + specCount + " spec" + (specCount == 1 ? "" : "s" ) + ", " + results.failedCount + " failure" + ((results.failedCount == 1) ? "" : "s");
|
||||
message += " in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s";
|
||||
this.runnerMessageSpan.replaceChild(this.createDom('a', { className: 'description', href: '?'}, message), this.runnerMessageSpan.firstChild);
|
||||
|
||||
this.finishedAtSpan.appendChild(document.createTextNode("Finished at " + new Date().toString()));
|
||||
};
|
||||
|
||||
jasmine.TrivialReporter.prototype.reportSuiteResults = function(suite) {
|
||||
var results = suite.results();
|
||||
var status = results.passed() ? 'passed' : 'failed';
|
||||
if (results.totalCount === 0) { // todo: change this to check results.skipped
|
||||
status = 'skipped';
|
||||
}
|
||||
this.suiteDivs[suite.id].className += " " + status;
|
||||
};
|
||||
|
||||
jasmine.TrivialReporter.prototype.reportSpecStarting = function(spec) {
|
||||
if (this.logRunningSpecs) {
|
||||
this.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...');
|
||||
}
|
||||
};
|
||||
|
||||
jasmine.TrivialReporter.prototype.reportSpecResults = function(spec) {
|
||||
var results = spec.results();
|
||||
var status = results.passed() ? 'passed' : 'failed';
|
||||
if (results.skipped) {
|
||||
status = 'skipped';
|
||||
}
|
||||
var specDiv = this.createDom('div', { className: 'spec ' + status },
|
||||
this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(spec.getFullName()) }, "run"),
|
||||
this.createDom('a', {
|
||||
className: 'description',
|
||||
href: '?spec=' + encodeURIComponent(spec.getFullName()),
|
||||
title: spec.getFullName()
|
||||
}, spec.description));
|
||||
|
||||
|
||||
var resultItems = results.getItems();
|
||||
var messagesDiv = this.createDom('div', { className: 'messages' });
|
||||
for (var i = 0; i < resultItems.length; i++) {
|
||||
var result = resultItems[i];
|
||||
|
||||
if (result.type == 'log') {
|
||||
messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString()));
|
||||
} else if (result.type == 'expect' && result.passed && !result.passed()) {
|
||||
messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message));
|
||||
|
||||
if (result.trace.stack) {
|
||||
messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (messagesDiv.childNodes.length > 0) {
|
||||
specDiv.appendChild(messagesDiv);
|
||||
}
|
||||
|
||||
this.suiteDivs[spec.suite.id].appendChild(specDiv);
|
||||
};
|
||||
|
||||
jasmine.TrivialReporter.prototype.log = function() {
|
||||
var console = jasmine.getGlobal().console;
|
||||
if (console && console.log) {
|
||||
if (console.log.apply) {
|
||||
console.log.apply(console, arguments);
|
||||
} else {
|
||||
console.log(arguments); // ie fix: console.log.apply doesn't exist on ie
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
jasmine.TrivialReporter.prototype.getLocation = function() {
|
||||
return this.document.location;
|
||||
};
|
||||
|
||||
jasmine.TrivialReporter.prototype.specFilter = function(spec) {
|
||||
var paramMap = {};
|
||||
var params = this.getLocation().search.substring(1).split('&');
|
||||
for (var i = 0; i < params.length; i++) {
|
||||
var p = params[i].split('=');
|
||||
paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]);
|
||||
}
|
||||
|
||||
if (!paramMap.spec) {
|
||||
return true;
|
||||
}
|
||||
return spec.getFullName().indexOf(paramMap.spec) === 0;
|
||||
};
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
body {
|
||||
font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif;
|
||||
}
|
||||
|
||||
|
||||
.jasmine_reporter a:visited, .jasmine_reporter a {
|
||||
color: #303;
|
||||
}
|
||||
|
||||
.jasmine_reporter a:hover, .jasmine_reporter a:active {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
.run_spec {
|
||||
float:right;
|
||||
padding-right: 5px;
|
||||
font-size: .8em;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.jasmine_reporter {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.banner {
|
||||
color: #303;
|
||||
background-color: #fef;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
float: left;
|
||||
font-size: 1.1em;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.logo .version {
|
||||
font-size: .6em;
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
.runner.running {
|
||||
background-color: yellow;
|
||||
}
|
||||
|
||||
|
||||
.options {
|
||||
text-align: right;
|
||||
font-size: .8em;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.suite {
|
||||
border: 1px outset gray;
|
||||
margin: 5px 0;
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
.suite .suite {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.suite.passed {
|
||||
background-color: #dfd;
|
||||
}
|
||||
|
||||
.suite.failed {
|
||||
background-color: #fdd;
|
||||
}
|
||||
|
||||
.spec {
|
||||
margin: 5px;
|
||||
padding-left: 1em;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.spec.failed, .spec.passed, .spec.skipped {
|
||||
padding-bottom: 5px;
|
||||
border: 1px solid gray;
|
||||
}
|
||||
|
||||
.spec.failed {
|
||||
background-color: #fbb;
|
||||
border-color: red;
|
||||
}
|
||||
|
||||
.spec.passed {
|
||||
background-color: #bfb;
|
||||
border-color: green;
|
||||
}
|
||||
|
||||
.spec.skipped {
|
||||
background-color: #bbb;
|
||||
}
|
||||
|
||||
.messages {
|
||||
border-left: 1px dashed gray;
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.passed {
|
||||
background-color: #cfc;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.failed {
|
||||
background-color: #fbb;
|
||||
}
|
||||
|
||||
.skipped {
|
||||
color: #777;
|
||||
background-color: #eee;
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
/*.resultMessage {*/
|
||||
/*white-space: pre;*/
|
||||
/*}*/
|
||||
|
||||
.resultMessage span.result {
|
||||
display: block;
|
||||
line-height: 2em;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.resultMessage .mismatch {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.stackTrace {
|
||||
white-space: pre;
|
||||
font-size: .8em;
|
||||
margin-left: 10px;
|
||||
max-height: 5em;
|
||||
overflow: auto;
|
||||
border: 1px inset red;
|
||||
padding: 1em;
|
||||
background: #eef;
|
||||
}
|
||||
|
||||
.finished-at {
|
||||
padding-left: 1em;
|
||||
font-size: .6em;
|
||||
}
|
||||
|
||||
.show-passed .passed,
|
||||
.show-skipped .skipped {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
#jasmine_content {
|
||||
position:fixed;
|
||||
right: 100%;
|
||||
}
|
||||
|
||||
.runner {
|
||||
border: 1px solid gray;
|
||||
display: block;
|
||||
margin: 5px 0;
|
||||
padding: 2px 0 2px 10px;
|
||||
}
|
||||
|
After Width: | Height: | Size: 905 B |
|
|
@ -0,0 +1,478 @@
|
|||
/*
|
||||
http://www.JSON.org/json2.js
|
||||
2009-08-17
|
||||
|
||||
Public Domain.
|
||||
|
||||
NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
|
||||
|
||||
See http://www.JSON.org/js.html
|
||||
|
||||
This file creates a global JSON object containing two methods: stringify
|
||||
and parse.
|
||||
|
||||
JSON.stringify(value, replacer, space)
|
||||
value any JavaScript value, usually an object or array.
|
||||
|
||||
replacer an optional parameter that determines how object
|
||||
values are stringified for objects. It can be a
|
||||
function or an array of strings.
|
||||
|
||||
space an optional parameter that specifies the indentation
|
||||
of nested structures. If it is omitted, the text will
|
||||
be packed without extra whitespace. If it is a number,
|
||||
it will specify the number of spaces to indent at each
|
||||
level. If it is a string (such as '\t' or ' '),
|
||||
it contains the characters used to indent at each level.
|
||||
|
||||
This method produces a JSON text from a JavaScript value.
|
||||
|
||||
When an object value is found, if the object contains a toJSON
|
||||
method, its toJSON method will be called and the result will be
|
||||
stringified. A toJSON method does not serialize: it returns the
|
||||
value represented by the name/value pair that should be serialized,
|
||||
or undefined if nothing should be serialized. The toJSON method
|
||||
will be passed the key associated with the value, and this will be
|
||||
bound to the value
|
||||
|
||||
For example, this would serialize Dates as ISO strings.
|
||||
|
||||
Date.prototype.toJSON = function (key) {
|
||||
function f(n) {
|
||||
// Format integers to have at least two digits.
|
||||
return n < 10 ? '0' + n : n;
|
||||
}
|
||||
|
||||
return this.getUTCFullYear() + '-' +
|
||||
f(this.getUTCMonth() + 1) + '-' +
|
||||
f(this.getUTCDate()) + 'T' +
|
||||
f(this.getUTCHours()) + ':' +
|
||||
f(this.getUTCMinutes()) + ':' +
|
||||
f(this.getUTCSeconds()) + 'Z';
|
||||
};
|
||||
|
||||
You can provide an optional replacer method. It will be passed the
|
||||
key and value of each member, with this bound to the containing
|
||||
object. The value that is returned from your method will be
|
||||
serialized. If your method returns undefined, then the member will
|
||||
be excluded from the serialization.
|
||||
|
||||
If the replacer parameter is an array of strings, then it will be
|
||||
used to select the members to be serialized. It filters the results
|
||||
such that only members with keys listed in the replacer array are
|
||||
stringified.
|
||||
|
||||
Values that do not have JSON representations, such as undefined or
|
||||
functions, will not be serialized. Such values in objects will be
|
||||
dropped; in arrays they will be replaced with null. You can use
|
||||
a replacer function to replace those with JSON values.
|
||||
JSON.stringify(undefined) returns undefined.
|
||||
|
||||
The optional space parameter produces a stringification of the
|
||||
value that is filled with line breaks and indentation to make it
|
||||
easier to read.
|
||||
|
||||
If the space parameter is a non-empty string, then that string will
|
||||
be used for indentation. If the space parameter is a number, then
|
||||
the indentation will be that many spaces.
|
||||
|
||||
Example:
|
||||
|
||||
text = JSON.stringify(['e', {pluribus: 'unum'}]);
|
||||
// text is '["e",{"pluribus":"unum"}]'
|
||||
|
||||
|
||||
text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
|
||||
// text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
|
||||
|
||||
text = JSON.stringify([new Date()], function (key, value) {
|
||||
return this[key] instanceof Date ?
|
||||
'Date(' + this[key] + ')' : value;
|
||||
});
|
||||
// text is '["Date(---current time---)"]'
|
||||
|
||||
|
||||
JSON.parse(text, reviver)
|
||||
This method parses a JSON text to produce an object or array.
|
||||
It can throw a SyntaxError exception.
|
||||
|
||||
The optional reviver parameter is a function that can filter and
|
||||
transform the results. It receives each of the keys and values,
|
||||
and its return value is used instead of the original value.
|
||||
If it returns what it received, then the structure is not modified.
|
||||
If it returns undefined then the member is deleted.
|
||||
|
||||
Example:
|
||||
|
||||
// Parse the text. Values that look like ISO date strings will
|
||||
// be converted to Date objects.
|
||||
|
||||
myData = JSON.parse(text, function (key, value) {
|
||||
var a;
|
||||
if (typeof value === 'string') {
|
||||
a =
|
||||
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
|
||||
if (a) {
|
||||
return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
|
||||
+a[5], +a[6]));
|
||||
}
|
||||
}
|
||||
return value;
|
||||
});
|
||||
|
||||
myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
|
||||
var d;
|
||||
if (typeof value === 'string' &&
|
||||
value.slice(0, 5) === 'Date(' &&
|
||||
value.slice(-1) === ')') {
|
||||
d = new Date(value.slice(5, -1));
|
||||
if (d) {
|
||||
return d;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
});
|
||||
|
||||
|
||||
This is a reference implementation. You are free to copy, modify, or
|
||||
redistribute.
|
||||
|
||||
This code should be minified before deployment.
|
||||
See http://javascript.crockford.com/jsmin.html
|
||||
|
||||
USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
|
||||
NOT CONTROL.
|
||||
*/
|
||||
|
||||
/*jslint evil: true */
|
||||
|
||||
/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
|
||||
call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
|
||||
getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
|
||||
lastIndex, length, parse, prototype, push, replace, slice, stringify,
|
||||
test, toJSON, toString, valueOf
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
// Create a JSON object only if one does not already exist. We create the
|
||||
// methods in a closure to avoid creating global variables.
|
||||
|
||||
if (!this.JSON) {
|
||||
this.JSON = {};
|
||||
}
|
||||
|
||||
(function () {
|
||||
|
||||
function f(n) {
|
||||
// Format integers to have at least two digits.
|
||||
return n < 10 ? '0' + n : n;
|
||||
}
|
||||
|
||||
if (typeof Date.prototype.toJSON !== 'function') {
|
||||
|
||||
Date.prototype.toJSON = function (key) {
|
||||
|
||||
return isFinite(this.valueOf()) ?
|
||||
this.getUTCFullYear() + '-' +
|
||||
f(this.getUTCMonth() + 1) + '-' +
|
||||
f(this.getUTCDate()) + 'T' +
|
||||
f(this.getUTCHours()) + ':' +
|
||||
f(this.getUTCMinutes()) + ':' +
|
||||
f(this.getUTCSeconds()) + 'Z' : null;
|
||||
};
|
||||
|
||||
String.prototype.toJSON =
|
||||
Number.prototype.toJSON =
|
||||
Boolean.prototype.toJSON = function (key) {
|
||||
return this.valueOf();
|
||||
};
|
||||
}
|
||||
|
||||
var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
|
||||
escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
|
||||
gap,
|
||||
indent,
|
||||
meta = { // table of character substitutions
|
||||
'\b': '\\b',
|
||||
'\t': '\\t',
|
||||
'\n': '\\n',
|
||||
'\f': '\\f',
|
||||
'\r': '\\r',
|
||||
'"' : '\\"',
|
||||
'\\': '\\\\'
|
||||
},
|
||||
rep;
|
||||
|
||||
|
||||
function quote(string) {
|
||||
|
||||
// If the string contains no control characters, no quote characters, and no
|
||||
// backslash characters, then we can safely slap some quotes around it.
|
||||
// Otherwise we must also replace the offending characters with safe escape
|
||||
// sequences.
|
||||
|
||||
escapable.lastIndex = 0;
|
||||
return escapable.test(string) ?
|
||||
'"' + string.replace(escapable, function (a) {
|
||||
var c = meta[a];
|
||||
return typeof c === 'string' ? c :
|
||||
'\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
|
||||
}) + '"' :
|
||||
'"' + string + '"';
|
||||
}
|
||||
|
||||
|
||||
function str(key, holder) {
|
||||
// Produce a string from holder[key].
|
||||
|
||||
var i, // The loop counter.
|
||||
k, // The member key.
|
||||
v, // The member value.
|
||||
length,
|
||||
mind = gap,
|
||||
partial,
|
||||
value = holder[key];
|
||||
|
||||
// If the value has a toJSON method, call it to obtain a replacement value.
|
||||
|
||||
if (value && typeof value === 'object' &&
|
||||
typeof value.toJSON === 'function') {
|
||||
value = value.toJSON(key);
|
||||
}
|
||||
|
||||
// If we were called with a replacer function, then call the replacer to
|
||||
// obtain a replacement value.
|
||||
|
||||
if (typeof rep === 'function') {
|
||||
value = rep.call(holder, key, value);
|
||||
}
|
||||
|
||||
// What happens next depends on the value's type.
|
||||
|
||||
switch (typeof value) {
|
||||
case 'string':
|
||||
return quote(value);
|
||||
|
||||
case 'number':
|
||||
|
||||
// JSON numbers must be finite. Encode non-finite numbers as null.
|
||||
|
||||
return isFinite(value) ? String(value) : 'null';
|
||||
|
||||
case 'boolean':
|
||||
case 'null':
|
||||
|
||||
// If the value is a boolean or null, convert it to a string. Note:
|
||||
// typeof null does not produce 'null'. The case is included here in
|
||||
// the remote chance that this gets fixed someday.
|
||||
|
||||
return String(value);
|
||||
|
||||
// If the type is 'object', we might be dealing with an object or an array or
|
||||
// null.
|
||||
|
||||
case 'object':
|
||||
|
||||
// Due to a specification blunder in ECMAScript, typeof null is 'object',
|
||||
// so watch out for that case.
|
||||
|
||||
if (!value) {
|
||||
return 'null';
|
||||
}
|
||||
|
||||
// Make an array to hold the partial results of stringifying this object value.
|
||||
|
||||
gap += indent;
|
||||
partial = [];
|
||||
|
||||
// Is the value an array?
|
||||
|
||||
if (Object.prototype.toString.apply(value) === '[object Array]') {
|
||||
|
||||
// The value is an array. Stringify every element. Use null as a placeholder
|
||||
// for non-JSON values.
|
||||
|
||||
length = value.length;
|
||||
for (i = 0; i < length; i += 1) {
|
||||
partial[i] = str(i, value) || 'null';
|
||||
}
|
||||
|
||||
// Join all of the elements together, separated with commas, and wrap them in
|
||||
// brackets.
|
||||
|
||||
v = partial.length === 0 ? '[]' :
|
||||
gap ? '[\n' + gap +
|
||||
partial.join(',\n' + gap) + '\n' +
|
||||
mind + ']' :
|
||||
'[' + partial.join(',') + ']';
|
||||
gap = mind;
|
||||
return v;
|
||||
}
|
||||
|
||||
// If the replacer is an array, use it to select the members to be stringified.
|
||||
|
||||
if (rep && typeof rep === 'object') {
|
||||
length = rep.length;
|
||||
for (i = 0; i < length; i += 1) {
|
||||
k = rep[i];
|
||||
if (typeof k === 'string') {
|
||||
v = str(k, value);
|
||||
if (v) {
|
||||
partial.push(quote(k) + (gap ? ': ' : ':') + v);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
// Otherwise, iterate through all of the keys in the object.
|
||||
|
||||
for (k in value) {
|
||||
if (Object.hasOwnProperty.call(value, k)) {
|
||||
v = str(k, value);
|
||||
if (v) {
|
||||
partial.push(quote(k) + (gap ? ': ' : ':') + v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Join all of the member texts together, separated with commas,
|
||||
// and wrap them in braces.
|
||||
|
||||
v = partial.length === 0 ? '{}' :
|
||||
gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' +
|
||||
mind + '}' : '{' + partial.join(',') + '}';
|
||||
gap = mind;
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
// If the JSON object does not yet have a stringify method, give it one.
|
||||
|
||||
if (typeof JSON.stringify !== 'function') {
|
||||
JSON.stringify = function (value, replacer, space) {
|
||||
// The stringify method takes a value and an optional replacer, and an optional
|
||||
// space parameter, and returns a JSON text. The replacer can be a function
|
||||
// that can replace values, or an array of strings that will select the keys.
|
||||
// A default replacer method can be provided. Use of the space parameter can
|
||||
// produce text that is more easily readable.
|
||||
|
||||
var i;
|
||||
gap = '';
|
||||
indent = '';
|
||||
|
||||
// If the space parameter is a number, make an indent string containing that
|
||||
// many spaces.
|
||||
|
||||
if (typeof space === 'number') {
|
||||
for (i = 0; i < space; i += 1) {
|
||||
indent += ' ';
|
||||
}
|
||||
|
||||
// If the space parameter is a string, it will be used as the indent string.
|
||||
|
||||
} else if (typeof space === 'string') {
|
||||
indent = space;
|
||||
}
|
||||
|
||||
// If there is a replacer, it must be a function or an array.
|
||||
// Otherwise, throw an error.
|
||||
|
||||
rep = replacer;
|
||||
if (replacer && typeof replacer !== 'function' &&
|
||||
(typeof replacer !== 'object' ||
|
||||
typeof replacer.length !== 'number')) {
|
||||
throw new Error('JSON.stringify');
|
||||
}
|
||||
|
||||
// Make a fake root object containing our value under the key of ''.
|
||||
// Return the result of stringifying the value.
|
||||
|
||||
return str('', {'': value});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// If the JSON object does not yet have a parse method, give it one.
|
||||
|
||||
if (typeof JSON.parse !== 'function') {
|
||||
JSON.parse = function (text, reviver) {
|
||||
|
||||
// The parse method takes a text and an optional reviver function, and returns
|
||||
// a JavaScript value if the text is a valid JSON text.
|
||||
|
||||
var j;
|
||||
|
||||
function walk(holder, key) {
|
||||
|
||||
// The walk method is used to recursively walk the resulting structure so
|
||||
// that modifications can be made.
|
||||
|
||||
var k, v, value = holder[key];
|
||||
if (value && typeof value === 'object') {
|
||||
for (k in value) {
|
||||
if (Object.hasOwnProperty.call(value, k)) {
|
||||
v = walk(value, k);
|
||||
if (v !== undefined) {
|
||||
value[k] = v;
|
||||
} else {
|
||||
delete value[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return reviver.call(holder, key, value);
|
||||
}
|
||||
|
||||
|
||||
// Parsing happens in four stages. In the first stage, we replace certain
|
||||
// Unicode characters with escape sequences. JavaScript handles many characters
|
||||
// incorrectly, either silently deleting them, or treating them as line endings.
|
||||
|
||||
cx.lastIndex = 0;
|
||||
if (cx.test(text)) {
|
||||
text = text.replace(cx, function (a) {
|
||||
return '\\u' +
|
||||
('0000' + a.charCodeAt(0).toString(16)).slice(-4);
|
||||
});
|
||||
}
|
||||
|
||||
// In the second stage, we run the text against regular expressions that look
|
||||
// for non-JSON patterns. We are especially concerned with '()' and 'new'
|
||||
// because they can cause invocation, and '=' because it can cause mutation.
|
||||
// But just to be safe, we want to reject all unexpected forms.
|
||||
|
||||
// We split the second stage into 4 regexp operations in order to work around
|
||||
// crippling inefficiencies in IE's and Safari's regexp engines. First we
|
||||
// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
|
||||
// replace all simple value tokens with ']' characters. Third, we delete all
|
||||
// open brackets that follow a colon or comma or that begin the text. Finally,
|
||||
// we look to see that the remaining characters are only whitespace or ']' or
|
||||
// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
|
||||
|
||||
if (/^[\],:{}\s]*$/.
|
||||
test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').
|
||||
replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
|
||||
replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
|
||||
|
||||
// In the third stage we use the eval function to compile the text into a
|
||||
// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
|
||||
// in JavaScript: it can begin a block or an object literal. We wrap the text
|
||||
// in parens to eliminate the ambiguity.
|
||||
|
||||
j = eval('(' + text + ')');
|
||||
|
||||
// In the optional fourth stage, we recursively walk the new structure, passing
|
||||
// each name/value pair to a reviver function for possible transformation.
|
||||
|
||||
return typeof reviver === 'function' ?
|
||||
walk({'': j}, '') : j;
|
||||
}
|
||||
|
||||
// If the text is not JSON parseable, then a SyntaxError is thrown.
|
||||
|
||||
throw new SyntaxError('JSON.parse');
|
||||
};
|
||||
}
|
||||
}());
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
describe('Date', function () {
|
||||
|
||||
describe('now', function () {
|
||||
it('should be the current time', function () {
|
||||
expect(Date.now() === new Date().getTime()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parse", function () {
|
||||
// TODO: Write the rest of the test.
|
||||
|
||||
it('should support extended years', function () {
|
||||
|
||||
expect(Date.parse('0001-01-01T00:00:00Z')).toBe(-62135596800000);
|
||||
expect(Date.parse('+275760-09-13T00:00:00.000Z')).toBe(8.64e15);
|
||||
expect(Date.parse('+033658-09-27T01:46:40.000Z')).toBe(1e15);
|
||||
expect(Date.parse('-000001-01-01T00:00:00Z')).toBe(-62198755200000);
|
||||
expect(Date.parse('+002009-12-15T00:00:00Z')).toBe(1260835200000);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("toISOString", function () {
|
||||
// TODO: write the rest of the test.
|
||||
|
||||
it('should support extended years', function () {
|
||||
expect(new Date(-62198755200000).toISOString().indexOf('-000001-01-01')).toBe(0);
|
||||
expect(new Date(8.64e15).toISOString().indexOf('+275760-09-13')).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("toJSON", function () {
|
||||
it('should return the isoString when stringified', function () {
|
||||
var date = new Date();
|
||||
expect(JSON.stringify(date.toISOString())).toBe(JSON.stringify(date));
|
||||
})
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
|
||||
describe('Function', function() {
|
||||
"use strict";
|
||||
describe('bind', function() {
|
||||
var actual, expected,
|
||||
testSubject;
|
||||
|
||||
testSubject = {
|
||||
push: function(o) {
|
||||
this.a.push(o);
|
||||
}
|
||||
};
|
||||
|
||||
function func() {
|
||||
Array.prototype.forEach.call(arguments, function(a) {
|
||||
this.push(a);
|
||||
}, this);
|
||||
return this;
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
actual = [];
|
||||
testSubject.a = [];
|
||||
});
|
||||
|
||||
it('binds properly without a context', function() {
|
||||
var context;
|
||||
testSubject.func = function() {
|
||||
context = this;
|
||||
}.bind();
|
||||
testSubject.func();
|
||||
expect(context).toBe(function() {return this}.call());
|
||||
});
|
||||
it('binds properly without a context, and still supplies bound arguments', function() {
|
||||
var a, context;
|
||||
testSubject.func = function() {
|
||||
a = Array.prototype.slice.call(arguments);
|
||||
context = this;
|
||||
}.bind(undefined, 1,2,3);
|
||||
testSubject.func(1,2,3);
|
||||
expect(a).toEqual([1,2,3,1,2,3]);
|
||||
expect(context).toBe(function() {return this}.call());
|
||||
});
|
||||
it('binds a context properly', function() {
|
||||
testSubject.func = func.bind(actual);
|
||||
testSubject.func(1,2,3);
|
||||
expect(actual).toEqual([1,2,3]);
|
||||
expect(testSubject.a).toEqual([]);
|
||||
});
|
||||
it('binds a context and supplies bound arguments', function() {
|
||||
testSubject.func = func.bind(actual, 1,2,3);
|
||||
testSubject.func(4,5,6);
|
||||
expect(actual).toEqual([1,2,3,4,5,6]);
|
||||
expect(testSubject.a).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns properly without binding a context', function() {
|
||||
testSubject.func = function() {
|
||||
return this;
|
||||
}.bind();
|
||||
var context = testSubject.func();
|
||||
expect(context).toBe(function() {return this}.call());
|
||||
});
|
||||
it('returns properly without binding a context, and still supplies bound arguments', function() {
|
||||
var context;
|
||||
testSubject.func = function() {
|
||||
context = this;
|
||||
return Array.prototype.slice.call(arguments);
|
||||
}.bind(undefined, 1,2,3);
|
||||
actual = testSubject.func(1,2,3);
|
||||
expect(context).toBe(function() {return this}.call());
|
||||
expect(actual).toEqual([1,2,3,1,2,3]);
|
||||
});
|
||||
it('returns properly while binding a context properly', function() {
|
||||
var ret;
|
||||
testSubject.func = func.bind(actual);
|
||||
ret = testSubject.func(1,2,3);
|
||||
expect(ret).toBe(actual);
|
||||
expect(ret).not.toBe(testSubject);
|
||||
});
|
||||
it('returns properly while binding a context and supplies bound arguments', function() {
|
||||
var ret;
|
||||
testSubject.func = func.bind(actual, 1,2,3);
|
||||
ret = testSubject.func(4,5,6);
|
||||
expect(ret).toBe(actual);
|
||||
expect(ret).not.toBe(testSubject);
|
||||
});
|
||||
it('passes the correct arguments as a constructor', function() {
|
||||
var ret, expected = { name: "Correct" };
|
||||
testSubject.func = function(arg) {
|
||||
return arg;
|
||||
}.bind({ name: "Incorrect" });
|
||||
ret = new testSubject.func(expected);
|
||||
expect(ret).toBe(expected);
|
||||
});
|
||||
it('returns the return value of the bound function when called as a constructor', function () {
|
||||
var oracle = [1, 2, 3];
|
||||
var subject = function () {
|
||||
return oracle;
|
||||
}.bind(null);
|
||||
var result = new subject;
|
||||
expect(result).toBe(oracle);
|
||||
});
|
||||
it('returns the correct value if constructor returns primitive', function() {
|
||||
var oracle = [1, 2, 3];
|
||||
var subject = function () {
|
||||
return oracle;
|
||||
}.bind(null);
|
||||
var result = new subject;
|
||||
expect(result).toBe(oracle);
|
||||
|
||||
oracle = {};
|
||||
result = new subject;
|
||||
expect(result).toBe(oracle);
|
||||
|
||||
oracle = function(){};
|
||||
result = new subject;
|
||||
expect(result).toBe(oracle);
|
||||
|
||||
oracle = "asdf";
|
||||
result = new subject;
|
||||
expect(result).not.toBe(oracle);
|
||||
|
||||
oracle = null;
|
||||
result = new subject;
|
||||
expect(result).not.toBe(oracle);
|
||||
|
||||
oracle = true;
|
||||
result = new subject;
|
||||
expect(result).not.toBe(oracle);
|
||||
|
||||
oracle = 1;
|
||||
result = new subject;
|
||||
expect(result).not.toBe(oracle);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
describe('Object', function () {
|
||||
"use strict";
|
||||
|
||||
describe("Object.keys", function () {
|
||||
var obj = {
|
||||
"str": "boz",
|
||||
"obj": { },
|
||||
"arr": [],
|
||||
"bool": true,
|
||||
"num": 42,
|
||||
"null": null,
|
||||
"undefined": undefined
|
||||
};
|
||||
|
||||
var loopedValues = [];
|
||||
for (var k in obj) {
|
||||
loopedValues.push(k);
|
||||
}
|
||||
|
||||
var keys = Object.keys(obj);
|
||||
it('should have correct length', function () {
|
||||
expect(keys.length).toBe(7);
|
||||
});
|
||||
|
||||
it('should return an Array', function () {
|
||||
expect(Array.isArray(keys)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return names which are own properties', function () {
|
||||
keys.forEach(function (name) {
|
||||
expect(obj.hasOwnProperty(name)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return names which are enumerable', function () {
|
||||
keys.forEach(function (name) {
|
||||
expect(loopedValues.indexOf(name)).toNotBe(-1);
|
||||
})
|
||||
});
|
||||
|
||||
it('should throw error for non object', function () {
|
||||
var e = {};
|
||||
expect(function () {
|
||||
try {
|
||||
Object.keys(42)
|
||||
} catch (err) {
|
||||
throw e;
|
||||
}
|
||||
}).toThrow(e);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Object.isExtensible", function () {
|
||||
var obj = { };
|
||||
|
||||
it('should return true if object is extensible', function () {
|
||||
expect(Object.isExtensible(obj)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if object is not extensible', function () {
|
||||
expect(Object.isExtensible(Object.preventExtensions(obj))).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if object is seal', function () {
|
||||
expect(Object.isExtensible(Object.seal(obj))).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if object is freeze', function () {
|
||||
expect(Object.isExtensible(Object.freeze(obj))).toBe(false);
|
||||
});
|
||||
|
||||
it('should throw error for non object', function () {
|
||||
var e1 = {};
|
||||
expect(function () {
|
||||
try {
|
||||
Object.isExtensible(42)
|
||||
} catch (err) {
|
||||
throw e1;
|
||||
}
|
||||
}).toThrow(e1);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
describe('String', function() {
|
||||
"use strict";
|
||||
describe("trim", function() {
|
||||
var test = "\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028\u2029\uFEFFHello, World!\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028\u2029\uFEFF";
|
||||
|
||||
it('trims all ES5 whitespace', function() {
|
||||
expect(test.trim()).toEqual("Hello, World!");
|
||||
expect(test.trim().length).toEqual(13);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
language: node_js
|
||||
node_js:
|
||||
- 0.8
|
||||
before_script:
|
||||
- "export DISPLAY=:99.0"
|
||||
- "sh -e /etc/init.d/xvfb start"
|
||||
script: "npm install; make standalone; phantomjs test/phantom-jasmine/run_jasmine_test.coffee test/run/jasmine_test.html"
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
// ==========================================
|
||||
// Copyright 2013 Twitter, Inc
|
||||
// Licensed under The MIT License
|
||||
// http://opensource.org/licenses/MIT
|
||||
// ==========================================
|
||||
|
||||
"use strict";
|
||||
|
||||
define(
|
||||
|
||||
[
|
||||
'./utils',
|
||||
'./compose'
|
||||
],
|
||||
|
||||
function (util, compose) {
|
||||
|
||||
var advice = {
|
||||
|
||||
around: function(base, wrapped) {
|
||||
return function composedAround() {
|
||||
// unpacking arguments by hand benchmarked faster
|
||||
var i = 0, l = arguments.length, args = new Array(l + 1);
|
||||
args[0] = base.bind(this);
|
||||
for (; i < l; i++) args[i + 1] = arguments[i];
|
||||
|
||||
return wrapped.apply(this, args);
|
||||
}
|
||||
},
|
||||
|
||||
before: function(base, before) {
|
||||
var beforeFn = (typeof before == 'function') ? before : before.obj[before.fnName];
|
||||
return function composedBefore() {
|
||||
beforeFn.apply(this, arguments);
|
||||
return base.apply(this, arguments);
|
||||
}
|
||||
},
|
||||
|
||||
after: function(base, after) {
|
||||
var afterFn = (typeof after == 'function') ? after : after.obj[after.fnName];
|
||||
return function composedAfter() {
|
||||
var res = (base.unbound || base).apply(this, arguments);
|
||||
afterFn.apply(this, arguments);
|
||||
return res;
|
||||
}
|
||||
},
|
||||
|
||||
// a mixin that allows other mixins to augment existing functions by adding additional
|
||||
// code before, after or around.
|
||||
withAdvice: function() {
|
||||
['before', 'after', 'around'].forEach(function(m) {
|
||||
this[m] = function(method, fn) {
|
||||
|
||||
compose.unlockProperty(this, method, function() {
|
||||
if (typeof this[method] == 'function') {
|
||||
return this[method] = advice[m](this[method], fn);
|
||||
} else {
|
||||
return this[method] = fn;
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
}, this);
|
||||
}
|
||||
};
|
||||
|
||||
return advice;
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,308 @@
|
|||
// ==========================================
|
||||
// Copyright 2013 Twitter, Inc
|
||||
// Licensed under The MIT License
|
||||
// http://opensource.org/licenses/MIT
|
||||
// ==========================================
|
||||
|
||||
"use strict";
|
||||
|
||||
define(
|
||||
|
||||
[
|
||||
'./advice',
|
||||
'./utils',
|
||||
'./compose',
|
||||
'./registry'
|
||||
],
|
||||
|
||||
function(advice, utils, compose, registry) {
|
||||
|
||||
var functionNameRegEx = /function (.*?)\s?\(/;
|
||||
var componentId = 0;
|
||||
|
||||
function teardownInstance(instanceInfo){
|
||||
instanceInfo.events.slice().forEach(function(event) {
|
||||
var args = [event.type];
|
||||
|
||||
event.element && args.unshift(event.element);
|
||||
(typeof event.callback == 'function') && args.push(event.callback);
|
||||
|
||||
this.off.apply(this, args);
|
||||
}, instanceInfo.instance);
|
||||
}
|
||||
|
||||
|
||||
function teardown() {
|
||||
teardownInstance(registry.findInstanceInfo(this));
|
||||
}
|
||||
|
||||
//teardown for all instances of this constructor
|
||||
function teardownAll() {
|
||||
var componentInfo = registry.findComponentInfo(this);
|
||||
|
||||
componentInfo && Object.keys(componentInfo.instances).forEach(function(k) {
|
||||
var info = componentInfo.instances[k];
|
||||
info.instance.teardown();
|
||||
});
|
||||
}
|
||||
|
||||
function checkSerializable(type, data) {
|
||||
try {
|
||||
window.postMessage(data, '*');
|
||||
} catch(e) {
|
||||
console.log('unserializable data for event',type,':',data);
|
||||
throw new Error(
|
||||
["The event", type, "on component", this.toString(), "was triggered with non-serializable data"].join(" ")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//common mixin allocates basic functionality - used by all component prototypes
|
||||
//callback context is bound to component
|
||||
function withBaseComponent() {
|
||||
|
||||
// delegate trigger, bind and unbind to an element
|
||||
// if $element not supplied, use component's node
|
||||
// other arguments are passed on
|
||||
// event can be either a string specifying the type
|
||||
// of the event, or a hash specifying both the type
|
||||
// and a default function to be called.
|
||||
this.trigger = function() {
|
||||
var $element, type, data, event, defaultFn;
|
||||
var lastIndex = arguments.length - 1, lastArg = arguments[lastIndex];
|
||||
|
||||
if (typeof lastArg != "string" && !(lastArg && lastArg.defaultBehavior)) {
|
||||
lastIndex--;
|
||||
data = lastArg;
|
||||
}
|
||||
|
||||
if (lastIndex == 1) {
|
||||
$element = $(arguments[0]);
|
||||
event = arguments[1];
|
||||
} else {
|
||||
$element = this.$node;
|
||||
event = arguments[0];
|
||||
}
|
||||
|
||||
if (event.defaultBehavior) {
|
||||
defaultFn = event.defaultBehavior;
|
||||
event = $.Event(event.type);
|
||||
}
|
||||
|
||||
type = event.type || event;
|
||||
|
||||
if (window.DEBUG && window.DEBUG.enabled && window.postMessage) {
|
||||
checkSerializable.call(this, type, data);
|
||||
}
|
||||
|
||||
if (typeof this.attr.eventData === 'object') {
|
||||
data = $.extend(true, {}, this.attr.eventData, data);
|
||||
}
|
||||
|
||||
$element.trigger((event || type), data);
|
||||
|
||||
if (defaultFn && !event.isDefaultPrevented()) {
|
||||
(this[defaultFn] || defaultFn).call(this);
|
||||
}
|
||||
|
||||
return $element;
|
||||
};
|
||||
|
||||
this.on = function() {
|
||||
var $element, type, callback, originalCb;
|
||||
var lastIndex = arguments.length - 1, origin = arguments[lastIndex];
|
||||
|
||||
if (typeof origin == "object") {
|
||||
//delegate callback
|
||||
originalCb = utils.delegate(
|
||||
this.resolveDelegateRules(origin)
|
||||
);
|
||||
} else {
|
||||
originalCb = origin;
|
||||
}
|
||||
|
||||
if (lastIndex == 2) {
|
||||
$element = $(arguments[0]);
|
||||
type = arguments[1];
|
||||
} else {
|
||||
$element = this.$node;
|
||||
type = arguments[0];
|
||||
}
|
||||
|
||||
if (typeof originalCb != 'function' && typeof originalCb != 'object') {
|
||||
throw new Error("Unable to bind to '" + type + "' because the given callback is not a function or an object");
|
||||
}
|
||||
|
||||
callback = originalCb.bind(this);
|
||||
callback.target = originalCb;
|
||||
|
||||
// if the original callback is already branded by jQuery's guid, copy it to the context-bound version
|
||||
if (originalCb.guid) {
|
||||
callback.guid = originalCb.guid;
|
||||
}
|
||||
|
||||
$element.on(type, callback);
|
||||
|
||||
// get jquery's guid from our bound fn, so unbinding will work
|
||||
originalCb.guid = callback.guid;
|
||||
|
||||
return callback;
|
||||
};
|
||||
|
||||
this.off = function() {
|
||||
var $element, type, callback;
|
||||
var lastIndex = arguments.length - 1;
|
||||
|
||||
if (typeof arguments[lastIndex] == "function") {
|
||||
callback = arguments[lastIndex];
|
||||
lastIndex -= 1;
|
||||
}
|
||||
|
||||
if (lastIndex == 1) {
|
||||
$element = $(arguments[0]);
|
||||
type = arguments[1];
|
||||
} else {
|
||||
$element = this.$node;
|
||||
type = arguments[0];
|
||||
}
|
||||
|
||||
return $element.off(type, callback);
|
||||
};
|
||||
|
||||
this.resolveDelegateRules = function(ruleInfo) {
|
||||
var rules = {};
|
||||
|
||||
Object.keys(ruleInfo).forEach(function(r) {
|
||||
if (!r in this.attr) {
|
||||
throw new Error('Component "' + this.toString() + '" wants to listen on "' + r + '" but no such attribute was defined.');
|
||||
}
|
||||
rules[this.attr[r]] = ruleInfo[r];
|
||||
}, this);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
this.defaultAttrs = function(defaults) {
|
||||
utils.push(this.defaults, defaults, true) || (this.defaults = defaults);
|
||||
};
|
||||
|
||||
this.select = function(attributeKey) {
|
||||
return this.$node.find(this.attr[attributeKey]);
|
||||
};
|
||||
|
||||
this.initialize = $.noop;
|
||||
this.teardown = teardown;
|
||||
}
|
||||
|
||||
function attachTo(selector/*, options args */) {
|
||||
// unpacking arguments by hand benchmarked faster
|
||||
var l = arguments.length;
|
||||
var args = new Array(l - 1);
|
||||
for (var i = 1; i < l; i++) args[i - 1] = arguments[i];
|
||||
|
||||
if (!selector) {
|
||||
throw new Error("Component needs to be attachTo'd a jQuery object, native node or selector string");
|
||||
}
|
||||
|
||||
var options = utils.merge.apply(utils, args);
|
||||
|
||||
$(selector).each(function(i, node) {
|
||||
var rawNode = node.jQuery ? node[0] : node;
|
||||
var componentInfo = registry.findComponentInfo(this)
|
||||
if (componentInfo && componentInfo.isAttachedTo(rawNode)) {
|
||||
//already attached
|
||||
return;
|
||||
}
|
||||
|
||||
new this(node, options);
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
// define the constructor for a custom component type
|
||||
// takes an unlimited number of mixin functions as arguments
|
||||
// typical api call with 3 mixins: define(timeline, withTweetCapability, withScrollCapability);
|
||||
function define(/*mixins*/) {
|
||||
// unpacking arguments by hand benchmarked faster
|
||||
var l = arguments.length;
|
||||
var mixins = new Array(l);
|
||||
for (var i = 0; i < l; i++) mixins[i] = arguments[i];
|
||||
|
||||
Component.toString = function() {
|
||||
var prettyPrintMixins = mixins.map(function(mixin) {
|
||||
if (mixin.name == null) {
|
||||
//function name property not supported by this browser, use regex
|
||||
var m = mixin.toString().match(functionNameRegEx);
|
||||
return (m && m[1]) ? m[1] : "";
|
||||
} else {
|
||||
return (mixin.name != "withBaseComponent") ? mixin.name : "";
|
||||
}
|
||||
}).filter(Boolean).join(', ');
|
||||
return prettyPrintMixins;
|
||||
};
|
||||
|
||||
if (window.DEBUG && window.DEBUG.enabled) {
|
||||
Component.describe = Component.toString();
|
||||
}
|
||||
|
||||
//'options' is optional hash to be merged with 'defaults' in the component definition
|
||||
function Component(node, options) {
|
||||
options = options || {};
|
||||
this.identity = componentId++;
|
||||
|
||||
if (!node) {
|
||||
throw new Error("Component needs a node");
|
||||
}
|
||||
|
||||
if (node.jquery) {
|
||||
this.node = node[0];
|
||||
this.$node = node;
|
||||
} else {
|
||||
this.node = node;
|
||||
this.$node = $(node);
|
||||
}
|
||||
|
||||
this.toString = Component.toString;
|
||||
if (window.DEBUG && window.DEBUG.enabled) {
|
||||
this.describe = this.toString();
|
||||
}
|
||||
|
||||
//merge defaults with supplied options
|
||||
//put options in attr.__proto__ to avoid merge overhead
|
||||
var attr = Object.create(options);
|
||||
for (var key in this.defaults) {
|
||||
if (!options.hasOwnProperty(key)) {
|
||||
attr[key] = this.defaults[key];
|
||||
}
|
||||
}
|
||||
this.attr = attr;
|
||||
|
||||
Object.keys(this.defaults || {}).forEach(function(key) {
|
||||
if (this.defaults[key] === null && this.attr[key] === null) {
|
||||
throw new Error('Required attribute "' + key + '" not specified in attachTo for component "' + this.toString() + '".');
|
||||
}
|
||||
}, this);
|
||||
|
||||
this.initialize.call(this, options);
|
||||
}
|
||||
|
||||
Component.attachTo = attachTo;
|
||||
Component.teardownAll = teardownAll;
|
||||
|
||||
// prepend common mixins to supplied list, then mixin all flavors
|
||||
mixins.unshift(withBaseComponent, advice.withAdvice, registry.withRegistration);
|
||||
|
||||
compose.mixin(Component.prototype, mixins);
|
||||
|
||||
return Component;
|
||||
}
|
||||
|
||||
define.teardownAll = function() {
|
||||
registry.components.slice().forEach(function(c) {
|
||||
c.component.teardownAll();
|
||||
});
|
||||
registry.reset();
|
||||
};
|
||||
|
||||
return define;
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
// ==========================================
|
||||
// Copyright 2013 Twitter, Inc
|
||||
// Licensed under The MIT License
|
||||
// http://opensource.org/licenses/MIT
|
||||
// ==========================================
|
||||
|
||||
"use strict";
|
||||
|
||||
define(
|
||||
|
||||
[
|
||||
'./utils',
|
||||
'../tools/debug/debug'
|
||||
],
|
||||
|
||||
function(util, debug) {
|
||||
|
||||
//enumerables are shims - getOwnPropertyDescriptor shim doesn't work
|
||||
var canWriteProtect = debug.enabled && !util.isEnumerable(Object, 'getOwnPropertyDescriptor');
|
||||
//whitelist of unlockable property names
|
||||
var dontLock = ['mixedIn'];
|
||||
|
||||
if (canWriteProtect) {
|
||||
//IE8 getOwnPropertyDescriptor is built-in but throws exeption on non DOM objects
|
||||
try {
|
||||
Object.getOwnPropertyDescriptor(Object, 'keys');
|
||||
} catch(e) {
|
||||
canWriteProtect = false;
|
||||
}
|
||||
}
|
||||
|
||||
function setPropertyWritability(obj, isWritable) {
|
||||
if (!canWriteProtect) {
|
||||
return;
|
||||
}
|
||||
|
||||
var props = Object.create(null);
|
||||
|
||||
Object.keys(obj).forEach(
|
||||
function (key) {
|
||||
if (dontLock.indexOf(key) < 0) {
|
||||
var desc = Object.getOwnPropertyDescriptor(obj, key);
|
||||
desc.writable = isWritable;
|
||||
props[key] = desc;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Object.defineProperties(obj, props);
|
||||
}
|
||||
|
||||
function unlockProperty(obj, prop, op) {
|
||||
var writable;
|
||||
|
||||
if (!canWriteProtect || !obj.hasOwnProperty(prop)) {
|
||||
op.call(obj);
|
||||
return;
|
||||
}
|
||||
|
||||
writable = Object.getOwnPropertyDescriptor(obj, prop).writable;
|
||||
Object.defineProperty(obj, prop, { writable: true });
|
||||
op.call(obj);
|
||||
Object.defineProperty(obj, prop, { writable: writable });
|
||||
}
|
||||
|
||||
function mixin(base, mixins) {
|
||||
base.mixedIn = base.hasOwnProperty('mixedIn') ? base.mixedIn : [];
|
||||
|
||||
mixins.forEach(function(mixin) {
|
||||
if (base.mixedIn.indexOf(mixin) == -1) {
|
||||
setPropertyWritability(base, false);
|
||||
mixin.call(base);
|
||||
base.mixedIn.push(mixin);
|
||||
}
|
||||
});
|
||||
|
||||
setPropertyWritability(base, true);
|
||||
}
|
||||
|
||||
return {
|
||||
mixin: mixin,
|
||||
unlockProperty: unlockProperty
|
||||
};
|
||||
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
// ==========================================
|
||||
// Copyright 2013 Twitter, Inc
|
||||
// Licensed under The MIT License
|
||||
// http://opensource.org/licenses/MIT
|
||||
// ==========================================
|
||||
|
||||
define(
|
||||
|
||||
[
|
||||
'./advice',
|
||||
'./component',
|
||||
'./compose',
|
||||
'./logger',
|
||||
'./registry',
|
||||
'./utils'
|
||||
],
|
||||
|
||||
function (advice, component, compose, logger, registry, utils) {
|
||||
|
||||
return {
|
||||
advice: advice,
|
||||
component: component,
|
||||
compose: compose,
|
||||
logger: logger,
|
||||
registry: registry,
|
||||
utils: utils
|
||||
};
|
||||
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
// ==========================================
|
||||
// Copyright 2013 Twitter, Inc
|
||||
// Licensed under The MIT License
|
||||
// http://opensource.org/licenses/MIT
|
||||
// ==========================================
|
||||
|
||||
"use strict";
|
||||
|
||||
define(
|
||||
|
||||
[
|
||||
'./compose',
|
||||
'./utils'
|
||||
],
|
||||
|
||||
function (compose, util) {
|
||||
|
||||
var actionSymbols = {
|
||||
on:'<-',
|
||||
trigger: '->',
|
||||
off: 'x '
|
||||
};
|
||||
|
||||
function elemToString(elem) {
|
||||
var tagStr = elem.tagName ? elem.tagName.toLowerCase() : elem.toString();
|
||||
var classStr = elem.className ? "." + (elem.className) : "";
|
||||
var result = tagStr + classStr;
|
||||
return elem.tagName ? ['\'', '\''].join(result) : result;
|
||||
}
|
||||
|
||||
function log(action, component, eventArgs) {
|
||||
|
||||
var name, elem, fn, fnName, logFilter, toRegExp, actionLoggable, nameLoggable;
|
||||
|
||||
if (typeof eventArgs[eventArgs.length-1] == 'function') {
|
||||
fn = eventArgs.pop();
|
||||
fn = fn.unbound || fn; //use unbound version if any (better info)
|
||||
}
|
||||
|
||||
if (typeof eventArgs[eventArgs.length - 1] == 'object') {
|
||||
eventArgs.pop(); //trigger data arg - not logged right now
|
||||
}
|
||||
|
||||
if (eventArgs.length == 2) {
|
||||
elem = eventArgs[0];
|
||||
name = eventArgs[1];
|
||||
} else {
|
||||
elem = component.$node[0];
|
||||
name = eventArgs[0];
|
||||
}
|
||||
|
||||
if (window.DEBUG && window.DEBUG.enabled) {
|
||||
logFilter = DEBUG.events.logFilter;
|
||||
|
||||
// no regex for you, actions...
|
||||
actionLoggable = logFilter.actions=="all" || (logFilter.actions.indexOf(action) > -1);
|
||||
// event name filter allow wildcards or regex...
|
||||
toRegExp = function(expr) {
|
||||
return expr.test ? expr : new RegExp("^" + expr.replace(/\*/g, ".*") + "$");
|
||||
};
|
||||
nameLoggable =
|
||||
logFilter.eventNames=="all" ||
|
||||
logFilter.eventNames.some(function(e) {return toRegExp(e).test(name)});
|
||||
|
||||
if (actionLoggable && nameLoggable) {
|
||||
console.info(
|
||||
actionSymbols[action],
|
||||
action,
|
||||
'[' + name + ']',
|
||||
elemToString(elem),
|
||||
component.constructor.toString(),
|
||||
fn && (fnName = fn.name || fn.displayName) && '-> ' + fnName
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function withLogging() {
|
||||
this.before('trigger', function() {
|
||||
log('trigger', this, util.toArray(arguments));
|
||||
});
|
||||
this.before('on', function() {
|
||||
log('on', this, util.toArray(arguments));
|
||||
});
|
||||
this.before('off', function(eventArgs) {
|
||||
log('off', this, util.toArray(arguments));
|
||||
});
|
||||
}
|
||||
|
||||
return withLogging;
|
||||
}
|
||||
);
|
||||
220
resources/flightjs-example-app/components/flight/lib/registry.js
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
// ==========================================
|
||||
// Copyright 2013 Twitter, Inc
|
||||
// Licensed under The MIT License
|
||||
// http://opensource.org/licenses/MIT
|
||||
// ==========================================
|
||||
|
||||
"use strict";
|
||||
|
||||
define(
|
||||
|
||||
[
|
||||
'./utils'
|
||||
],
|
||||
|
||||
function (util) {
|
||||
|
||||
function parseEventArgs(instance, args) {
|
||||
var element, type, callback;
|
||||
var end = args.length;
|
||||
|
||||
if (typeof args[end - 1] === 'function') {
|
||||
end -= 1;
|
||||
callback = args[end];
|
||||
}
|
||||
|
||||
if (typeof args[end - 1] === 'object') {
|
||||
end -= 1;
|
||||
}
|
||||
|
||||
if (end == 2) {
|
||||
element = args[0];
|
||||
type = args[1];
|
||||
} else {
|
||||
element = instance.node;
|
||||
type = args[0];
|
||||
}
|
||||
|
||||
return {
|
||||
element: element,
|
||||
type: type,
|
||||
callback: callback
|
||||
};
|
||||
}
|
||||
|
||||
function matchEvent(a, b) {
|
||||
return (
|
||||
(a.element == b.element) &&
|
||||
(a.type == b.type) &&
|
||||
(b.callback == null || (a.callback == b.callback))
|
||||
);
|
||||
}
|
||||
|
||||
function Registry() {
|
||||
|
||||
var registry = this;
|
||||
|
||||
(this.reset = function() {
|
||||
this.components = [];
|
||||
this.allInstances = {};
|
||||
this.events = [];
|
||||
}).call(this);
|
||||
|
||||
function ComponentInfo(component) {
|
||||
this.component = component;
|
||||
this.attachedTo = [];
|
||||
this.instances = {};
|
||||
|
||||
this.addInstance = function(instance) {
|
||||
var instanceInfo = new InstanceInfo(instance);
|
||||
this.instances[instance.identity] = instanceInfo;
|
||||
this.attachedTo.push(instance.node);
|
||||
|
||||
return instanceInfo;
|
||||
}
|
||||
|
||||
this.removeInstance = function(instance) {
|
||||
delete this.instances[instance.identity];
|
||||
var indexOfNode = this.attachedTo.indexOf(instance.node);
|
||||
(indexOfNode > -1) && this.attachedTo.splice(indexOfNode, 1);
|
||||
|
||||
if (!this.instances.length) {
|
||||
//if I hold no more instances remove me from registry
|
||||
registry.removeComponentInfo(this);
|
||||
}
|
||||
}
|
||||
|
||||
this.isAttachedTo = function(node) {
|
||||
return this.attachedTo.indexOf(node) > -1;
|
||||
}
|
||||
}
|
||||
|
||||
function InstanceInfo(instance) {
|
||||
this.instance = instance;
|
||||
this.events = [];
|
||||
|
||||
this.addBind = function(event) {
|
||||
this.events.push(event);
|
||||
registry.events.push(event);
|
||||
};
|
||||
|
||||
this.removeBind = function(event) {
|
||||
for (var i = 0, e; e = this.events[i]; i++) {
|
||||
if (matchEvent(e, event)) {
|
||||
this.events.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.addInstance = function(instance) {
|
||||
var component = this.findComponentInfo(instance);
|
||||
|
||||
if (!component) {
|
||||
component = new ComponentInfo(instance.constructor);
|
||||
this.components.push(component);
|
||||
}
|
||||
|
||||
var inst = component.addInstance(instance);
|
||||
|
||||
this.allInstances[instance.identity] = inst;
|
||||
|
||||
return component;
|
||||
};
|
||||
|
||||
this.removeInstance = function(instance) {
|
||||
var index, instInfo = this.findInstanceInfo(instance);
|
||||
|
||||
//remove from component info
|
||||
var componentInfo = this.findComponentInfo(instance);
|
||||
componentInfo && componentInfo.removeInstance(instance);
|
||||
|
||||
//remove from registry
|
||||
delete this.allInstances[instance.identity];
|
||||
};
|
||||
|
||||
this.removeComponentInfo = function(componentInfo) {
|
||||
var index = this.components.indexOf(componentInfo);
|
||||
(index > -1) && this.components.splice(index, 1);
|
||||
};
|
||||
|
||||
this.findComponentInfo = function(which) {
|
||||
var component = which.attachTo ? which : which.constructor;
|
||||
|
||||
for (var i = 0, c; c = this.components[i]; i++) {
|
||||
if (c.component === component) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
this.findInstanceInfo = function(instance) {
|
||||
return this.allInstances[instance.identity] || null;
|
||||
};
|
||||
|
||||
this.findInstanceInfoByNode = function(node) {
|
||||
var result = [];
|
||||
Object.keys(this.allInstances).forEach(function(k) {
|
||||
var thisInstanceInfo = this.allInstances[k];
|
||||
if(thisInstanceInfo.instance.node === node) {
|
||||
result.push(thisInstanceInfo);
|
||||
}
|
||||
}, this);
|
||||
return result;
|
||||
};
|
||||
|
||||
this.on = function(componentOn) {
|
||||
var instance = registry.findInstanceInfo(this), boundCallback;
|
||||
|
||||
// unpacking arguments by hand benchmarked faster
|
||||
var l = arguments.length, i = 1;
|
||||
var otherArgs = new Array(l - 1);
|
||||
for (; i < l; i++) otherArgs[i - 1] = arguments[i];
|
||||
|
||||
if (instance) {
|
||||
boundCallback = componentOn.apply(null, otherArgs);
|
||||
if (boundCallback) {
|
||||
otherArgs[otherArgs.length-1] = boundCallback;
|
||||
}
|
||||
var event = parseEventArgs(this, otherArgs);
|
||||
instance.addBind(event);
|
||||
}
|
||||
};
|
||||
|
||||
this.off = function(el, type, callback) {
|
||||
var event = parseEventArgs(this, arguments),
|
||||
instance = registry.findInstanceInfo(this);
|
||||
|
||||
if (instance) {
|
||||
instance.removeBind(event);
|
||||
}
|
||||
};
|
||||
|
||||
//debug tools may want to add advice to trigger
|
||||
if (window.DEBUG && DEBUG.enabled) {
|
||||
registry.trigger = new Function;
|
||||
}
|
||||
|
||||
this.teardown = function() {
|
||||
registry.removeInstance(this);
|
||||
};
|
||||
|
||||
this.withRegistration = function() {
|
||||
this.before('initialize', function() {
|
||||
registry.addInstance(this);
|
||||
});
|
||||
|
||||
this.around('on', registry.on);
|
||||
this.after('off', registry.off);
|
||||
//debug tools may want to add advice to trigger
|
||||
window.DEBUG && DEBUG.enabled && this.after('trigger', registry.trigger);
|
||||
this.after('teardown', {obj:registry, fnName:'teardown'});
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
return new Registry;
|
||||
}
|
||||
);
|
||||
236
resources/flightjs-example-app/components/flight/lib/utils.js
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
// ==========================================
|
||||
// Copyright 2013 Twitter, Inc
|
||||
// Licensed under The MIT License
|
||||
// http://opensource.org/licenses/MIT
|
||||
// ==========================================
|
||||
|
||||
"use strict";
|
||||
|
||||
define(
|
||||
|
||||
[],
|
||||
|
||||
function () {
|
||||
|
||||
var arry = [];
|
||||
var DEFAULT_INTERVAL = 100;
|
||||
|
||||
var utils = {
|
||||
|
||||
isDomObj: function(obj) {
|
||||
return !!(obj.nodeType || (obj === window));
|
||||
},
|
||||
|
||||
toArray: function(obj, from) {
|
||||
return arry.slice.call(obj, from);
|
||||
},
|
||||
|
||||
// returns new object representing multiple objects merged together
|
||||
// optional final argument is boolean which specifies if merge is recursive
|
||||
// original objects are unmodified
|
||||
//
|
||||
// usage:
|
||||
// var base = {a:2, b:6};
|
||||
// var extra = {b:3, c:4};
|
||||
// merge(base, extra); //{a:2, b:3, c:4}
|
||||
// base; //{a:2, b:6}
|
||||
//
|
||||
// var base = {a:2, b:6};
|
||||
// var extra = {b:3, c:4};
|
||||
// var extraExtra = {a:4, d:9};
|
||||
// merge(base, extra, extraExtra); //{a:4, b:3, c:4. d: 9}
|
||||
// base; //{a:2, b:6}
|
||||
//
|
||||
// var base = {a:2, b:{bb:4, cc:5}};
|
||||
// var extra = {a:4, b:{cc:7, dd:1}};
|
||||
// merge(base, extra, true); //{a:4, b:{bb:4, cc:7, dd:1}}
|
||||
// base; //{a:2, b:6}
|
||||
|
||||
merge: function(/*obj1, obj2,....deepCopy*/) {
|
||||
// unpacking arguments by hand benchmarked faster
|
||||
var l = arguments.length,
|
||||
i = 0,
|
||||
args = new Array(l + 1);
|
||||
for (; i < l; i++) args[i + 1] = arguments[i];
|
||||
|
||||
if (l === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
//start with empty object so a copy is created
|
||||
args[0] = {};
|
||||
|
||||
if (args[args.length - 1] === true) {
|
||||
//jquery extend requires deep copy as first arg
|
||||
args.pop();
|
||||
args.unshift(true);
|
||||
}
|
||||
|
||||
return $.extend.apply(undefined, args);
|
||||
},
|
||||
|
||||
// updates base in place by copying properties of extra to it
|
||||
// optionally clobber protected
|
||||
// usage:
|
||||
// var base = {a:2, b:6};
|
||||
// var extra = {c:4};
|
||||
// push(base, extra); //{a:2, b:6, c:4}
|
||||
// base; //{a:2, b:6, c:4}
|
||||
//
|
||||
// var base = {a:2, b:6};
|
||||
// var extra = {b: 4 c:4};
|
||||
// push(base, extra, true); //Error ("utils.push attempted to overwrite 'b' while running in protected mode")
|
||||
// base; //{a:2, b:6}
|
||||
//
|
||||
// objects with the same key will merge recursively when protect is false
|
||||
// eg:
|
||||
// var base = {a:16, b:{bb:4, cc:10}};
|
||||
// var extra = {b:{cc:25, dd:19}, c:5};
|
||||
// push(base, extra); //{a:16, {bb:4, cc:25, dd:19}, c:5}
|
||||
//
|
||||
push: function(base, extra, protect) {
|
||||
if (base) {
|
||||
Object.keys(extra || {}).forEach(function(key) {
|
||||
if (base[key] && protect) {
|
||||
throw Error("utils.push attempted to overwrite '" + key + "' while running in protected mode");
|
||||
}
|
||||
|
||||
if (typeof base[key] == "object" && typeof extra[key] == "object") {
|
||||
//recurse
|
||||
this.push(base[key], extra[key]);
|
||||
} else {
|
||||
//no protect, so extra wins
|
||||
base[key] = extra[key];
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
|
||||
return base;
|
||||
},
|
||||
|
||||
isEnumerable: function(obj, property) {
|
||||
return Object.keys(obj).indexOf(property) > -1;
|
||||
},
|
||||
|
||||
//build a function from other function(s)
|
||||
//util.compose(a,b,c) -> a(b(c()));
|
||||
//implementation lifted from underscore.js (c) 2009-2012 Jeremy Ashkenas
|
||||
compose: function() {
|
||||
var funcs = arguments;
|
||||
|
||||
return function() {
|
||||
var args = arguments;
|
||||
|
||||
for (var i = funcs.length-1; i >= 0; i--) {
|
||||
args = [funcs[i].apply(this, args)];
|
||||
}
|
||||
|
||||
return args[0];
|
||||
};
|
||||
},
|
||||
|
||||
// Can only unique arrays of homogeneous primitives, e.g. an array of only strings, an array of only booleans, or an array of only numerics
|
||||
uniqueArray: function(array) {
|
||||
var u = {}, a = [];
|
||||
|
||||
for (var i = 0, l = array.length; i < l; ++i) {
|
||||
if (u.hasOwnProperty(array[i])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
a.push(array[i]);
|
||||
u[array[i]] = 1;
|
||||
}
|
||||
|
||||
return a;
|
||||
},
|
||||
|
||||
debounce: function(func, wait, immediate) {
|
||||
if (typeof wait != 'number') {
|
||||
wait = DEFAULT_INTERVAL;
|
||||
}
|
||||
|
||||
var timeout, result;
|
||||
|
||||
return function() {
|
||||
var context = this, args = arguments;
|
||||
var later = function() {
|
||||
timeout = null;
|
||||
if (!immediate) {
|
||||
result = func.apply(context, args);
|
||||
}
|
||||
};
|
||||
var callNow = immediate && !timeout;
|
||||
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
|
||||
if (callNow) {
|
||||
result = func.apply(context, args);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
},
|
||||
|
||||
throttle: function(func, wait) {
|
||||
if (typeof wait != 'number') {
|
||||
wait = DEFAULT_INTERVAL;
|
||||
}
|
||||
|
||||
var context, args, timeout, throttling, more, result;
|
||||
var whenDone = this.debounce(function(){
|
||||
more = throttling = false;
|
||||
}, wait);
|
||||
|
||||
return function() {
|
||||
context = this; args = arguments;
|
||||
var later = function() {
|
||||
timeout = null;
|
||||
if (more) {
|
||||
result = func.apply(context, args);
|
||||
}
|
||||
whenDone();
|
||||
};
|
||||
|
||||
if (!timeout) {
|
||||
timeout = setTimeout(later, wait);
|
||||
}
|
||||
|
||||
if (throttling) {
|
||||
more = true;
|
||||
} else {
|
||||
throttling = true;
|
||||
result = func.apply(context, args);
|
||||
}
|
||||
|
||||
whenDone();
|
||||
return result;
|
||||
};
|
||||
},
|
||||
|
||||
countThen: function(num, base) {
|
||||
return function() {
|
||||
if (!--num) { return base.apply(this, arguments); }
|
||||
};
|
||||
},
|
||||
|
||||
delegate: function(rules) {
|
||||
return function(e, data) {
|
||||
var target = $(e.target), parent;
|
||||
|
||||
Object.keys(rules).forEach(function(selector) {
|
||||
if ((parent = target.closest(selector)).length) {
|
||||
data = data || {};
|
||||
data.el = parent[0];
|
||||
return rules[selector].apply(this, [e, data]);
|
||||
}
|
||||
}, this);
|
||||
};
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
return utils;
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
"use strict";
|
||||
|
||||
define(
|
||||
|
||||
[
|
||||
'../../lib/registry',
|
||||
'../../lib/utils'
|
||||
],
|
||||
|
||||
function(registry, utils) {
|
||||
|
||||
var logFilter;
|
||||
|
||||
//******************************************************************************************
|
||||
// Search object model
|
||||
//******************************************************************************************
|
||||
|
||||
function traverse(util, searchTerm, options) {
|
||||
var options = options || {};
|
||||
var obj = options.obj || window;
|
||||
var path = options.path || ((obj==window) ? "window" : "");
|
||||
var props = Object.keys(obj);
|
||||
props.forEach(function(prop) {
|
||||
if ((tests[util] || util)(searchTerm, obj, prop)){
|
||||
console.log([path, ".", prop].join(""), "->",["(", typeof obj[prop], ")"].join(""), obj[prop]);
|
||||
}
|
||||
if(Object.prototype.toString.call(obj[prop])=="[object Object]" && (obj[prop] != obj) && path.split(".").indexOf(prop) == -1) {
|
||||
traverse(util, searchTerm, {obj: obj[prop], path: [path,prop].join(".")});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function search(util, expected, searchTerm, options) {
|
||||
if (!expected || typeof searchTerm == expected) {
|
||||
traverse(util, searchTerm, options);
|
||||
} else {
|
||||
console.error([searchTerm, 'must be', expected].join(' '))
|
||||
}
|
||||
}
|
||||
|
||||
var tests = {
|
||||
'name': function(searchTerm, obj, prop) {return searchTerm == prop},
|
||||
'nameContains': function(searchTerm, obj, prop) {return prop.indexOf(searchTerm)>-1},
|
||||
'type': function(searchTerm, obj, prop) {return obj[prop] instanceof searchTerm},
|
||||
'value': function(searchTerm, obj, prop) {return obj[prop] === searchTerm},
|
||||
'valueCoerced': function(searchTerm, obj, prop) {return obj[prop] == searchTerm}
|
||||
}
|
||||
|
||||
function byName(searchTerm, options) {search('name', 'string', searchTerm, options);};
|
||||
function byNameContains(searchTerm, options) {search('nameContains', 'string', searchTerm, options);};
|
||||
function byType(searchTerm, options) {search('type', 'function', searchTerm, options);};
|
||||
function byValue(searchTerm, options) {search('value', null, searchTerm, options);};
|
||||
function byValueCoerced(searchTerm, options) {search('valueCoerced', null, searchTerm, options);};
|
||||
function custom(fn, options) {traverse(fn, null, options);};
|
||||
|
||||
//******************************************************************************************
|
||||
// Event logging
|
||||
//******************************************************************************************
|
||||
|
||||
var ALL = 'all'; //no filter
|
||||
|
||||
//no logging by default
|
||||
var defaultEventNamesFilter = [];
|
||||
var defaultActionsFilter = [];
|
||||
|
||||
var logFilter = retrieveLogFilter();
|
||||
|
||||
function filterEventLogsByAction(/*actions*/) {
|
||||
var actions = [].slice.call(arguments);
|
||||
|
||||
logFilter.eventNames.length || (logFilter.eventNames = ALL);
|
||||
logFilter.actions = actions.length ? actions : ALL;
|
||||
saveLogFilter();
|
||||
}
|
||||
|
||||
function filterEventLogsByName(/*eventNames*/) {
|
||||
var eventNames = [].slice.call(arguments);
|
||||
|
||||
logFilter.actions.length || (logFilter.actions = ALL);
|
||||
logFilter.eventNames = eventNames.length ? eventNames : ALL;
|
||||
saveLogFilter();
|
||||
}
|
||||
|
||||
function hideAllEventLogs() {
|
||||
logFilter.actions = [];
|
||||
logFilter.eventNames = [];
|
||||
saveLogFilter();
|
||||
}
|
||||
|
||||
function showAllEventLogs() {
|
||||
logFilter.actions = ALL;
|
||||
logFilter.eventNames = ALL;
|
||||
saveLogFilter();
|
||||
}
|
||||
|
||||
function saveLogFilter() {
|
||||
if (window.localStorage) {
|
||||
localStorage.setItem('logFilter_eventNames', logFilter.eventNames);
|
||||
localStorage.setItem('logFilter_actions', logFilter.actions);
|
||||
}
|
||||
}
|
||||
|
||||
function retrieveLogFilter() {
|
||||
var result = {
|
||||
eventNames: (window.localStorage && localStorage.getItem('logFilter_eventNames')) || defaultEventNamesFilter,
|
||||
actions: (window.localStorage && localStorage.getItem('logFilter_actions')) || defaultActionsFilter
|
||||
};
|
||||
//reconstitute arrays
|
||||
Object.keys(result).forEach(function(k) {
|
||||
var thisProp = result[k];
|
||||
if (typeof thisProp == 'string' && thisProp !== ALL) {
|
||||
result[k] = thisProp.split(',');
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
enable: function(enable) {
|
||||
this.enabled = !!enable;
|
||||
|
||||
if (enable && window.console) {
|
||||
console.info('Booting in DEBUG mode');
|
||||
console.info('You can configure event logging with DEBUG.events.logAll()/logNone()/logByName()/logByAction()');
|
||||
}
|
||||
|
||||
window.DEBUG = this;
|
||||
},
|
||||
|
||||
find: {
|
||||
byName: byName,
|
||||
byNameContains: byNameContains,
|
||||
byType: byType,
|
||||
byValue: byValue,
|
||||
byValueCoerced: byValueCoerced,
|
||||
custom: custom
|
||||
},
|
||||
|
||||
events: {
|
||||
logFilter: logFilter,
|
||||
|
||||
// Accepts any number of action args
|
||||
// e.g. DEBUG.events.logByAction("on", "off")
|
||||
logByAction: filterEventLogsByAction,
|
||||
|
||||
// Accepts any number of event name args (inc. regex or wildcards)
|
||||
// e.g. DEBUG.events.logByName(/ui.*/, "*Thread*");
|
||||
logByName: filterEventLogsByName,
|
||||
|
||||
logAll: showAllEventLogs,
|
||||
logNone: hideAllEventLogs
|
||||
}
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2013 Twitter, Inc and others
|
||||
|
||||
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.
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
# jasmine-flight [](http://travis-ci.org/flightjs/jasmine-flight)
|
||||
|
||||
Extensions to the Jasmine test framework for use with [Flight](https://github.com/flightjs/flight)
|
||||
|
||||
# Getting started
|
||||
|
||||
Include [jasmine-flight.js](https://raw.github.com/flightjs/jasmine-flight/master/lib/jasmine-flight.js)
|
||||
in your app and load it in your test runner.
|
||||
|
||||
Or install it with [Bower](http://bower.io/):
|
||||
|
||||
```bash
|
||||
bower install --save-dev jasmine-flight
|
||||
```
|
||||
|
||||
**N.B.** jasmine-flight depends on
|
||||
[jasmine](https://github.com/pivotal/jasmine) and
|
||||
[jasmine-jquery](https://github.com/velesin/jasmine-jquery)
|
||||
|
||||
## Components
|
||||
|
||||
```javascript
|
||||
describeComponent('path/to/component', function () {
|
||||
beforeEach(function () {
|
||||
setupComponent();
|
||||
});
|
||||
|
||||
it('should do x', function () {
|
||||
// a component instance is now accessible as this.component
|
||||
// the component root node is attached to the DOM
|
||||
// the component root node is also available as this.$node
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Mixins
|
||||
|
||||
```javascript
|
||||
describeMixin('path/to/mixin', function () {
|
||||
// initialize the component and attach it to the DOM
|
||||
beforeEach(function () {
|
||||
setupComponent();
|
||||
});
|
||||
|
||||
it('should do x', function () {
|
||||
expect(this.component.doSomething()).toBe(expected);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Event spy
|
||||
|
||||
```javascript
|
||||
describeComponent('data/twitter_profile', function () {
|
||||
beforeEach(function () {
|
||||
setupComponent();
|
||||
});
|
||||
|
||||
describe('listens for uiNeedsTwitterUserId', function () {
|
||||
// was the event triggered?
|
||||
it('and triggers dataTwitterUserId', function () {
|
||||
var eventSpy = spyOnEvent(document, 'dataTwitterProfile');
|
||||
$(document).trigger('uiNeedsTwitterUserId', {
|
||||
screen_name: 'tbrd'
|
||||
});
|
||||
expect(eventSpy).toHaveBeenTriggeredOn(document);
|
||||
});
|
||||
|
||||
// is the user id correct?
|
||||
it('and has correct id', function () {
|
||||
var eventSpy = spyOnEvent(document, 'dataTwitterUserId');
|
||||
$(document).trigger('uiNeedsTwitteruserId', {
|
||||
screen_name: 'tbrd'
|
||||
});
|
||||
expect(eventSpy.mostRecentCall.data).toEqual({
|
||||
screen_name: 'tbrd',
|
||||
id: 4149861
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## setupComponent
|
||||
|
||||
```javascript
|
||||
setupComponent(optionalFixture, optionalOptions);
|
||||
```
|
||||
|
||||
Calling `setupComponent` twice will create an instance, tear it down and create a new one.
|
||||
|
||||
### HTML Fixtures
|
||||
|
||||
```javascript
|
||||
describeComponent('ui/twitter_profile', function () {
|
||||
// is the component attached to the fixture?
|
||||
it('this.component.$node has class "foo"', function () {
|
||||
setupComponent('<span class="foo">Test</span>');
|
||||
expect(this.component.$node).toHaveClass('foo');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Component Options
|
||||
|
||||
```javascript
|
||||
describeComponent('data/twitter_profile', function () {
|
||||
// is the option set correctly?
|
||||
it('this.component.attr.baseUrl is set', function () {
|
||||
setupComponent({
|
||||
baseUrl: 'http://twitter.com/1.1/'
|
||||
});
|
||||
expect(this.component.attr.baseUrl).toBe('http://twitter.com/1.1/');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
# Teardown
|
||||
|
||||
Components are automatically torn down after each test.
|
||||
|
||||
## Contributing to this project
|
||||
|
||||
Anyone and everyone is welcome to contribute. Please take a moment to
|
||||
review the [guidelines for contributing](CONTRIBUTING.md).
|
||||
|
||||
* [Bug reports](CONTRIBUTING.md#bugs)
|
||||
* [Feature requests](CONTRIBUTING.md#features)
|
||||
* [Pull requests](CONTRIBUTING.md#pull-requests)
|
||||
|
||||
## Authors
|
||||
|
||||
* [@tbrd](http://github.com/tbrd)
|
||||
|
||||
## Thanks
|
||||
|
||||
* [@esbie](http://github.com/esbie) and
|
||||
[@skilldrick](http://github.com/skilldrick) for creating the original
|
||||
`describeComponent` & `describeMixin` methods.
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2013 Twitter, Inc and other contributors.
|
||||
|
||||
Licensed under the MIT License
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "jasmine-flight",
|
||||
"version": "1.1.0",
|
||||
"description": "Extensions to the Jasmine test framework for use with Flight",
|
||||
"main": "lib/jasmine-flight.js",
|
||||
"keywords": [
|
||||
"flight",
|
||||
"jasmine",
|
||||
"test"
|
||||
],
|
||||
"ignore": [
|
||||
".*",
|
||||
"test",
|
||||
"package.json",
|
||||
"karma.conf.js",
|
||||
"CONTRIBUTING.md",
|
||||
"CHANGELOG.md"
|
||||
],
|
||||
"devDependencies": {
|
||||
"flight": "~1.0.9"
|
||||
},
|
||||
"gitHead": "5174302b9a87ebdda99de6a436ec2a64200fc5ff",
|
||||
"readme": "# jasmine-flight [](http://travis-ci.org/flightjs/jasmine-flight)\n\nExtensions to the Jasmine test framework for use with [Flight](https://github.com/flightjs/flight)\n\n# Getting started\n\nInclude [jasmine-flight.js](https://raw.github.com/flightjs/jasmine-flight/master/lib/jasmine-flight.js)\nin your app and load it in your test runner.\n\nOr install it with [Bower](http://bower.io/):\n\n```bash\nbower install --save-dev jasmine-flight\n```\n\n**N.B.** jasmine-flight depends on\n[jasmine](https://github.com/pivotal/jasmine) and\n[jasmine-jquery](https://github.com/velesin/jasmine-jquery)\n\n## Components\n\n```javascript\ndescribeComponent('path/to/component', function () {\n beforeEach(function () {\n setupComponent();\n });\n\n it('should do x', function () {\n // a component instance is now accessible as this.component\n // the component root node is attached to the DOM\n // the component root node is also available as this.$node\n });\n});\n```\n\n## Mixins\n\n```javascript\ndescribeMixin('path/to/mixin', function () {\n // initialize the component and attach it to the DOM\n beforeEach(function () {\n setupComponent();\n });\n\n it('should do x', function () {\n expect(this.component.doSomething()).toBe(expected);\n });\n});\n```\n\n## Event spy\n\n```javascript\ndescribeComponent('data/twitter_profile', function () {\n beforeEach(function () {\n setupComponent();\n });\n\n describe('listens for uiNeedsTwitterUserId', function () {\n // was the event triggered?\n it('and triggers dataTwitterUserId', function () {\n var eventSpy = spyOnEvent(document, 'dataTwitterProfile');\n $(document).trigger('uiNeedsTwitterUserId', {\n screen_name: 'tbrd'\n });\n expect(eventSpy).toHaveBeenTriggeredOn(document);\n });\n\n // is the user id correct?\n it('and has correct id', function () {\n var eventSpy = spyOnEvent(document, 'dataTwitterUserId');\n $(document).trigger('uiNeedsTwitteruserId', {\n screen_name: 'tbrd'\n });\n expect(eventSpy.mostRecentCall.data).toEqual({\n screen_name: 'tbrd',\n id: 4149861\n });\n });\n });\n});\n```\n\n## setupComponent\n\n```javascript\nsetupComponent(optionalFixture, optionalOptions);\n```\n\nCalling `setupComponent` twice will create an instance, tear it down and create a new one.\n\n### HTML Fixtures\n\n```javascript\ndescribeComponent('ui/twitter_profile', function () {\n // is the component attached to the fixture?\n it('this.component.$node has class \"foo\"', function () {\n setupComponent('<span class=\"foo\">Test</span>');\n expect(this.component.$node).toHaveClass('foo');\n });\n});\n```\n\n### Component Options\n\n```javascript\ndescribeComponent('data/twitter_profile', function () {\n // is the option set correctly?\n it('this.component.attr.baseUrl is set', function () {\n setupComponent({\n baseUrl: 'http://twitter.com/1.1/'\n });\n expect(this.component.attr.baseUrl).toBe('http://twitter.com/1.1/');\n });\n});\n```\n\n# Teardown\n\nComponents are automatically torn down after each test.\n\n## Contributing to this project\n\nAnyone and everyone is welcome to contribute. Please take a moment to\nreview the [guidelines for contributing](CONTRIBUTING.md).\n\n* [Bug reports](CONTRIBUTING.md#bugs)\n* [Feature requests](CONTRIBUTING.md#features)\n* [Pull requests](CONTRIBUTING.md#pull-requests)\n\n## Authors\n\n* [@tbrd](http://github.com/tbrd)\n\n## Thanks\n\n* [@esbie](http://github.com/esbie) and\n [@skilldrick](http://github.com/skilldrick) for creating the original\n `describeComponent` & `describeMixin` methods.\n\n## License\n\nCopyright 2013 Twitter, Inc and other contributors.\n\nLicensed under the MIT License\n",
|
||||
"readmeFilename": "README.md",
|
||||
"_id": "jasmine-flight@1.1.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/flightjs/jasmine-flight.git"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,444 @@
|
|||
/**
|
||||
* Copyright 2013, Twitter Inc. and other contributors
|
||||
* Licensed under the MIT License
|
||||
*/
|
||||
|
||||
(function (root) {
|
||||
'use strict';
|
||||
|
||||
jasmine.flight = {};
|
||||
|
||||
/**
|
||||
* Wrapper for describe. Load component before each test.
|
||||
*
|
||||
* @param componentPath
|
||||
* @param specDefinitions
|
||||
*/
|
||||
|
||||
root.describeComponent = function (componentPath, specDefinitions) {
|
||||
jasmine.getEnv().describeComponent(componentPath, specDefinitions);
|
||||
};
|
||||
|
||||
jasmine.Env.prototype.describeComponent = function (componentPath, specDefinitions) {
|
||||
describe(componentPath, function () {
|
||||
beforeEach(function () {
|
||||
this.Component = this.component = this.$node = null;
|
||||
|
||||
var requireCallback = function (registry, Component) {
|
||||
registry.reset();
|
||||
this.Component = Component;
|
||||
}.bind(this);
|
||||
|
||||
require(['flight/lib/registry', componentPath], requireCallback);
|
||||
|
||||
waitsFor(function () {
|
||||
return this.Component !== null;
|
||||
}.bind(this));
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
if (this.$node) {
|
||||
this.$node.remove();
|
||||
this.$node = null;
|
||||
}
|
||||
|
||||
var requireCallback = function (defineComponent) {
|
||||
if (this.component) {
|
||||
this.component = null;
|
||||
}
|
||||
|
||||
this.Component = null;
|
||||
defineComponent.teardownAll();
|
||||
}.bind(this);
|
||||
|
||||
require(['flight/lib/component'], requireCallback);
|
||||
|
||||
waitsFor(function () {
|
||||
return this.Component === null;
|
||||
}.bind(this));
|
||||
});
|
||||
|
||||
specDefinitions.apply(this);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper for describe. Load mixin before each test.
|
||||
*
|
||||
* @param mixinPath
|
||||
* @param specDefinitions
|
||||
*/
|
||||
|
||||
root.describeMixin = function (mixinPath, specDefinitions) {
|
||||
jasmine.getEnv().describeMixin(mixinPath, specDefinitions);
|
||||
};
|
||||
|
||||
jasmine.Env.prototype.describeMixin = function (mixinPath, specDefinitions) {
|
||||
describe(mixinPath, function () {
|
||||
beforeEach(function () {
|
||||
this.Component = this.component = this.$node = null;
|
||||
|
||||
var requireCallback = function (registry, defineComponent, Mixin) {
|
||||
registry.reset();
|
||||
this.Component = defineComponent(function () {}, Mixin);
|
||||
}.bind(this);
|
||||
|
||||
require(['flight/lib/registry', 'flight/lib/component', mixinPath], requireCallback);
|
||||
|
||||
waitsFor(function () {
|
||||
return this.Component !== null;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
if (this.$node) {
|
||||
this.$node.remove();
|
||||
this.$node = null;
|
||||
}
|
||||
|
||||
var requireCallback = function (defineComponent) {
|
||||
if (this.component) {
|
||||
this.component = null;
|
||||
}
|
||||
|
||||
this.Component = null;
|
||||
defineComponent.teardownAll();
|
||||
}.bind(this);
|
||||
|
||||
require(['flight/lib/component'], requireCallback);
|
||||
|
||||
waitsFor(function () {
|
||||
return this.Component === null;
|
||||
}.bind(this));
|
||||
});
|
||||
|
||||
specDefinitions.apply(this);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper for describe. Load module before each test.
|
||||
*
|
||||
* @param modulePath
|
||||
* @param specDefinitions
|
||||
*/
|
||||
|
||||
root.describeModule = function (modulePath, specDefinitions) {
|
||||
return jasmine.getEnv().describeModule(modulePath, specDefinitions);
|
||||
};
|
||||
|
||||
jasmine.Env.prototype.describeModule = function (modulePath, specDefinitions) {
|
||||
describe(modulePath, function () {
|
||||
beforeEach(function () {
|
||||
this.module = null;
|
||||
|
||||
var requireCallback = function (module) {
|
||||
this.module = module;
|
||||
}.bind(this);
|
||||
|
||||
require([modulePath], requireCallback);
|
||||
|
||||
waitsFor(function () {
|
||||
return this.module !== null;
|
||||
});
|
||||
});
|
||||
|
||||
specDefinitions.apply(this);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create root node and initialize component. Fixture should be html string
|
||||
* or jQuery object.
|
||||
*
|
||||
* @param fixture {String} (Optional)
|
||||
* @param options {Options} (Optional)
|
||||
*/
|
||||
|
||||
root.setupComponent = function (fixture, options) {
|
||||
jasmine.getEnv().currentSpec.setupComponent(fixture, options);
|
||||
};
|
||||
|
||||
jasmine.Spec.prototype.setupComponent = function (fixture, options) {
|
||||
if (this.component) {
|
||||
this.component.teardown();
|
||||
this.$node.remove();
|
||||
}
|
||||
|
||||
this.$node = $('<div class="component-root" />');
|
||||
$('body').append(this.$node);
|
||||
|
||||
if (fixture instanceof jQuery || typeof fixture === 'string') {
|
||||
this.$node.append(fixture);
|
||||
} else {
|
||||
options = fixture;
|
||||
fixture = null;
|
||||
}
|
||||
|
||||
options = options === undefined ? {} : options;
|
||||
|
||||
this.component = new this.Component(this.$node, options);
|
||||
};
|
||||
|
||||
|
||||
(function (namespace) {
|
||||
var eventsData = {
|
||||
spiedEvents: {},
|
||||
handlers: []
|
||||
};
|
||||
|
||||
namespace.formatElement = function ($element) {
|
||||
var limit = 200;
|
||||
var output = '';
|
||||
|
||||
if ($element instanceof jQuery) {
|
||||
output = jasmine.JQuery.elementToString($element);
|
||||
if (output.length > limit) {
|
||||
output = output.slice(0, 200) + '...';
|
||||
}
|
||||
} else {
|
||||
//$element should always be a jQuery object
|
||||
output = 'element is not a jQuery object';
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
namespace.compareColors = function (color1, color2) {
|
||||
if (color1.charAt(0) === color2.charAt(0)) {
|
||||
return color1 === color2;
|
||||
} else {
|
||||
return namespace.hex2rgb(color1) === namespace.hex2rgb(color2);
|
||||
}
|
||||
};
|
||||
|
||||
namespace.hex2rgb = function (colorString) {
|
||||
if (colorString.charAt(0) !== '#') return colorString;
|
||||
// note: hexStr should be #rrggbb
|
||||
var hex = parseInt(colorString.substring(1), 16);
|
||||
var r = (hex & 0xff0000) >> 16;
|
||||
var g = (hex & 0x00ff00) >> 8;
|
||||
var b = hex & 0x0000ff;
|
||||
return 'rgb(' + r + ', ' + g + ', ' + b + ')';
|
||||
};
|
||||
|
||||
namespace.events = {
|
||||
spyOn: function (selector, eventName) {
|
||||
eventsData.spiedEvents[[selector, eventName]] = {
|
||||
callCount: 0,
|
||||
calls: [],
|
||||
mostRecentCall: {},
|
||||
name: eventName
|
||||
};
|
||||
|
||||
var handler = function (e, data) {
|
||||
var call = {
|
||||
event: e,
|
||||
args: jasmine.util.argsToArray(arguments),
|
||||
data: data
|
||||
};
|
||||
eventsData.spiedEvents[[selector, eventName]].callCount++;
|
||||
eventsData.spiedEvents[[selector, eventName]].calls.push(call);
|
||||
eventsData.spiedEvents[[selector, eventName]].mostRecentCall = call;
|
||||
};
|
||||
|
||||
jQuery(selector).on(eventName, handler);
|
||||
eventsData.handlers.push(handler);
|
||||
return eventsData.spiedEvents[[selector, eventName]];
|
||||
},
|
||||
|
||||
eventArgs: function (selector, eventName, expectedArg) {
|
||||
var actualArgs = eventsData.spiedEvents[[selector, eventName]].mostRecentCall.args;
|
||||
|
||||
if (!actualArgs) {
|
||||
throw 'No event spy found on ' + eventName + '. Try adding a call to spyOnEvent or make sure that the selector the event is triggered on and the selector being spied on are correct.';
|
||||
}
|
||||
|
||||
// remove extra event metadata if it is not tested for
|
||||
if ((actualArgs.length === 2) && typeof actualArgs[1] === 'object' &&
|
||||
expectedArg && !expectedArg.scribeContext && !expectedArg.sourceEventData && !expectedArg.scribeData) {
|
||||
actualArgs[1] = $.extend({}, actualArgs[1]);
|
||||
delete actualArgs[1].sourceEventData;
|
||||
delete actualArgs[1].scribeContext;
|
||||
delete actualArgs[1].scribeData;
|
||||
}
|
||||
|
||||
return actualArgs;
|
||||
},
|
||||
|
||||
wasTriggered: function (selector, event) {
|
||||
var spiedEvent = eventsData.spiedEvents[[selector, event]];
|
||||
return spiedEvent && spiedEvent.callCount > 0;
|
||||
},
|
||||
|
||||
wasTriggeredWith: function (selector, eventName, expectedArg, env) {
|
||||
var actualArgs = jasmine.flight.events.eventArgs(selector, eventName, expectedArg);
|
||||
return actualArgs && env.contains_(actualArgs, expectedArg);
|
||||
},
|
||||
|
||||
wasTriggeredWithData: function (selector, eventName, expectedArg, env) {
|
||||
var actualArgs = jasmine.flight.events.eventArgs(selector, eventName, expectedArg);
|
||||
var valid;
|
||||
|
||||
if (actualArgs) {
|
||||
valid = false;
|
||||
for (var i = 0; i < actualArgs.length; i++) {
|
||||
if (jasmine.flight.validateHash(expectedArg, actualArgs[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
cleanUp: function () {
|
||||
eventsData.spiedEvents = {};
|
||||
eventsData.handlers = [];
|
||||
}
|
||||
};
|
||||
|
||||
namespace.validateHash = function (a, b, intersection) {
|
||||
var validHash;
|
||||
for (var field in a) {
|
||||
if ((typeof a[field] === 'object') && (typeof b[field] === 'object')) {
|
||||
validHash = jasmine.flight.validateHash(a[field], b[field]);
|
||||
} else if (intersection && (typeof a[field] === 'undefined' || typeof b[field] === 'undefined')) {
|
||||
validHash = true;
|
||||
} else {
|
||||
validHash = (a[field] === b[field]);
|
||||
}
|
||||
if (!validHash) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return validHash;
|
||||
};
|
||||
})(jasmine.flight);
|
||||
|
||||
beforeEach(function () {
|
||||
this.addMatchers({
|
||||
toHaveBeenTriggeredOn: function () {
|
||||
var selector = arguments[0];
|
||||
var eventName = typeof this.actual === 'string' ? this.actual : this.actual.name;
|
||||
var wasTriggered = jasmine.flight.events.wasTriggered(selector, eventName);
|
||||
|
||||
this.message = function () {
|
||||
var $pp = function (obj) {
|
||||
var description;
|
||||
var attr;
|
||||
|
||||
if (!(obj instanceof jQuery)) {
|
||||
obj = $(obj);
|
||||
}
|
||||
|
||||
description = [
|
||||
obj.get(0).nodeName
|
||||
];
|
||||
|
||||
attr = obj.get(0).attributes || [];
|
||||
|
||||
for (var x = 0; x < attr.length; x++) {
|
||||
description.push(attr[x].name + '="' + attr[x].value + '"');
|
||||
}
|
||||
|
||||
return '<' + description.join(' ') + '>';
|
||||
};
|
||||
|
||||
if (wasTriggered) {
|
||||
return [
|
||||
'<div class="value-mismatch">Expected event ' + eventName + ' to have been triggered on' + selector,
|
||||
'<div class="value-mismatch">Expected event ' + eventName + ' not to have been triggered on' + selector
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
'Expected event ' + eventName + ' to have been triggered on ' + $pp(selector),
|
||||
'Expected event ' + eventName + ' not to have been triggered on ' + $pp(selector)
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
return wasTriggered;
|
||||
},
|
||||
|
||||
toHaveBeenTriggeredOnAndWith: function () {
|
||||
var selector = arguments[0];
|
||||
var expectedArg = arguments[1];
|
||||
var exactMatch = !arguments[2];
|
||||
var wasTriggered = jasmine.flight.events.wasTriggered(selector, this.actual);
|
||||
|
||||
this.message = function () {
|
||||
var $pp = function (obj) {
|
||||
var description;
|
||||
var attr;
|
||||
|
||||
if (!(obj instanceof jQuery)) {
|
||||
obj = $(obj);
|
||||
}
|
||||
|
||||
description = [
|
||||
obj.get(0).nodeName
|
||||
];
|
||||
|
||||
attr = obj.get(0).attributes || [];
|
||||
|
||||
for (var x = 0; x < attr.length; x++) {
|
||||
description.push(attr[x].name + '="' + attr[x].value + '"');
|
||||
}
|
||||
|
||||
return '<' + description.join(' ') + '>';
|
||||
};
|
||||
|
||||
if (wasTriggered) {
|
||||
var actualArg = jasmine.flight.events.eventArgs(selector, this.actual, expectedArg)[1];
|
||||
return [
|
||||
'<div class="value-mismatch">Expected event ' + this.actual.name + ' to have been triggered on' + selector,
|
||||
'<div class="value-mismatch">Expected event ' + this.actual.name + ' not to have been triggered on' + selector
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
'Expected event ' + this.actual.name + ' to have been triggered on ' + $pp(selector),
|
||||
'Expected event ' + this.actual.name + ' not to have been triggered on ' + $pp(selector)
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
if (!wasTriggered) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (exactMatch) {
|
||||
return jasmine.flight.events.wasTriggeredWith(selector, this.actual, expectedArg, this.env);
|
||||
} else {
|
||||
return jasmine.flight.events.wasTriggeredWithData(selector, this.actual, expectedArg, this.env);
|
||||
}
|
||||
},
|
||||
|
||||
toHaveCss: function (prop, val) {
|
||||
var result;
|
||||
if (val instanceof RegExp) {
|
||||
result = val.test(this.actual.css(prop));
|
||||
} else if (prop.match(/color/)) {
|
||||
//IE returns colors as hex strings; other browsers return rgb(r, g, b) strings
|
||||
result = jasmine.flight.compareColors(this.actual.css(prop), val);
|
||||
} else {
|
||||
result = this.actual.css(prop) === val;
|
||||
//sometimes .css() returns strings when it should return numbers
|
||||
if (!result && typeof val === 'number') {
|
||||
result = parseFloat(this.actual.css(prop), 10) === val;
|
||||
}
|
||||
}
|
||||
|
||||
this.actual = jasmine.flight.formatElement(this.actual);
|
||||
return result;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
root.spyOnEvent = function (selector, eventName) {
|
||||
jasmine.JQuery.events.spyOn(selector, eventName);
|
||||
return jasmine.flight.events.spyOn(selector, eventName);
|
||||
};
|
||||
|
||||
}(this));
|
||||
|
|
@ -0,0 +1,659 @@
|
|||
/*!
|
||||
Jasmine-jQuery: a set of jQuery helpers for Jasmine tests.
|
||||
|
||||
Version 1.5.2
|
||||
|
||||
https://github.com/velesin/jasmine-jquery
|
||||
|
||||
Copyright (c) 2010-2013 Wojciech Zawistowski, Travis Jeffery
|
||||
|
||||
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.
|
||||
*/
|
||||
var readFixtures = function() {
|
||||
return jasmine.getFixtures().proxyCallTo_('read', arguments)
|
||||
}
|
||||
|
||||
var preloadFixtures = function() {
|
||||
jasmine.getFixtures().proxyCallTo_('preload', arguments)
|
||||
}
|
||||
|
||||
var loadFixtures = function() {
|
||||
jasmine.getFixtures().proxyCallTo_('load', arguments)
|
||||
}
|
||||
|
||||
var appendLoadFixtures = function() {
|
||||
jasmine.getFixtures().proxyCallTo_('appendLoad', arguments)
|
||||
}
|
||||
|
||||
var setFixtures = function(html) {
|
||||
jasmine.getFixtures().proxyCallTo_('set', arguments)
|
||||
}
|
||||
|
||||
var appendSetFixtures = function() {
|
||||
jasmine.getFixtures().proxyCallTo_('appendSet', arguments)
|
||||
}
|
||||
|
||||
var sandbox = function(attributes) {
|
||||
return jasmine.getFixtures().sandbox(attributes)
|
||||
}
|
||||
|
||||
var spyOnEvent = function(selector, eventName) {
|
||||
return jasmine.JQuery.events.spyOn(selector, eventName)
|
||||
}
|
||||
|
||||
var preloadStyleFixtures = function() {
|
||||
jasmine.getStyleFixtures().proxyCallTo_('preload', arguments)
|
||||
}
|
||||
|
||||
var loadStyleFixtures = function() {
|
||||
jasmine.getStyleFixtures().proxyCallTo_('load', arguments)
|
||||
}
|
||||
|
||||
var appendLoadStyleFixtures = function() {
|
||||
jasmine.getStyleFixtures().proxyCallTo_('appendLoad', arguments)
|
||||
}
|
||||
|
||||
var setStyleFixtures = function(html) {
|
||||
jasmine.getStyleFixtures().proxyCallTo_('set', arguments)
|
||||
}
|
||||
|
||||
var appendSetStyleFixtures = function(html) {
|
||||
jasmine.getStyleFixtures().proxyCallTo_('appendSet', arguments)
|
||||
}
|
||||
|
||||
var loadJSONFixtures = function() {
|
||||
return jasmine.getJSONFixtures().proxyCallTo_('load', arguments)
|
||||
}
|
||||
|
||||
var getJSONFixture = function(url) {
|
||||
return jasmine.getJSONFixtures().proxyCallTo_('read', arguments)[url]
|
||||
}
|
||||
|
||||
jasmine.spiedEventsKey = function (selector, eventName) {
|
||||
return [$(selector).selector, eventName].toString()
|
||||
}
|
||||
|
||||
jasmine.getFixtures = function() {
|
||||
return jasmine.currentFixtures_ = jasmine.currentFixtures_ || new jasmine.Fixtures()
|
||||
}
|
||||
|
||||
jasmine.getStyleFixtures = function() {
|
||||
return jasmine.currentStyleFixtures_ = jasmine.currentStyleFixtures_ || new jasmine.StyleFixtures()
|
||||
}
|
||||
|
||||
jasmine.Fixtures = function() {
|
||||
this.containerId = 'jasmine-fixtures'
|
||||
this.fixturesCache_ = {}
|
||||
this.fixturesPath = 'spec/javascripts/fixtures'
|
||||
}
|
||||
|
||||
jasmine.Fixtures.prototype.set = function(html) {
|
||||
this.cleanUp()
|
||||
this.createContainer_(html)
|
||||
}
|
||||
|
||||
jasmine.Fixtures.prototype.appendSet= function(html) {
|
||||
this.addToContainer_(html)
|
||||
}
|
||||
|
||||
jasmine.Fixtures.prototype.preload = function() {
|
||||
this.read.apply(this, arguments)
|
||||
}
|
||||
|
||||
jasmine.Fixtures.prototype.load = function() {
|
||||
this.cleanUp()
|
||||
this.createContainer_(this.read.apply(this, arguments))
|
||||
}
|
||||
|
||||
jasmine.Fixtures.prototype.appendLoad = function() {
|
||||
this.addToContainer_(this.read.apply(this, arguments))
|
||||
}
|
||||
|
||||
jasmine.Fixtures.prototype.read = function() {
|
||||
var htmlChunks = []
|
||||
|
||||
var fixtureUrls = arguments
|
||||
for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) {
|
||||
htmlChunks.push(this.getFixtureHtml_(fixtureUrls[urlIndex]))
|
||||
}
|
||||
|
||||
return htmlChunks.join('')
|
||||
}
|
||||
|
||||
jasmine.Fixtures.prototype.clearCache = function() {
|
||||
this.fixturesCache_ = {}
|
||||
}
|
||||
|
||||
jasmine.Fixtures.prototype.cleanUp = function() {
|
||||
$('#' + this.containerId).remove()
|
||||
}
|
||||
|
||||
jasmine.Fixtures.prototype.sandbox = function(attributes) {
|
||||
var attributesToSet = attributes || {}
|
||||
return $('<div id="sandbox" />').attr(attributesToSet)
|
||||
}
|
||||
|
||||
jasmine.Fixtures.prototype.createContainer_ = function(html) {
|
||||
var container
|
||||
if(html instanceof $) {
|
||||
container = $('<div id="' + this.containerId + '" />')
|
||||
container.html(html)
|
||||
} else {
|
||||
container = '<div id="' + this.containerId + '">' + html + '</div>'
|
||||
}
|
||||
$(document.body).append(container)
|
||||
}
|
||||
|
||||
jasmine.Fixtures.prototype.addToContainer_ = function(html){
|
||||
var container = $(document.body).find('#'+this.containerId).append(html)
|
||||
if(!container.length){
|
||||
this.createContainer_(html)
|
||||
}
|
||||
}
|
||||
|
||||
jasmine.Fixtures.prototype.getFixtureHtml_ = function(url) {
|
||||
if (typeof this.fixturesCache_[url] === 'undefined') {
|
||||
this.loadFixtureIntoCache_(url)
|
||||
}
|
||||
return this.fixturesCache_[url]
|
||||
}
|
||||
|
||||
jasmine.Fixtures.prototype.loadFixtureIntoCache_ = function(relativeUrl) {
|
||||
var url = this.makeFixtureUrl_(relativeUrl)
|
||||
var request = $.ajax({
|
||||
type: "GET",
|
||||
url: url + "?" + new Date().getTime(),
|
||||
async: false
|
||||
})
|
||||
this.fixturesCache_[relativeUrl] = request.responseText
|
||||
}
|
||||
|
||||
jasmine.Fixtures.prototype.makeFixtureUrl_ = function(relativeUrl){
|
||||
return this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl
|
||||
}
|
||||
|
||||
jasmine.Fixtures.prototype.proxyCallTo_ = function(methodName, passedArguments) {
|
||||
return this[methodName].apply(this, passedArguments)
|
||||
}
|
||||
|
||||
|
||||
jasmine.StyleFixtures = function() {
|
||||
this.fixturesCache_ = {}
|
||||
this.fixturesNodes_ = []
|
||||
this.fixturesPath = 'spec/javascripts/fixtures'
|
||||
}
|
||||
|
||||
jasmine.StyleFixtures.prototype.set = function(css) {
|
||||
this.cleanUp()
|
||||
this.createStyle_(css)
|
||||
}
|
||||
|
||||
jasmine.StyleFixtures.prototype.appendSet = function(css) {
|
||||
this.createStyle_(css)
|
||||
}
|
||||
|
||||
jasmine.StyleFixtures.prototype.preload = function() {
|
||||
this.read_.apply(this, arguments)
|
||||
}
|
||||
|
||||
jasmine.StyleFixtures.prototype.load = function() {
|
||||
this.cleanUp()
|
||||
this.createStyle_(this.read_.apply(this, arguments))
|
||||
}
|
||||
|
||||
jasmine.StyleFixtures.prototype.appendLoad = function() {
|
||||
this.createStyle_(this.read_.apply(this, arguments))
|
||||
}
|
||||
|
||||
jasmine.StyleFixtures.prototype.cleanUp = function() {
|
||||
while(this.fixturesNodes_.length) {
|
||||
this.fixturesNodes_.pop().remove()
|
||||
}
|
||||
}
|
||||
|
||||
jasmine.StyleFixtures.prototype.createStyle_ = function(html) {
|
||||
var styleText = $('<div></div>').html(html).text(),
|
||||
style = $('<style>' + styleText + '</style>')
|
||||
|
||||
this.fixturesNodes_.push(style)
|
||||
|
||||
$('head').append(style)
|
||||
}
|
||||
|
||||
jasmine.StyleFixtures.prototype.clearCache = jasmine.Fixtures.prototype.clearCache
|
||||
|
||||
jasmine.StyleFixtures.prototype.read_ = jasmine.Fixtures.prototype.read
|
||||
|
||||
jasmine.StyleFixtures.prototype.getFixtureHtml_ = jasmine.Fixtures.prototype.getFixtureHtml_
|
||||
|
||||
jasmine.StyleFixtures.prototype.loadFixtureIntoCache_ = jasmine.Fixtures.prototype.loadFixtureIntoCache_
|
||||
|
||||
jasmine.StyleFixtures.prototype.makeFixtureUrl_ = jasmine.Fixtures.prototype.makeFixtureUrl_
|
||||
|
||||
jasmine.StyleFixtures.prototype.proxyCallTo_ = jasmine.Fixtures.prototype.proxyCallTo_
|
||||
|
||||
jasmine.getJSONFixtures = function() {
|
||||
return jasmine.currentJSONFixtures_ = jasmine.currentJSONFixtures_ || new jasmine.JSONFixtures()
|
||||
}
|
||||
|
||||
jasmine.JSONFixtures = function() {
|
||||
this.fixturesCache_ = {}
|
||||
this.fixturesPath = 'spec/javascripts/fixtures/json'
|
||||
}
|
||||
|
||||
jasmine.JSONFixtures.prototype.load = function() {
|
||||
this.read.apply(this, arguments)
|
||||
return this.fixturesCache_
|
||||
}
|
||||
|
||||
jasmine.JSONFixtures.prototype.read = function() {
|
||||
var fixtureUrls = arguments
|
||||
for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) {
|
||||
this.getFixtureData_(fixtureUrls[urlIndex])
|
||||
}
|
||||
return this.fixturesCache_
|
||||
}
|
||||
|
||||
jasmine.JSONFixtures.prototype.clearCache = function() {
|
||||
this.fixturesCache_ = {}
|
||||
}
|
||||
|
||||
jasmine.JSONFixtures.prototype.getFixtureData_ = function(url) {
|
||||
this.loadFixtureIntoCache_(url)
|
||||
return this.fixturesCache_[url]
|
||||
}
|
||||
|
||||
jasmine.JSONFixtures.prototype.loadFixtureIntoCache_ = function(relativeUrl) {
|
||||
var self = this
|
||||
var url = this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl
|
||||
$.ajax({
|
||||
async: false, // must be synchronous to guarantee that no tests are run before fixture is loaded
|
||||
cache: false,
|
||||
dataType: 'json',
|
||||
url: url,
|
||||
success: function(data) {
|
||||
self.fixturesCache_[relativeUrl] = data
|
||||
},
|
||||
error: function(jqXHR, status, errorThrown) {
|
||||
throw Error('JSONFixture could not be loaded: ' + url + ' (status: ' + status + ', message: ' + errorThrown.message + ')')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
jasmine.JSONFixtures.prototype.proxyCallTo_ = function(methodName, passedArguments) {
|
||||
return this[methodName].apply(this, passedArguments)
|
||||
}
|
||||
|
||||
jasmine.JQuery = function() {}
|
||||
|
||||
jasmine.JQuery.browserTagCaseIndependentHtml = function(html) {
|
||||
return $('<div/>').append(html).html()
|
||||
}
|
||||
|
||||
jasmine.JQuery.elementToString = function(element) {
|
||||
var domEl = $(element).get(0)
|
||||
if (domEl == undefined || domEl.cloneNode)
|
||||
return $('<div />').append($(element).clone()).html()
|
||||
else
|
||||
return element.toString()
|
||||
}
|
||||
|
||||
jasmine.JQuery.matchersClass = {}
|
||||
|
||||
!function(namespace) {
|
||||
var data = {
|
||||
spiedEvents: {},
|
||||
handlers: []
|
||||
}
|
||||
|
||||
namespace.events = {
|
||||
spyOn: function(selector, eventName) {
|
||||
var handler = function(e) {
|
||||
data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] = jasmine.util.argsToArray(arguments)
|
||||
}
|
||||
$(selector).on(eventName, handler)
|
||||
data.handlers.push(handler)
|
||||
return {
|
||||
selector: selector,
|
||||
eventName: eventName,
|
||||
handler: handler,
|
||||
reset: function(){
|
||||
delete data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
args: function(selector, eventName) {
|
||||
var actualArgs = data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)];
|
||||
|
||||
if (!actualArgs) {
|
||||
throw "There is no spy for " + eventName + " on " + selector.toString() + ". Make sure to create a spy using spyOnEvent.";
|
||||
}
|
||||
|
||||
return actualArgs;
|
||||
},
|
||||
|
||||
wasTriggered: function(selector, eventName) {
|
||||
return !!(data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)])
|
||||
},
|
||||
|
||||
wasTriggeredWith: function(selector, eventName, expectedArgs, env) {
|
||||
var actualArgs = jasmine.JQuery.events.args(selector, eventName).slice(1);
|
||||
if (Object.prototype.toString.call(expectedArgs) !== '[object Array]') {
|
||||
actualArgs = actualArgs[0];
|
||||
}
|
||||
return env.equals_(expectedArgs, actualArgs);
|
||||
},
|
||||
|
||||
wasPrevented: function(selector, eventName) {
|
||||
var args = data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)],
|
||||
e = args ? args[0] : undefined;
|
||||
return e && e.isDefaultPrevented()
|
||||
},
|
||||
|
||||
wasStopped: function(selector, eventName) {
|
||||
var args = data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)],
|
||||
e = args ? args[0] : undefined;
|
||||
return e && e.isPropagationStopped()
|
||||
},
|
||||
|
||||
cleanUp: function() {
|
||||
data.spiedEvents = {}
|
||||
data.handlers = []
|
||||
}
|
||||
}
|
||||
}(jasmine.JQuery)
|
||||
|
||||
!function(){
|
||||
var jQueryMatchers = {
|
||||
toHaveClass: function(className) {
|
||||
return this.actual.hasClass(className)
|
||||
},
|
||||
|
||||
toHaveCss: function(css){
|
||||
for (var prop in css){
|
||||
if (this.actual.css(prop) !== css[prop]) return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
|
||||
toBeVisible: function() {
|
||||
return this.actual.is(':visible')
|
||||
},
|
||||
|
||||
toBeHidden: function() {
|
||||
return this.actual.is(':hidden')
|
||||
},
|
||||
|
||||
toBeSelected: function() {
|
||||
return this.actual.is(':selected')
|
||||
},
|
||||
|
||||
toBeChecked: function() {
|
||||
return this.actual.is(':checked')
|
||||
},
|
||||
|
||||
toBeEmpty: function() {
|
||||
return this.actual.is(':empty')
|
||||
},
|
||||
|
||||
toExist: function() {
|
||||
return $(document).find(this.actual).length
|
||||
},
|
||||
|
||||
toHaveLength: function(length) {
|
||||
return this.actual.length === length
|
||||
},
|
||||
|
||||
toHaveAttr: function(attributeName, expectedAttributeValue) {
|
||||
return hasProperty(this.actual.attr(attributeName), expectedAttributeValue)
|
||||
},
|
||||
|
||||
toHaveProp: function(propertyName, expectedPropertyValue) {
|
||||
return hasProperty(this.actual.prop(propertyName), expectedPropertyValue)
|
||||
},
|
||||
|
||||
toHaveId: function(id) {
|
||||
return this.actual.attr('id') == id
|
||||
},
|
||||
|
||||
toHaveHtml: function(html) {
|
||||
return this.actual.html() == jasmine.JQuery.browserTagCaseIndependentHtml(html)
|
||||
},
|
||||
|
||||
toContainHtml: function(html){
|
||||
var actualHtml = this.actual.html()
|
||||
var expectedHtml = jasmine.JQuery.browserTagCaseIndependentHtml(html)
|
||||
return (actualHtml.indexOf(expectedHtml) >= 0)
|
||||
},
|
||||
|
||||
toHaveText: function(text) {
|
||||
var trimmedText = $.trim(this.actual.text())
|
||||
if (text && $.isFunction(text.test)) {
|
||||
return text.test(trimmedText)
|
||||
} else {
|
||||
return trimmedText == text
|
||||
}
|
||||
},
|
||||
|
||||
toContainText: function(text) {
|
||||
var trimmedText = $.trim(this.actual.text())
|
||||
if (text && $.isFunction(text.test)) {
|
||||
return text.test(trimmedText)
|
||||
} else {
|
||||
return trimmedText.indexOf(text) != -1;
|
||||
}
|
||||
},
|
||||
|
||||
toHaveValue: function(value) {
|
||||
return this.actual.val() === value
|
||||
},
|
||||
|
||||
toHaveData: function(key, expectedValue) {
|
||||
return hasProperty(this.actual.data(key), expectedValue)
|
||||
},
|
||||
|
||||
toBe: function(selector) {
|
||||
return this.actual.is(selector)
|
||||
},
|
||||
|
||||
toContain: function(selector) {
|
||||
return this.actual.find(selector).length
|
||||
},
|
||||
|
||||
toBeDisabled: function(selector){
|
||||
return this.actual.is(':disabled')
|
||||
},
|
||||
|
||||
toBeFocused: function(selector) {
|
||||
return this.actual[0] === this.actual[0].ownerDocument.activeElement
|
||||
},
|
||||
|
||||
toHandle: function(event) {
|
||||
|
||||
var events = $._data(this.actual.get(0), "events")
|
||||
|
||||
if(!events || !event || typeof event !== "string") {
|
||||
return false
|
||||
}
|
||||
|
||||
var namespaces = event.split(".")
|
||||
var eventType = namespaces.shift()
|
||||
var sortedNamespaces = namespaces.slice(0).sort()
|
||||
var namespaceRegExp = new RegExp("(^|\\.)" + sortedNamespaces.join("\\.(?:.*\\.)?") + "(\\.|$)")
|
||||
|
||||
if(events[eventType] && namespaces.length) {
|
||||
for(var i = 0; i < events[eventType].length; i++) {
|
||||
var namespace = events[eventType][i].namespace
|
||||
if(namespaceRegExp.test(namespace)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return events[eventType] && events[eventType].length > 0
|
||||
}
|
||||
},
|
||||
|
||||
// tests the existence of a specific event binding + handler
|
||||
toHandleWith: function(eventName, eventHandler) {
|
||||
var stack = $._data(this.actual.get(0), "events")[eventName]
|
||||
for (var i = 0; i < stack.length; i++) {
|
||||
if (stack[i].handler == eventHandler) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var hasProperty = function(actualValue, expectedValue) {
|
||||
if (expectedValue === undefined) return actualValue !== undefined
|
||||
return actualValue == expectedValue
|
||||
}
|
||||
|
||||
var bindMatcher = function(methodName) {
|
||||
var builtInMatcher = jasmine.Matchers.prototype[methodName]
|
||||
|
||||
jasmine.JQuery.matchersClass[methodName] = function() {
|
||||
if (this.actual
|
||||
&& (this.actual instanceof $
|
||||
|| jasmine.isDomNode(this.actual))) {
|
||||
this.actual = $(this.actual)
|
||||
var result = jQueryMatchers[methodName].apply(this, arguments)
|
||||
var element
|
||||
if (this.actual.get && (element = this.actual.get()[0]) && !$.isWindow(element) && element.tagName !== "HTML")
|
||||
this.actual = jasmine.JQuery.elementToString(this.actual)
|
||||
return result
|
||||
}
|
||||
|
||||
if (builtInMatcher) {
|
||||
return builtInMatcher.apply(this, arguments)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for(var methodName in jQueryMatchers) {
|
||||
bindMatcher(methodName)
|
||||
}
|
||||
}()
|
||||
|
||||
beforeEach(function() {
|
||||
this.addMatchers(jasmine.JQuery.matchersClass)
|
||||
this.addMatchers({
|
||||
toHaveBeenTriggeredOn: function(selector) {
|
||||
this.message = function() {
|
||||
return [
|
||||
"Expected event " + this.actual + " to have been triggered on " + selector,
|
||||
"Expected event " + this.actual + " not to have been triggered on " + selector
|
||||
]
|
||||
}
|
||||
return jasmine.JQuery.events.wasTriggered(selector, this.actual)
|
||||
}
|
||||
})
|
||||
this.addMatchers({
|
||||
toHaveBeenTriggered: function(){
|
||||
var eventName = this.actual.eventName,
|
||||
selector = this.actual.selector
|
||||
this.message = function() {
|
||||
return [
|
||||
"Expected event " + eventName + " to have been triggered on " + selector,
|
||||
"Expected event " + eventName + " not to have been triggered on " + selector
|
||||
]
|
||||
}
|
||||
return jasmine.JQuery.events.wasTriggered(selector, eventName)
|
||||
}
|
||||
})
|
||||
this.addMatchers({
|
||||
toHaveBeenTriggeredOnAndWith: function() {
|
||||
var selector = arguments[0],
|
||||
expectedArgs = arguments[1],
|
||||
wasTriggered = jasmine.JQuery.events.wasTriggered(selector, this.actual);
|
||||
this.message = function() {
|
||||
if (wasTriggered) {
|
||||
var actualArgs = jasmine.JQuery.events.args(selector, this.actual, expectedArgs)[1];
|
||||
return [
|
||||
"Expected event " + this.actual + " to have been triggered with " + jasmine.pp(expectedArgs) + " but it was triggered with " + jasmine.pp(actualArgs),
|
||||
"Expected event " + this.actual + " not to have been triggered with " + jasmine.pp(expectedArgs) + " but it was triggered with " + jasmine.pp(actualArgs)
|
||||
]
|
||||
} else {
|
||||
return [
|
||||
"Expected event " + this.actual + " to have been triggered on " + selector,
|
||||
"Expected event " + this.actual + " not to have been triggered on " + selector
|
||||
]
|
||||
}
|
||||
}
|
||||
return wasTriggered && jasmine.JQuery.events.wasTriggeredWith(selector, this.actual, expectedArgs, this.env);
|
||||
}
|
||||
})
|
||||
this.addMatchers({
|
||||
toHaveBeenPreventedOn: function(selector) {
|
||||
this.message = function() {
|
||||
return [
|
||||
"Expected event " + this.actual + " to have been prevented on " + selector,
|
||||
"Expected event " + this.actual + " not to have been prevented on " + selector
|
||||
]
|
||||
}
|
||||
return jasmine.JQuery.events.wasPrevented(selector, this.actual)
|
||||
}
|
||||
})
|
||||
this.addMatchers({
|
||||
toHaveBeenPrevented: function() {
|
||||
var eventName = this.actual.eventName,
|
||||
selector = this.actual.selector
|
||||
this.message = function() {
|
||||
return [
|
||||
"Expected event " + eventName + " to have been prevented on " + selector,
|
||||
"Expected event " + eventName + " not to have been prevented on " + selector
|
||||
]
|
||||
}
|
||||
return jasmine.JQuery.events.wasPrevented(selector, eventName)
|
||||
}
|
||||
})
|
||||
this.addMatchers({
|
||||
toHaveBeenStoppedOn: function(selector) {
|
||||
this.message = function() {
|
||||
return [
|
||||
"Expected event " + this.actual + " to have been stopped on " + selector,
|
||||
"Expected event " + this.actual + " not to have been stopped on " + selector
|
||||
]
|
||||
}
|
||||
return jasmine.JQuery.events.wasStopped(selector, this.actual)
|
||||
}
|
||||
})
|
||||
this.addMatchers({
|
||||
toHaveBeenStopped: function() {
|
||||
var eventName = this.actual.eventName,
|
||||
selector = this.actual.selector
|
||||
this.message = function() {
|
||||
return [
|
||||
"Expected event " + eventName + " to have been stopped on " + selector,
|
||||
"Expected event " + eventName + " not to have been stopped on " + selector
|
||||
]
|
||||
}
|
||||
return jasmine.JQuery.events.wasStopped(selector, eventName)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(function() {
|
||||
jasmine.getFixtures().cleanUp()
|
||||
jasmine.getStyleFixtures().cleanUp()
|
||||
jasmine.JQuery.events.cleanUp()
|
||||
})
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "jquery",
|
||||
"version": "1.8.3",
|
||||
"main": "./jquery.js",
|
||||
"dependencies": {},
|
||||
"_id": "jquery@1.8.3",
|
||||
"readme": "ERROR: No README.md file found!",
|
||||
"description": "ERROR: No README.md file found!",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/components/jquery.git"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "components/jquery",
|
||||
"description": "jQuery JavaScript Library",
|
||||
"type": "component",
|
||||
"homepage": "http://jquery.com",
|
||||
"license": "MIT",
|
||||
"support": {
|
||||
"irc": "irc://irc.freenode.org/jquery",
|
||||
"issues": "http://bugs.jquery.com",
|
||||
"forum": "http://forum.jquery.com",
|
||||
"wiki": "http://docs.jquery.com/",
|
||||
"source": "https://github.com/jquery/jquery"
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "John Resig",
|
||||
"email": "jeresig@gmail.com"
|
||||
}
|
||||
],
|
||||
"extra": {
|
||||
"js": "jquery.js"
|
||||
}
|
||||
}
|
||||
9472
resources/flightjs-example-app/components/jquery/jquery.js
vendored
Normal file
2
resources/flightjs-example-app/components/jquery/jquery.min.js
vendored
Normal file
635
resources/flightjs-example-app/components/mustache/mustache.js
Normal file
|
|
@ -0,0 +1,635 @@
|
|||
/*!
|
||||
* mustache.js - Logic-less {{mustache}} templates with JavaScript
|
||||
* http://github.com/janl/mustache.js
|
||||
*/
|
||||
|
||||
/*global define: false*/
|
||||
|
||||
var Mustache;
|
||||
|
||||
/*!
|
||||
* mustache.js - Logic-less {{mustache}} templates with JavaScript
|
||||
* http://github.com/janl/mustache.js
|
||||
*/
|
||||
|
||||
/*global define: false*/
|
||||
|
||||
var Mustache;
|
||||
|
||||
(function (exports) {
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
module.exports = exports; // CommonJS
|
||||
} else if (typeof define === "function") {
|
||||
define(exports); // AMD
|
||||
} else {
|
||||
Mustache = exports; // <script>
|
||||
}
|
||||
}((function () {
|
||||
|
||||
var exports = {};
|
||||
|
||||
exports.name = "mustache.js";
|
||||
exports.version = "0.7.1";
|
||||
exports.tags = ["{{", "}}"];
|
||||
|
||||
exports.Scanner = Scanner;
|
||||
exports.Context = Context;
|
||||
exports.Writer = Writer;
|
||||
|
||||
var whiteRe = /\s*/;
|
||||
var spaceRe = /\s+/;
|
||||
var nonSpaceRe = /\S/;
|
||||
var eqRe = /\s*=/;
|
||||
var curlyRe = /\s*\}/;
|
||||
var tagRe = /#|\^|\/|>|\{|&|=|!/;
|
||||
|
||||
// Workaround for https://issues.apache.org/jira/browse/COUCHDB-577
|
||||
// See https://github.com/janl/mustache.js/issues/189
|
||||
function testRe(re, string) {
|
||||
return RegExp.prototype.test.call(re, string);
|
||||
}
|
||||
|
||||
function isWhitespace(string) {
|
||||
return !testRe(nonSpaceRe, string);
|
||||
}
|
||||
|
||||
var isArray = Array.isArray || function (obj) {
|
||||
return Object.prototype.toString.call(obj) === "[object Array]";
|
||||
};
|
||||
|
||||
function escapeRe(string) {
|
||||
return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
|
||||
}
|
||||
|
||||
var entityMap = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
"/": '/'
|
||||
};
|
||||
|
||||
function escapeHtml(string) {
|
||||
return String(string).replace(/[&<>"'\/]/g, function (s) {
|
||||
return entityMap[s];
|
||||
});
|
||||
}
|
||||
|
||||
// Export the escaping function so that the user may override it.
|
||||
// See https://github.com/janl/mustache.js/issues/244
|
||||
exports.escape = escapeHtml;
|
||||
|
||||
function Scanner(string) {
|
||||
this.string = string;
|
||||
this.tail = string;
|
||||
this.pos = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the tail is empty (end of string).
|
||||
*/
|
||||
Scanner.prototype.eos = function () {
|
||||
return this.tail === "";
|
||||
};
|
||||
|
||||
/**
|
||||
* Tries to match the given regular expression at the current position.
|
||||
* Returns the matched text if it can match, the empty string otherwise.
|
||||
*/
|
||||
Scanner.prototype.scan = function (re) {
|
||||
var match = this.tail.match(re);
|
||||
|
||||
if (match && match.index === 0) {
|
||||
this.tail = this.tail.substring(match[0].length);
|
||||
this.pos += match[0].length;
|
||||
return match[0];
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
/**
|
||||
* Skips all text until the given regular expression can be matched. Returns
|
||||
* the skipped string, which is the entire tail if no match can be made.
|
||||
*/
|
||||
Scanner.prototype.scanUntil = function (re) {
|
||||
var match, pos = this.tail.search(re);
|
||||
|
||||
switch (pos) {
|
||||
case -1:
|
||||
match = this.tail;
|
||||
this.pos += this.tail.length;
|
||||
this.tail = "";
|
||||
break;
|
||||
case 0:
|
||||
match = "";
|
||||
break;
|
||||
default:
|
||||
match = this.tail.substring(0, pos);
|
||||
this.tail = this.tail.substring(pos);
|
||||
this.pos += pos;
|
||||
}
|
||||
|
||||
return match;
|
||||
};
|
||||
|
||||
function Context(view, parent) {
|
||||
this.view = view;
|
||||
this.parent = parent;
|
||||
this.clearCache();
|
||||
}
|
||||
|
||||
Context.make = function (view) {
|
||||
return (view instanceof Context) ? view : new Context(view);
|
||||
};
|
||||
|
||||
Context.prototype.clearCache = function () {
|
||||
this._cache = {};
|
||||
};
|
||||
|
||||
Context.prototype.push = function (view) {
|
||||
return new Context(view, this);
|
||||
};
|
||||
|
||||
Context.prototype.lookup = function (name) {
|
||||
var value = this._cache[name];
|
||||
|
||||
if (!value) {
|
||||
if (name === ".") {
|
||||
value = this.view;
|
||||
} else {
|
||||
var context = this;
|
||||
|
||||
while (context) {
|
||||
if (name.indexOf(".") > 0) {
|
||||
var names = name.split("."), i = 0;
|
||||
|
||||
value = context.view;
|
||||
|
||||
while (value && i < names.length) {
|
||||
value = value[names[i++]];
|
||||
}
|
||||
} else {
|
||||
value = context.view[name];
|
||||
}
|
||||
|
||||
if (value != null) {
|
||||
break;
|
||||
}
|
||||
|
||||
context = context.parent;
|
||||
}
|
||||
}
|
||||
|
||||
this._cache[name] = value;
|
||||
}
|
||||
|
||||
if (typeof value === "function") {
|
||||
value = value.call(this.view);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
function Writer() {
|
||||
this.clearCache();
|
||||
}
|
||||
|
||||
Writer.prototype.clearCache = function () {
|
||||
this._cache = {};
|
||||
this._partialCache = {};
|
||||
};
|
||||
|
||||
Writer.prototype.compile = function (template, tags) {
|
||||
var fn = this._cache[template];
|
||||
|
||||
if (!fn) {
|
||||
var tokens = exports.parse(template, tags);
|
||||
fn = this._cache[template] = this.compileTokens(tokens, template);
|
||||
}
|
||||
|
||||
return fn;
|
||||
};
|
||||
|
||||
Writer.prototype.compilePartial = function (name, template, tags) {
|
||||
var fn = this.compile(template, tags);
|
||||
this._partialCache[name] = fn;
|
||||
return fn;
|
||||
};
|
||||
|
||||
Writer.prototype.compileTokens = function (tokens, template) {
|
||||
var fn = compileTokens(tokens);
|
||||
var self = this;
|
||||
|
||||
return function (view, partials) {
|
||||
if (partials) {
|
||||
if (typeof partials === "function") {
|
||||
self._loadPartial = partials;
|
||||
} else {
|
||||
for (var name in partials) {
|
||||
self.compilePartial(name, partials[name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fn(self, Context.make(view), template);
|
||||
};
|
||||
};
|
||||
|
||||
Writer.prototype.render = function (template, view, partials) {
|
||||
return this.compile(template)(view, partials);
|
||||
};
|
||||
|
||||
Writer.prototype._section = function (name, context, text, callback) {
|
||||
var value = context.lookup(name);
|
||||
|
||||
switch (typeof value) {
|
||||
case "object":
|
||||
if (isArray(value)) {
|
||||
var buffer = "";
|
||||
|
||||
for (var i = 0, len = value.length; i < len; ++i) {
|
||||
buffer += callback(this, context.push(value[i]));
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
return value ? callback(this, context.push(value)) : "";
|
||||
case "function":
|
||||
var self = this;
|
||||
var scopedRender = function (template) {
|
||||
return self.render(template, context);
|
||||
};
|
||||
|
||||
var result = value.call(context.view, text, scopedRender);
|
||||
return result != null ? result : "";
|
||||
default:
|
||||
if (value) {
|
||||
return callback(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
Writer.prototype._inverted = function (name, context, callback) {
|
||||
var value = context.lookup(name);
|
||||
|
||||
// Use JavaScript's definition of falsy. Include empty arrays.
|
||||
// See https://github.com/janl/mustache.js/issues/186
|
||||
if (!value || (isArray(value) && value.length === 0)) {
|
||||
return callback(this, context);
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
Writer.prototype._partial = function (name, context) {
|
||||
if (!(name in this._partialCache) && this._loadPartial) {
|
||||
this.compilePartial(name, this._loadPartial(name));
|
||||
}
|
||||
|
||||
var fn = this._partialCache[name];
|
||||
|
||||
return fn ? fn(context) : "";
|
||||
};
|
||||
|
||||
Writer.prototype._name = function (name, context) {
|
||||
var value = context.lookup(name);
|
||||
|
||||
if (typeof value === "function") {
|
||||
value = value.call(context.view);
|
||||
}
|
||||
|
||||
return (value == null) ? "" : String(value);
|
||||
};
|
||||
|
||||
Writer.prototype._escaped = function (name, context) {
|
||||
return exports.escape(this._name(name, context));
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the bounds of the section represented by the given `token` in
|
||||
* the original template by drilling down into nested sections to find the
|
||||
* last token that is part of that section. Returns an array of [start, end].
|
||||
*/
|
||||
function sectionBounds(token) {
|
||||
var start = token[3];
|
||||
var end = start;
|
||||
|
||||
var tokens;
|
||||
while ((tokens = token[4]) && tokens.length) {
|
||||
token = tokens[tokens.length - 1];
|
||||
end = token[3];
|
||||
}
|
||||
|
||||
return [start, end];
|
||||
}
|
||||
|
||||
/**
|
||||
* Low-level function that compiles the given `tokens` into a function
|
||||
* that accepts three arguments: a Writer, a Context, and the template.
|
||||
*/
|
||||
function compileTokens(tokens) {
|
||||
var subRenders = {};
|
||||
|
||||
function subRender(i, tokens, template) {
|
||||
if (!subRenders[i]) {
|
||||
var fn = compileTokens(tokens);
|
||||
subRenders[i] = function (writer, context) {
|
||||
return fn(writer, context, template);
|
||||
};
|
||||
}
|
||||
|
||||
return subRenders[i];
|
||||
}
|
||||
|
||||
return function (writer, context, template) {
|
||||
var buffer = "";
|
||||
var token, sectionText;
|
||||
|
||||
for (var i = 0, len = tokens.length; i < len; ++i) {
|
||||
token = tokens[i];
|
||||
|
||||
switch (token[0]) {
|
||||
case "#":
|
||||
sectionText = template.slice.apply(template, sectionBounds(token));
|
||||
buffer += writer._section(token[1], context, sectionText, subRender(i, token[4], template));
|
||||
break;
|
||||
case "^":
|
||||
buffer += writer._inverted(token[1], context, subRender(i, token[4], template));
|
||||
break;
|
||||
case ">":
|
||||
buffer += writer._partial(token[1], context);
|
||||
break;
|
||||
case "&":
|
||||
buffer += writer._name(token[1], context);
|
||||
break;
|
||||
case "name":
|
||||
buffer += writer._escaped(token[1], context);
|
||||
break;
|
||||
case "text":
|
||||
buffer += token[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return buffer;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Forms the given array of `tokens` into a nested tree structure where
|
||||
* tokens that represent a section have a fifth item: an array that contains
|
||||
* all tokens in that section.
|
||||
*/
|
||||
function nestTokens(tokens) {
|
||||
var tree = [];
|
||||
var collector = tree;
|
||||
var sections = [];
|
||||
var token, section;
|
||||
|
||||
for (var i = 0; i < tokens.length; ++i) {
|
||||
token = tokens[i];
|
||||
|
||||
switch (token[0]) {
|
||||
case "#":
|
||||
case "^":
|
||||
token[4] = [];
|
||||
sections.push(token);
|
||||
collector.push(token);
|
||||
collector = token[4];
|
||||
break;
|
||||
case "/":
|
||||
if (sections.length === 0) {
|
||||
throw new Error("Unopened section: " + token[1]);
|
||||
}
|
||||
|
||||
section = sections.pop();
|
||||
|
||||
if (section[1] !== token[1]) {
|
||||
throw new Error("Unclosed section: " + section[1]);
|
||||
}
|
||||
|
||||
if (sections.length > 0) {
|
||||
collector = sections[sections.length - 1][4];
|
||||
} else {
|
||||
collector = tree;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
collector.push(token);
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure there were no open sections when we're done.
|
||||
section = sections.pop();
|
||||
|
||||
if (section) {
|
||||
throw new Error("Unclosed section: " + section[1]);
|
||||
}
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines the values of consecutive text tokens in the given `tokens` array
|
||||
* to a single token.
|
||||
*/
|
||||
function squashTokens(tokens) {
|
||||
var token, lastToken, squashedTokens = [];
|
||||
|
||||
for (var i = 0; i < tokens.length; ++i) {
|
||||
token = tokens[i];
|
||||
|
||||
if (lastToken && lastToken[0] === "text" && token[0] === "text") {
|
||||
lastToken[1] += token[1];
|
||||
lastToken[3] = token[3];
|
||||
} else {
|
||||
lastToken = token;
|
||||
squashedTokens.push(token);
|
||||
}
|
||||
}
|
||||
|
||||
return squashedTokens;
|
||||
}
|
||||
|
||||
function escapeTags(tags) {
|
||||
if (tags.length !== 2) {
|
||||
throw new Error("Invalid tags: " + tags.join(" "));
|
||||
}
|
||||
|
||||
return [
|
||||
new RegExp(escapeRe(tags[0]) + "\\s*"),
|
||||
new RegExp("\\s*" + escapeRe(tags[1]))
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Breaks up the given `template` string into a tree of token objects. If
|
||||
* `tags` is given here it must be an array with two string values: the
|
||||
* opening and closing tags used in the template (e.g. ["<%", "%>"]). Of
|
||||
* course, the default is to use mustaches (i.e. Mustache.tags).
|
||||
*/
|
||||
exports.parse = function (template, tags) {
|
||||
template = template || '';
|
||||
tags = tags || exports.tags;
|
||||
|
||||
var tagRes = escapeTags(tags);
|
||||
var scanner = new Scanner(template);
|
||||
|
||||
var tokens = [], // Buffer to hold the tokens
|
||||
spaces = [], // Indices of whitespace tokens on the current line
|
||||
hasTag = false, // Is there a {{tag}} on the current line?
|
||||
nonSpace = false; // Is there a non-space char on the current line?
|
||||
|
||||
// Strips all whitespace tokens array for the current line
|
||||
// if there was a {{#tag}} on it and otherwise only space.
|
||||
function stripSpace() {
|
||||
if (hasTag && !nonSpace) {
|
||||
while (spaces.length) {
|
||||
tokens.splice(spaces.pop(), 1);
|
||||
}
|
||||
} else {
|
||||
spaces = [];
|
||||
}
|
||||
|
||||
hasTag = false;
|
||||
nonSpace = false;
|
||||
}
|
||||
|
||||
var start, type, value, chr;
|
||||
|
||||
while (!scanner.eos()) {
|
||||
start = scanner.pos;
|
||||
value = scanner.scanUntil(tagRes[0]);
|
||||
|
||||
if (value) {
|
||||
for (var i = 0, len = value.length; i < len; ++i) {
|
||||
chr = value.charAt(i);
|
||||
|
||||
if (isWhitespace(chr)) {
|
||||
spaces.push(tokens.length);
|
||||
} else {
|
||||
nonSpace = true;
|
||||
}
|
||||
|
||||
tokens.push(["text", chr, start, start + 1]);
|
||||
start += 1;
|
||||
|
||||
if (chr === "\n") {
|
||||
stripSpace(); // Check for whitespace on the current line.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start = scanner.pos;
|
||||
|
||||
// Match the opening tag.
|
||||
if (!scanner.scan(tagRes[0])) {
|
||||
break;
|
||||
}
|
||||
|
||||
hasTag = true;
|
||||
type = scanner.scan(tagRe) || "name";
|
||||
|
||||
// Skip any whitespace between tag and value.
|
||||
scanner.scan(whiteRe);
|
||||
|
||||
// Extract the tag value.
|
||||
if (type === "=") {
|
||||
value = scanner.scanUntil(eqRe);
|
||||
scanner.scan(eqRe);
|
||||
scanner.scanUntil(tagRes[1]);
|
||||
} else if (type === "{") {
|
||||
var closeRe = new RegExp("\\s*" + escapeRe("}" + tags[1]));
|
||||
value = scanner.scanUntil(closeRe);
|
||||
scanner.scan(curlyRe);
|
||||
scanner.scanUntil(tagRes[1]);
|
||||
type = "&";
|
||||
} else {
|
||||
value = scanner.scanUntil(tagRes[1]);
|
||||
}
|
||||
|
||||
// Match the closing tag.
|
||||
if (!scanner.scan(tagRes[1])) {
|
||||
throw new Error("Unclosed tag at " + scanner.pos);
|
||||
}
|
||||
|
||||
tokens.push([type, value, start, scanner.pos]);
|
||||
|
||||
if (type === "name" || type === "{" || type === "&") {
|
||||
nonSpace = true;
|
||||
}
|
||||
|
||||
// Set the tags for the next time around.
|
||||
if (type === "=") {
|
||||
tags = value.split(spaceRe);
|
||||
tagRes = escapeTags(tags);
|
||||
}
|
||||
}
|
||||
|
||||
tokens = squashTokens(tokens);
|
||||
|
||||
return nestTokens(tokens);
|
||||
};
|
||||
|
||||
// The high-level clearCache, compile, compilePartial, and render functions
|
||||
// use this default writer.
|
||||
var _writer = new Writer();
|
||||
|
||||
/**
|
||||
* Clears all cached templates and partials in the default writer.
|
||||
*/
|
||||
exports.clearCache = function () {
|
||||
return _writer.clearCache();
|
||||
};
|
||||
|
||||
/**
|
||||
* Compiles the given `template` to a reusable function using the default
|
||||
* writer.
|
||||
*/
|
||||
exports.compile = function (template, tags) {
|
||||
return _writer.compile(template, tags);
|
||||
};
|
||||
|
||||
/**
|
||||
* Compiles the partial with the given `name` and `template` to a reusable
|
||||
* function using the default writer.
|
||||
*/
|
||||
exports.compilePartial = function (name, template, tags) {
|
||||
return _writer.compilePartial(name, template, tags);
|
||||
};
|
||||
|
||||
/**
|
||||
* Compiles the given array of tokens (the output of a parse) to a reusable
|
||||
* function using the default writer.
|
||||
*/
|
||||
exports.compileTokens = function (tokens, template) {
|
||||
return _writer.compileTokens(tokens, template);
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the `template` with the given `view` and `partials` using the
|
||||
* default writer.
|
||||
*/
|
||||
exports.render = function (template, view, partials) {
|
||||
return _writer.render(template, view, partials);
|
||||
};
|
||||
|
||||
// This is here for backwards compatibility with 0.4.x.
|
||||
exports.to_html = function (template, view, partials, send) {
|
||||
var result = exports.render(template, view, partials);
|
||||
|
||||
if (typeof send === "function") {
|
||||
send(result);
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
return exports;
|
||||
|
||||
}())));
|
||||
1993
resources/flightjs-example-app/components/requirejs/require.js
Normal file
60
resources/flightjs-example-app/index.html
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Flight Mail</title>
|
||||
<link rel="stylesheet" href="components/bootstrap/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="components/bootstrap/css/bootstrap-responsive.min.css">
|
||||
<link rel="stylesheet" href="app/css/custom.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<br>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row-fluid">
|
||||
<div class="span2">
|
||||
<div class="sidebar-nav">
|
||||
|
||||
<button id="new_mail" class="btn compose" data-toggle="dropdown">New</button>
|
||||
|
||||
<br><br>
|
||||
|
||||
<ul id="folders" class="nav nav-list">
|
||||
<li class="folder-item selected" id="inbox"><i class="icon-folder-close"></i> Inbox</li>
|
||||
<li class="folder-item" id="later"><i class="icon-folder-close"></i> Later</li>
|
||||
<li class="folder-item" id="sent"><i class="icon-share-alt"></i> Sent</li>
|
||||
<li class="folder-item" id="trash"><i class="icon-trash"></i> Trash</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mail_panel" class="span10">
|
||||
|
||||
<div id="mail_controls" class="btn-toolbar" style="margin: 0 0 13px">
|
||||
<div class="btn-group">
|
||||
<button id="delete_mail" class="btn mail-action" disabled><i class="icon-trash"></i> delete</button>
|
||||
<button id="move_mail" class="btn mail-action" disabled><i class="icon-move"></i> move</button>
|
||||
<button id="forward" class="btn compose mail-action single-item" disabled><i class="icon-arrow-right"></i> forward</button>
|
||||
<button id="reply" class="btn compose mail-action single-item" disabled><i class="icon-pencil"></i> reply</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table id="mail_items" class="hero-unit table table-hover" style="background-color: #FFFFFF">
|
||||
<tbody id="mail_items_TB">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="compose_box" class="compose-box modal hide fade in"></div>
|
||||
|
||||
<div id="move_to_selector" class="move-to-selector hide"></div>
|
||||
|
||||
<script src="components/jquery/jquery.js"></script>
|
||||
<script src="components/es5-shim/es5-shim.js"></script>
|
||||
<script src="components/es5-shim/es5-sham.js"></script>
|
||||
<script data-main="requireMain.js" src="components/requirejs/require.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
58
resources/flightjs-example-app/karma.conf.js
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// Karma configuration file
|
||||
//
|
||||
// For all available config options and default values, see:
|
||||
// https://github.com/karma-runner/karma/blob/stable/lib/config.js#L54
|
||||
|
||||
|
||||
// base path, that will be used to resolve files and exclude
|
||||
basePath = '';
|
||||
|
||||
// list of files / patterns to load in the browser
|
||||
files = [
|
||||
'components/es5-shim/es5-shim.js',
|
||||
'components/es5-shim/es5-sham.js',
|
||||
|
||||
// frameworks
|
||||
JASMINE,
|
||||
JASMINE_ADAPTER,
|
||||
REQUIRE,
|
||||
REQUIRE_ADAPTER,
|
||||
|
||||
// loaded without require
|
||||
'components/jquery/jquery.js',
|
||||
'components/jasmine-jquery/lib/jasmine-jquery.js',
|
||||
'components/jasmine-flight/lib/jasmine-flight.js',
|
||||
|
||||
// loaded with require
|
||||
{pattern: 'components/flight/**/*.js', included: false},
|
||||
{pattern: 'components/mustache/**/*.js', included: false},
|
||||
{pattern: 'app/**/*.js', included: false},
|
||||
{pattern: 'test/fixtures/**/*.html', included: false},
|
||||
{pattern: 'test/spec/**/*.js', included: false},
|
||||
|
||||
'test/test-main.js'
|
||||
];
|
||||
|
||||
// list of files to exclude
|
||||
exclude = [
|
||||
|
||||
];
|
||||
|
||||
// use dots reporter, as travis terminal does not support escaping sequences
|
||||
reporters = [
|
||||
'dots'
|
||||
];
|
||||
|
||||
// enable / disable watching file and executing tests whenever any file changes
|
||||
// CLI --auto-watch --no-auto-watch
|
||||
autoWatch = true;
|
||||
|
||||
// start these browsers
|
||||
browsers = [
|
||||
'Chrome',
|
||||
'Firefox'
|
||||
];
|
||||
|
||||
// auto run tests on start (when browsers are captured) and exit
|
||||
// CLI --single-run --no-single-run
|
||||
singleRun = false;
|
||||
11
resources/flightjs-example-app/package.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"private": true,
|
||||
"name": "flight-example-app",
|
||||
"version": "0.0.0",
|
||||
"devDependencies": {
|
||||
"karma": "~0.8.5"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "./node_modules/.bin/karma start --single-run --browsers Firefox"
|
||||
}
|
||||
}
|
||||
24
resources/flightjs-example-app/requireMain.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
requirejs.config({
|
||||
baseUrl: '',
|
||||
paths: {
|
||||
'flight': 'components/flight'
|
||||
}
|
||||
});
|
||||
|
||||
require(
|
||||
[
|
||||
'flight/lib/compose',
|
||||
'flight/lib/registry',
|
||||
'flight/lib/advice',
|
||||
'flight/lib/logger',
|
||||
'flight/tools/debug/debug'
|
||||
],
|
||||
|
||||
function(compose, registry, advice, withLogging, debug) {
|
||||
debug.enable(true);
|
||||
compose.mixin(registry, [advice.withAdvice, withLogging]);
|
||||
require(['app/boot/page'], function(initialize) {
|
||||
initialize();
|
||||
});
|
||||
}
|
||||
);
|
||||
BIN
resources/gauge.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
resources/gauge@2x.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
213
resources/interactive.mjs
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
import { BenchmarkRunner } from "./benchmark-runner.mjs";
|
||||
import { params } from "./params.mjs";
|
||||
import { Suites } from "./tests.mjs";
|
||||
|
||||
class InteractiveBenchmarkRunner extends BenchmarkRunner {
|
||||
_stepPromise = undefined;
|
||||
_stepPromiseResolve = undefined;
|
||||
_isRunning = false;
|
||||
_isStepping = false;
|
||||
|
||||
constructor(suites, iterationCount) {
|
||||
super(suites);
|
||||
this._client = this._createClient();
|
||||
if (!Number.isInteger(iterationCount) || iterationCount <= 0)
|
||||
throw Error("iterationCount must be a positive integer.");
|
||||
this._iterationCount = iterationCount;
|
||||
}
|
||||
|
||||
_createClient() {
|
||||
return {
|
||||
willStartFirstIteration: this._start.bind(this),
|
||||
willRunTest: this._testStart.bind(this),
|
||||
didRunTest: this._testDone.bind(this),
|
||||
didRunSuites: this._iterationDone.bind(this),
|
||||
didFinishLastIteration: this._done.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
_start() {
|
||||
if (this._isRunning)
|
||||
throw Error("Runner was not stopped before starting;");
|
||||
this._isRunning = true;
|
||||
if (this._isStepping)
|
||||
this._stepPromise = this._newStepPromise();
|
||||
}
|
||||
|
||||
_step() {
|
||||
if (!this._stepPromise) {
|
||||
// Allow switching to stepping mid-run.
|
||||
this._stepPromise = this._newStepPromise();
|
||||
} else {
|
||||
const resolve = this._stepPromiseResolve;
|
||||
this._stepPromise = this._newStepPromise();
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
|
||||
_newStepPromise() {
|
||||
return new Promise((resolve) => {
|
||||
this._stepPromiseResolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
_testStart(suite, test) {
|
||||
test.anchor.classList.add("running");
|
||||
}
|
||||
|
||||
async _testDone(suite, test) {
|
||||
const classList = test.anchor.classList;
|
||||
classList.remove("running");
|
||||
classList.add("ran");
|
||||
if (this._isStepping)
|
||||
await this._stepPromise;
|
||||
}
|
||||
|
||||
_iterationDone(measuredValues) {
|
||||
let results = "";
|
||||
for (const suiteName in measuredValues.tests) {
|
||||
let suiteResults = measuredValues.tests[suiteName];
|
||||
for (const testName in suiteResults.tests) {
|
||||
let testResults = suiteResults.tests[testName];
|
||||
for (const subtestName in testResults.tests)
|
||||
results += `${suiteName} : ${testName} : ${subtestName}: ${testResults.tests[subtestName]} ms\n`;
|
||||
}
|
||||
results += `${suiteName} : ${suiteResults.total} ms\n`;
|
||||
}
|
||||
results += `Arithmetic Mean : ${measuredValues.mean} ms\n`;
|
||||
results += `Geometric Mean : ${measuredValues.geomean} ms\n`;
|
||||
results += `Total : ${measuredValues.total} ms\n`;
|
||||
results += `Score : ${measuredValues.score} rpm\n`;
|
||||
|
||||
if (!results)
|
||||
return;
|
||||
|
||||
const pre = document.createElement("pre");
|
||||
document.body.appendChild(pre);
|
||||
pre.textContent = results;
|
||||
}
|
||||
|
||||
_done() {
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
runStep() {
|
||||
this._isStepping = true;
|
||||
if (!this._isRunning)
|
||||
this.runMultipleIterations(this._iterationCount);
|
||||
else
|
||||
this._step();
|
||||
}
|
||||
|
||||
runSuites() {
|
||||
if (this._isRunning) {
|
||||
if (this._isStepping) {
|
||||
// Switch to continuous running only if we've been stepping.
|
||||
this._isStepping = false;
|
||||
this._step();
|
||||
}
|
||||
} else {
|
||||
this._isStepping = false;
|
||||
this.runMultipleIterations(this._iterationCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expose Suites/BenchmarkRunner for backwards compatibility
|
||||
globalThis.BenchmarkRunner = InteractiveBenchmarkRunner;
|
||||
|
||||
function formatTestName(suiteName, testName) {
|
||||
return suiteName + (testName ? `/${testName}` : "");
|
||||
}
|
||||
|
||||
function createUIForSuites(suites, onStep, onRunSuites) {
|
||||
const control = document.createElement("nav");
|
||||
const ol = document.createElement("ol");
|
||||
const checkboxes = [];
|
||||
for (let suiteIndex = 0; suiteIndex < suites.length; suiteIndex++) {
|
||||
const suite = suites[suiteIndex];
|
||||
const li = document.createElement("li");
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.id = suite.name;
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.checked = !suite.disabled;
|
||||
checkbox.onchange = () => {
|
||||
suite.disabled = !checkbox.checked;
|
||||
};
|
||||
checkbox.onchange();
|
||||
checkboxes.push(checkbox);
|
||||
|
||||
li.appendChild(checkbox);
|
||||
var label = document.createElement("label");
|
||||
label.appendChild(document.createTextNode(formatTestName(suite.name)));
|
||||
li.appendChild(label);
|
||||
label.htmlFor = checkbox.id;
|
||||
|
||||
const testList = document.createElement("ol");
|
||||
for (let testIndex = 0; testIndex < suite.tests.length; testIndex++) {
|
||||
const testItem = document.createElement("li");
|
||||
const test = suite.tests[testIndex];
|
||||
const anchor = document.createElement("a");
|
||||
anchor.id = `${suite.name}-${test.name}`;
|
||||
test.anchor = anchor;
|
||||
anchor.appendChild(document.createTextNode(formatTestName(suite.name, test.name)));
|
||||
testItem.appendChild(anchor);
|
||||
testList.appendChild(testItem);
|
||||
}
|
||||
li.appendChild(testList);
|
||||
|
||||
ol.appendChild(li);
|
||||
}
|
||||
|
||||
control.appendChild(ol);
|
||||
|
||||
let button = document.createElement("button");
|
||||
button.textContent = "Step";
|
||||
button.onclick = onStep;
|
||||
control.appendChild(button);
|
||||
|
||||
button = document.createElement("button");
|
||||
button.textContent = "Run";
|
||||
button.id = "runSuites";
|
||||
button.onclick = onRunSuites;
|
||||
control.appendChild(button);
|
||||
|
||||
button = document.createElement("button");
|
||||
button.textContent = "Select all";
|
||||
button.onclick = () => {
|
||||
for (var suiteIndex = 0; suiteIndex < suites.length; suiteIndex++) {
|
||||
suites[suiteIndex].disabled = false;
|
||||
checkboxes[suiteIndex].checked = true;
|
||||
}
|
||||
};
|
||||
control.appendChild(button);
|
||||
|
||||
button = document.createElement("button");
|
||||
button.textContent = "Unselect all";
|
||||
button.onclick = () => {
|
||||
for (var suiteIndex = 0; suiteIndex < suites.length; suiteIndex++) {
|
||||
suites[suiteIndex].disabled = true;
|
||||
checkboxes[suiteIndex].checked = false;
|
||||
}
|
||||
};
|
||||
control.appendChild(button);
|
||||
|
||||
return control;
|
||||
}
|
||||
|
||||
function startTest() {
|
||||
if (params.suites.length > 0 || params.tags.length > 0)
|
||||
Suites.enable(params.suites, params.tags);
|
||||
|
||||
const interactiveRunner = new window.BenchmarkRunner(Suites, params.iterationCount);
|
||||
if (!(interactiveRunner instanceof InteractiveBenchmarkRunner))
|
||||
throw Error("window.BenchmarkRunner must be a subclass of InteractiveBenchmarkRunner");
|
||||
|
||||
// Don't call step while step is already executing.
|
||||
document.body.appendChild(createUIForSuites(Suites, interactiveRunner.runStep.bind(interactiveRunner), interactiveRunner.runSuites.bind(interactiveRunner)));
|
||||
|
||||
if (params.startAutomatically)
|
||||
document.getElementById("runSuites").click();
|
||||
}
|
||||
|
||||
window.addEventListener("load", startTest);
|
||||
BIN
resources/logo.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
resources/logo@2x.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
792
resources/main.css
Normal file
|
|
@ -0,0 +1,792 @@
|
|||
:root {
|
||||
--viewport-width: 800px;
|
||||
--viewport-height: 600px;
|
||||
--foreground: rgb(235, 235, 235);
|
||||
--foreground-alpha: rgba(235, 235, 235, 0.2);
|
||||
--inactive-color: rgb(128, 128, 128);
|
||||
--background: rgb(46, 51, 55);
|
||||
--running-background: #f5f5f5;
|
||||
--highlight: rgb(232, 79, 79);
|
||||
--text-width: 650px;
|
||||
--metrics-line-height: 25px;
|
||||
--scrollbar-width: 10px;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: "Helvetica Neue", Helvetica, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
::selection {
|
||||
color: var(--background);
|
||||
background-color: var(--foreground);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar,
|
||||
::-webkit-scrollbar-track,
|
||||
::-webkit-scrollbar-corner {
|
||||
background-color: var(--background);
|
||||
}
|
||||
::-webkit-scrollbar,
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: var(--inactive-color);
|
||||
border-radius: 8px;
|
||||
width: var(--scrollbar-width);
|
||||
height: var(--scrollbar-width);
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: var(--foreground);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--foreground);
|
||||
}
|
||||
|
||||
thead {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
h1,
|
||||
button,
|
||||
.button {
|
||||
font-family: "Futura-Medium", Futura, "Helvetica Neue", Helvetica, Verdana, sans-serif;
|
||||
}
|
||||
.button {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: Menlo, Monaco, monospace;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 1px solid var(--foreground);
|
||||
width: 50%;
|
||||
margin: 40px auto;
|
||||
}
|
||||
|
||||
img {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
|
||||
.no-select {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
main {
|
||||
}
|
||||
|
||||
.logo {
|
||||
position: absolute;
|
||||
left: -70px;
|
||||
top: 155px;
|
||||
width: 75px;
|
||||
height: 406px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.version {
|
||||
transform: rotate(-90deg);
|
||||
font-size: 65px;
|
||||
font-family: Futura-Medium, Futura, Roboto, "Helvetica Neue", Helvetica, Verdana, sans-serif;
|
||||
font-style: italic;
|
||||
letter-spacing: -0.05em;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: -105px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 30px;
|
||||
font-size: 40px;
|
||||
font-weight: normal;
|
||||
color: var(--foreground);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 10px 0 6px 0;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
line-height: 21px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
li {
|
||||
font-size: 16px;
|
||||
line-height: 21px;
|
||||
}
|
||||
|
||||
li + li {
|
||||
margin-top: 21px;
|
||||
}
|
||||
|
||||
li + ul > li {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
li + ul {
|
||||
margin-bottom: 21px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin-top: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
section#home > .buttons {
|
||||
margin-top: 80px;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
margin: 5px 0px;
|
||||
}
|
||||
|
||||
button,
|
||||
.button {
|
||||
appearance: none;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.button {
|
||||
box-sizing: border-box;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.buttons button,
|
||||
.buttons .button {
|
||||
appearance: none;
|
||||
border: 3px solid var(--foreground);
|
||||
border-radius: 10px;
|
||||
min-width: 200px;
|
||||
padding: 5px 0px;
|
||||
margin: 0 20px;
|
||||
font-size: 25px;
|
||||
color: var(--foreground);
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.buttons button:active {
|
||||
background-color: var(--foreground);
|
||||
color: var(--background);
|
||||
border-color: var(--foreground) !important;
|
||||
}
|
||||
|
||||
.buttons button:focus {
|
||||
outline: none;
|
||||
border-color: var(--highlight);
|
||||
}
|
||||
|
||||
.developer-mode {
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
background: #602525;
|
||||
border: 3px solid rgba(255, 255, 255, 0.5);
|
||||
position: fixed;
|
||||
left: 10px;
|
||||
top: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.developer-mode summary {
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
padding: 1rem;
|
||||
margin: -1rem;
|
||||
}
|
||||
|
||||
.developer-mode-content {
|
||||
flex: 1;
|
||||
max-height: 80vh;
|
||||
overflow: auto;
|
||||
margin-right: calc(0px - var(--scrollbar-width));
|
||||
padding-right: var(--scrollbar-width);
|
||||
}
|
||||
|
||||
.developer-mode-content ol {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 1em 0 0.5em 0;
|
||||
}
|
||||
.developer-mode-content .button-bar {
|
||||
display: flex;
|
||||
margin-top: 5px;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.developer-mode-content li + li {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.developer-mode-content button {
|
||||
background: white;
|
||||
flex: auto;
|
||||
padding: 4px;
|
||||
appearance: button;
|
||||
border: 2px solid rgba(255, 255, 255, 0.5);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.developer-mode-content .tag {
|
||||
border-radius: 100vh;
|
||||
color: white;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.developer-mode-content label {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
}
|
||||
.developer-mode-content hr {
|
||||
width: initial;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
section {
|
||||
display: none;
|
||||
--padding-width: 15px;
|
||||
--border-width: 6px;
|
||||
position: absolute;
|
||||
width: var(--viewport-width);
|
||||
height: var(--viewport-height);
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-top: calc(var(--viewport-height) / -2 - var(--padding-width) - var(--border-width));
|
||||
margin-left: calc(var(--viewport-width) / -2 - var(--padding-width) - var(--border-width));
|
||||
padding: var(--padding-width);
|
||||
border: var(--border-width) solid var(--foreground);
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
section:target {
|
||||
display: block;
|
||||
}
|
||||
|
||||
section > p {
|
||||
margin: 10px 20px;
|
||||
}
|
||||
|
||||
#testContainer {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
left: 15px;
|
||||
width: var(--viewport-width);
|
||||
height: var(--viewport-height);
|
||||
}
|
||||
|
||||
section#home p {
|
||||
margin: 0 auto;
|
||||
width: 70%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
section#home .content {
|
||||
margin-top: 160px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button.show-about {
|
||||
margin-top: 100px;
|
||||
font-size: 16px;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
#screen-size-warning {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#progress {
|
||||
position: absolute;
|
||||
bottom: -6px;
|
||||
left: 60px;
|
||||
right: 60px;
|
||||
height: 6px;
|
||||
border-left: 6px solid var(--background);
|
||||
border-right: 6px solid var(--background);
|
||||
}
|
||||
|
||||
#progress-completed {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 6px;
|
||||
width: 100%;
|
||||
appearance: none;
|
||||
border: none;
|
||||
background-color: var(--inactive-color);
|
||||
}
|
||||
|
||||
#progress-completed::-webkit-progress-value {
|
||||
background-color: var(--foreground);
|
||||
}
|
||||
|
||||
#progress-completed::-moz-progress-bar {
|
||||
background-color: var(--foreground);
|
||||
}
|
||||
|
||||
#progress .iteration-marker {
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 100%;
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
#info {
|
||||
position: absolute;
|
||||
bottom: -25px;
|
||||
left: 60px;
|
||||
right: 60px;
|
||||
height: 12px;
|
||||
color: var(--inactive-color);
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
#info-label {
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
}
|
||||
#info-progress {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
iframe.test-runner {
|
||||
background: var(--running-background);
|
||||
}
|
||||
|
||||
section#summary > #result-number,
|
||||
section#summary > #confidence-number {
|
||||
font-family: "Futura-CondensedMedium", Futura, "Helvetica Neue", Helvetica, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
section#summary > #result-number {
|
||||
text-align: center;
|
||||
font-size: 145px;
|
||||
line-height: 145px;
|
||||
}
|
||||
|
||||
section#summary > #confidence-number {
|
||||
text-align: center;
|
||||
font-size: 36px;
|
||||
line-height: 36px;
|
||||
color: var(--inactive-color);
|
||||
}
|
||||
|
||||
section#details {
|
||||
--viewport-height: max(600px, 90vh);
|
||||
--viewport-width: max(800px, 80vw);
|
||||
}
|
||||
|
||||
section#details .results-table {
|
||||
float: left;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
section#details .export-buttons {
|
||||
justify-content: center;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
section#details .export-buttons button,
|
||||
section#details .export-buttons .button {
|
||||
padding: 5px 10px;
|
||||
min-width: auto;
|
||||
font-size: 14px;
|
||||
margin: 0 3px;
|
||||
}
|
||||
|
||||
section#details .arithmetic-mean {
|
||||
clear: both;
|
||||
padding-top: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
section#details .arithmetic-mean > label {
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
section#details button.show-about {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
section#details h1 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
section#about .section-content > * {
|
||||
width: var(--text-width);
|
||||
padding-left: calc((var(--viewport-width) - var(--text-width)) / 2);
|
||||
}
|
||||
|
||||
section#details:target {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
section#details .all-metric-results {
|
||||
flex: auto;
|
||||
overflow-y: auto;
|
||||
padding: 0px 10px;
|
||||
}
|
||||
|
||||
section#details .arithmetic-mean {
|
||||
clear: both;
|
||||
padding-top: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
section#details .arithmetic-mean > label {
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
section#details h1 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
section#details .metric {
|
||||
margin: 0px 0 10px 0;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
section#details .metric dd {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.submetrics h3 {
|
||||
font-size: medium;
|
||||
}
|
||||
.metrics-table thead .prefix {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.metrics-table thead th {
|
||||
font-weight: normal;
|
||||
text-align: left;
|
||||
border-bottom: 1px var(--foreground) solid;
|
||||
padding: 0 4px 3px 0px;
|
||||
}
|
||||
.metrics-table thead th + th {
|
||||
padding: 0 4px 3px 4px;
|
||||
}
|
||||
|
||||
.nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
table .number {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.chart-legend {
|
||||
display: inline;
|
||||
padding-left: 10px;
|
||||
vertical-align: top;
|
||||
margin-top: 0;
|
||||
line-height: var(--metrics-line-height);
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.chart-legend td {
|
||||
padding: 0 5px 0 0;
|
||||
max-width: calc(var(--viewport-width) * 0.22);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
direction: rtl;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
section#details h1 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
section#about h1 {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 0px;
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
section#about .note {
|
||||
color: var(--inactive-color);
|
||||
}
|
||||
|
||||
.section-grid {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
grid-template-areas:
|
||||
"header"
|
||||
"content"
|
||||
"footer";
|
||||
}
|
||||
|
||||
.section-header {
|
||||
grid-area: header;
|
||||
}
|
||||
.section-content {
|
||||
grid-area: content;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.section-footer {
|
||||
grid-area: footer;
|
||||
}
|
||||
|
||||
.gauge {
|
||||
position: relative;
|
||||
width: 738px;
|
||||
height: 78px;
|
||||
background-image: -webkit-image-set(url("gauge@2x.png") 2x, url("gauge.png") 1x);
|
||||
background-image: image-set(url("gauge@2x.png") 2x, url("gauge.png") 1x);
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.gauge > .window {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 33px;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gauge > .window > .needle {
|
||||
position: absolute;
|
||||
left: 363px;
|
||||
bottom: -88px;
|
||||
width: 4px;
|
||||
height: 400px;
|
||||
background-color: rgb(247, 148, 29);
|
||||
transform: rotate(-70deg);
|
||||
transform-origin: 2px 400px;
|
||||
}
|
||||
|
||||
.all-metric-results .submetrics {
|
||||
display: none;
|
||||
margin-left: 35px;
|
||||
}
|
||||
.all-metric-results .submetrics.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.metric-chart {
|
||||
display: grid;
|
||||
grid-template-columns: calc((var(--viewport-width) - 120px) / 2) 1fr;
|
||||
}
|
||||
|
||||
.metric-chart-relative {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.relative-charts .metric-chart-absolute {
|
||||
display: none;
|
||||
}
|
||||
.relative-charts .metric-chart-relative {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.details-toggle {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
margin-left: -4px;
|
||||
}
|
||||
.details-toggle input {
|
||||
display: inline-block;
|
||||
appearance: none;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
.details-toggle input:after {
|
||||
content: "▶";
|
||||
color: var(--foreground);
|
||||
display: inline-block;
|
||||
transform-origin: 50%;
|
||||
position: relative;
|
||||
transition: all 250ms ease;
|
||||
}
|
||||
.details-toggle input:checked:after {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* Charts (lib/charts.js) */
|
||||
.bar-chart text,
|
||||
.scatter-plot text {
|
||||
font-size: 11px;
|
||||
fill: var(--foreground);
|
||||
dominant-baseline: hanging;
|
||||
}
|
||||
.bar-chart .axis,
|
||||
.scatter-plot .axis {
|
||||
stroke: #aaa;
|
||||
}
|
||||
.bar-chart .minMax {
|
||||
stroke: var(--foreground);
|
||||
stroke-width: 1;
|
||||
stroke-opacity: 0.5;
|
||||
stroke-dasharray: 2;
|
||||
}
|
||||
.bar-chart .mean {
|
||||
stroke: var(--foreground);
|
||||
stroke-width: 2;
|
||||
stroke-opacity: 0.5;
|
||||
}
|
||||
.bar-chart .mean:hover {
|
||||
stroke-opacity: 1;
|
||||
}
|
||||
.bar-chart g.bar {
|
||||
fill: #8ad;
|
||||
}
|
||||
.bar-chart g.bar.large text {
|
||||
display: none;
|
||||
}
|
||||
.bar-chart g.bar:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
.bar-chart g.bar text {
|
||||
fill: #ddd;
|
||||
text-anchor: middle;
|
||||
text-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.bar-chart g.bar:hover text {
|
||||
display: block;
|
||||
fill: #fff;
|
||||
text-shadow: 0 0 5px #000;
|
||||
}
|
||||
.bar-chart .label {
|
||||
text-anchor: end;
|
||||
dominant-baseline: middle;
|
||||
}
|
||||
|
||||
.scatter-plot .marker {
|
||||
stroke-width: 0px;
|
||||
opacity: 0.8;
|
||||
fill: currentColor;
|
||||
cursor: crosshair;
|
||||
}
|
||||
.scatter-plot .marker:hover {
|
||||
opacity: 1;
|
||||
filter: drop-shadow(0 0 2px currentColor);
|
||||
}
|
||||
.scatter-plot .percentile {
|
||||
fill: currentColor;
|
||||
fill-opacity: 0.3;
|
||||
stroke-width: 0px;
|
||||
}
|
||||
.scatter-plot .percentile line {
|
||||
stroke-width: 1px;
|
||||
}
|
||||
.scatter-plot .percentile:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Chart colors */
|
||||
.chart .blue {
|
||||
color: #5b9aff;
|
||||
stroke: #5b9aff;
|
||||
}
|
||||
|
||||
.chart .blue-light {
|
||||
color: #5bd6ff;
|
||||
stroke: #5bd6ff;
|
||||
}
|
||||
|
||||
.chart .green-light {
|
||||
color: #73d147;
|
||||
stroke: #73d147;
|
||||
}
|
||||
|
||||
.chart .green {
|
||||
color: #94bc4b;
|
||||
stroke: #94bc4b;
|
||||
}
|
||||
|
||||
.chart .yellow {
|
||||
color: #ded300;
|
||||
stroke: #ded300;
|
||||
}
|
||||
|
||||
.chart .orange {
|
||||
color: #fe8f06;
|
||||
stroke: #fe8f06;
|
||||
}
|
||||
|
||||
.chart .red {
|
||||
color: #de4040;
|
||||
stroke: #de4040;
|
||||
}
|
||||
|
||||
.chart .magenta {
|
||||
color: #cd1e90;
|
||||
stroke: #cd1e90;
|
||||
}
|
||||
|
||||
.chart .violet {
|
||||
color: #a41ecd;
|
||||
stroke: #a41ecd;
|
||||
}
|
||||
|
||||
.chart .purple {
|
||||
color: #7b3eff;
|
||||
stroke: #7b3eff;
|
||||
}
|
||||
|
||||
.chart .blue-dark {
|
||||
color: #2d4ef7;
|
||||
stroke: #2d4ef7;
|
||||
}
|
||||
|
||||
.chart .green-dark {
|
||||
color: #349a58;
|
||||
stroke: #349a58;
|
||||
}
|
||||
|
||||
.chart .ochre {
|
||||
color: #cc9900;
|
||||
stroke: #cc9900;
|
||||
}
|
||||
|
||||
.chart .rust {
|
||||
color: #cc5200;
|
||||
stroke: #cc5200;
|
||||
}
|
||||
|
||||
.chart .white {
|
||||
color: #fff;
|
||||
stroke: #fff;
|
||||
}
|
||||
355
resources/main.mjs
Normal file
|
|
@ -0,0 +1,355 @@
|
|||
import { BenchmarkRunner } from "./benchmark-runner.mjs";
|
||||
import * as Statistics from "./statistics.mjs";
|
||||
import { Suites } from "./tests.mjs";
|
||||
import { renderMetricView } from "./metric-ui.mjs";
|
||||
import { params } from "./params.mjs";
|
||||
import { createDeveloperModeContainer } from "./developer-mode.mjs";
|
||||
|
||||
// FIXME(camillobruni): Add base class
|
||||
class MainBenchmarkClient {
|
||||
developerMode = false;
|
||||
stepCount = null;
|
||||
suitesCount = null;
|
||||
_measuredValuesList = [];
|
||||
_finishedTestCount = 0;
|
||||
_progressCompleted = null;
|
||||
_isRunning = false;
|
||||
_hasResults = false;
|
||||
_developerModeContainer = null;
|
||||
_metrics = Object.create(null);
|
||||
|
||||
constructor() {
|
||||
window.addEventListener("DOMContentLoaded", () => this.prepareUI());
|
||||
this._showSection(window.location.hash);
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this._startBenchmark())
|
||||
this._showSection("#running");
|
||||
}
|
||||
|
||||
_startBenchmark() {
|
||||
if (this._isRunning)
|
||||
return false;
|
||||
|
||||
if (Suites.every((suite) => suite.disabled)) {
|
||||
const message = `No suites selected - "${params.suites}" does not exist.`;
|
||||
alert(message);
|
||||
console.error(
|
||||
message,
|
||||
params.suites,
|
||||
"\nValid values:",
|
||||
Suites.map((each) => each.name)
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
this._developerModeContainer?.remove();
|
||||
this._progressCompleted = document.getElementById("progress-completed");
|
||||
if (params.iterationCount < 50) {
|
||||
const progressNode = document.getElementById("progress");
|
||||
for (let i = 1; i < params.iterationCount; i++) {
|
||||
const iterationMarker = progressNode.appendChild(document.createElement("div"));
|
||||
iterationMarker.className = "iteration-marker";
|
||||
iterationMarker.style.left = `${(i / params.iterationCount) * 100}%`;
|
||||
}
|
||||
}
|
||||
|
||||
this._metrics = Object.create(null);
|
||||
this._isRunning = true;
|
||||
|
||||
const enabledSuites = Suites.filter((suite) => !suite.disabled);
|
||||
const totalSubtestsCount = enabledSuites.reduce((testsCount, suite) => {
|
||||
return testsCount + suite.tests.length;
|
||||
}, 0);
|
||||
this.stepCount = params.iterationCount * totalSubtestsCount;
|
||||
this._progressCompleted.max = this.stepCount;
|
||||
this.suitesCount = enabledSuites.length;
|
||||
const runner = new BenchmarkRunner(Suites, this);
|
||||
runner.runMultipleIterations(params.iterationCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
get metrics() {
|
||||
return this._metrics;
|
||||
}
|
||||
|
||||
willAddTestFrame(frame) {
|
||||
frame.style.left = "50%";
|
||||
frame.style.top = "50%";
|
||||
frame.style.transform = "translate(-50%, -50%)";
|
||||
}
|
||||
|
||||
willRunTest(suite, test) {
|
||||
document.getElementById("info-label").textContent = suite.name;
|
||||
document.getElementById("info-progress").textContent = `${this._finishedTestCount} / ${this.stepCount}`;
|
||||
}
|
||||
|
||||
didRunTest() {
|
||||
this._finishedTestCount++;
|
||||
this._progressCompleted.value = this._finishedTestCount;
|
||||
}
|
||||
|
||||
didRunSuites(measuredValues) {
|
||||
this._measuredValuesList.push(measuredValues);
|
||||
}
|
||||
|
||||
willStartFirstIteration() {
|
||||
this._measuredValuesList = [];
|
||||
this._finishedTestCount = 0;
|
||||
}
|
||||
|
||||
didFinishLastIteration(metrics) {
|
||||
console.assert(this._isRunning);
|
||||
this._isRunning = false;
|
||||
this._hasResults = true;
|
||||
this._metrics = metrics;
|
||||
|
||||
const scoreResults = this._computeResults(this._measuredValuesList, "score");
|
||||
this._updateGaugeNeedle(scoreResults.mean);
|
||||
document.getElementById("result-number").textContent = scoreResults.formattedMean;
|
||||
if (scoreResults.formattedDelta)
|
||||
document.getElementById("confidence-number").textContent = `\u00b1 ${scoreResults.formattedDelta}`;
|
||||
|
||||
this._populateDetailedResults(metrics);
|
||||
|
||||
if (params.developerMode)
|
||||
this.showResultsDetails();
|
||||
else
|
||||
this.showResultsSummary();
|
||||
}
|
||||
|
||||
_computeResults(measuredValuesList, displayUnit) {
|
||||
function valueForUnit(measuredValues) {
|
||||
if (displayUnit === "ms")
|
||||
return measuredValues.geomean;
|
||||
return measuredValues.score;
|
||||
}
|
||||
|
||||
function sigFigFromPercentDelta(percentDelta) {
|
||||
return Math.ceil(-Math.log(percentDelta) / Math.log(10)) + 3;
|
||||
}
|
||||
|
||||
function toSigFigPrecision(number, sigFig) {
|
||||
const nonDecimalDigitCount = number < 1 ? 0 : Math.floor(Math.log(number) / Math.log(10)) + 1;
|
||||
return number.toPrecision(Math.max(nonDecimalDigitCount, Math.min(6, sigFig)));
|
||||
}
|
||||
|
||||
const values = measuredValuesList.map(valueForUnit);
|
||||
const sum = values.reduce((a, b) => {
|
||||
return a + b;
|
||||
}, 0);
|
||||
const arithmeticMean = sum / values.length;
|
||||
let meanSigFig = 4;
|
||||
let formattedDelta;
|
||||
let formattedPercentDelta;
|
||||
const delta = Statistics.confidenceIntervalDelta(0.95, values.length, sum, Statistics.squareSum(values));
|
||||
if (!isNaN(delta)) {
|
||||
const percentDelta = (delta * 100) / arithmeticMean;
|
||||
meanSigFig = sigFigFromPercentDelta(percentDelta);
|
||||
formattedDelta = toSigFigPrecision(delta, 2);
|
||||
formattedPercentDelta = `${toSigFigPrecision(percentDelta, 2)}%`;
|
||||
}
|
||||
|
||||
const formattedMean = toSigFigPrecision(arithmeticMean, Math.max(meanSigFig, 3));
|
||||
|
||||
return {
|
||||
formattedValues: values.map((value) => {
|
||||
return `${toSigFigPrecision(value, 4)} ${displayUnit}`;
|
||||
}),
|
||||
mean: arithmeticMean,
|
||||
formattedMean: formattedMean,
|
||||
formattedDelta: formattedDelta,
|
||||
formattedMeanAndDelta: formattedMean + (formattedDelta ? ` \xb1 ${formattedDelta} (${formattedPercentDelta})` : ""),
|
||||
};
|
||||
}
|
||||
|
||||
_addDetailedResultsRow(table, iterationNumber, value) {
|
||||
const row = document.createElement("tr");
|
||||
const th = document.createElement("th");
|
||||
th.textContent = `Iteration ${iterationNumber + 1}`;
|
||||
const td = document.createElement("td");
|
||||
td.textContent = value;
|
||||
row.appendChild(th);
|
||||
row.appendChild(td);
|
||||
table.appendChild(row);
|
||||
}
|
||||
|
||||
_updateGaugeNeedle(score) {
|
||||
const needleAngle = Math.max(0, Math.min(score, 140)) - 70;
|
||||
const needleRotationValue = `rotate(${needleAngle}deg)`;
|
||||
|
||||
const gaugeNeedleElement = document.querySelector("#summary > .gauge .needle");
|
||||
gaugeNeedleElement.style.setProperty("-webkit-transform", needleRotationValue);
|
||||
gaugeNeedleElement.style.setProperty("-moz-transform", needleRotationValue);
|
||||
gaugeNeedleElement.style.setProperty("-ms-transform", needleRotationValue);
|
||||
gaugeNeedleElement.style.setProperty("transform", needleRotationValue);
|
||||
}
|
||||
|
||||
_populateDetailedResults(metrics) {
|
||||
const trackHeight = 24;
|
||||
document.documentElement.style.setProperty("--metrics-line-height", `${trackHeight}px`);
|
||||
const plotWidth = (params.viewport.width - 120) / 2;
|
||||
document.getElementById("geomean-chart").innerHTML = renderMetricView({
|
||||
metrics: [metrics.Geomean],
|
||||
width: plotWidth,
|
||||
trackHeight,
|
||||
renderChildren: false,
|
||||
colors: ["white"],
|
||||
});
|
||||
|
||||
const toplevelMetrics = Object.values(metrics).filter((each) => !each.parent && each.children.length > 0);
|
||||
document.getElementById("tests-chart").innerHTML = renderMetricView({
|
||||
metrics: toplevelMetrics,
|
||||
width: plotWidth,
|
||||
trackHeight,
|
||||
renderChildren: false,
|
||||
});
|
||||
|
||||
let html = "";
|
||||
for (const metric of toplevelMetrics) {
|
||||
html += renderMetricView({
|
||||
metrics: metric.children,
|
||||
width: plotWidth,
|
||||
trackHeight,
|
||||
title: metric.name,
|
||||
});
|
||||
}
|
||||
document.getElementById("metrics-results").innerHTML = html;
|
||||
|
||||
const filePrefix = `speedometer-3-${new Date().toISOString()}`;
|
||||
let jsonData = this._formattedJSONResult({ modern: false });
|
||||
let jsonLink = document.getElementById("download-classic-json");
|
||||
jsonLink.href = URL.createObjectURL(new Blob([jsonData], { type: "application/json" }));
|
||||
jsonLink.setAttribute("download", `${filePrefix}.json`);
|
||||
|
||||
jsonLink = document.getElementById("download-full-json");
|
||||
jsonData = this._formattedJSONResult({ modern: true });
|
||||
jsonLink.href = URL.createObjectURL(new Blob([jsonData], { type: "application/json" }));
|
||||
jsonLink.setAttribute("download", `${filePrefix}.json`);
|
||||
|
||||
const csvData = this._formattedCSVResult();
|
||||
const csvLink = document.getElementById("download-csv");
|
||||
csvLink.href = URL.createObjectURL(new Blob([csvData], { type: "text/csv" }));
|
||||
csvLink.setAttribute("download", `${filePrefix}.csv`);
|
||||
}
|
||||
|
||||
prepareUI() {
|
||||
window.addEventListener("hashchange", this._hashChangeHandler.bind(this));
|
||||
window.addEventListener("resize", this._resizeScreeHandler.bind(this));
|
||||
this._resizeScreeHandler();
|
||||
|
||||
document.querySelectorAll("logo").forEach((button) => {
|
||||
button.onclick = this._logoClickHandler.bind(this);
|
||||
});
|
||||
document.getElementById("copy-full-json").onclick = this.copyJsonResults.bind(this);
|
||||
document.getElementById("copy-csv").onclick = this.copyCSVResults.bind(this);
|
||||
document.querySelectorAll(".start-tests-button").forEach((button) => {
|
||||
button.onclick = this._startBenchmarkHandler.bind(this);
|
||||
});
|
||||
|
||||
if (params.suites.length > 0 || params.tags.length > 0)
|
||||
Suites.enable(params.suites, params.tags);
|
||||
|
||||
if (params.developerMode) {
|
||||
this._developerModeContainer = createDeveloperModeContainer(Suites);
|
||||
document.body.append(this._developerModeContainer);
|
||||
}
|
||||
|
||||
if (params.startAutomatically)
|
||||
this.start();
|
||||
}
|
||||
|
||||
_hashChangeHandler() {
|
||||
this._showSection(window.location.hash);
|
||||
}
|
||||
|
||||
_resizeScreeHandler() {
|
||||
// FIXME: Detect when the window size changes during the test.
|
||||
const mainSize = document.querySelector("main").getBoundingClientRect();
|
||||
const screenIsTooSmall = window.innerWidth < mainSize.width || window.innerHeight < mainSize.height;
|
||||
document.getElementById("min-screen-width").textContent = `${params.viewport.width + 50}px`;
|
||||
document.getElementById("min-screen-height").textContent = `${params.viewport.height + 50}px`;
|
||||
document.getElementById("screen-size").textContent = `${window.innerWidth}px by ${window.innerHeight}px`;
|
||||
document.getElementById("screen-size-warning").style.display = screenIsTooSmall ? null : "none";
|
||||
}
|
||||
|
||||
_startBenchmarkHandler() {
|
||||
this.start();
|
||||
}
|
||||
|
||||
_logoClickHandler(event) {
|
||||
// Prevent any accidental UI changes during benchmark runs.
|
||||
if (!this._isRunning)
|
||||
this._showSection("#home");
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
showResultsSummary() {
|
||||
this._showSection("#summary");
|
||||
}
|
||||
|
||||
showResultsDetails() {
|
||||
this._showSection("#details");
|
||||
}
|
||||
|
||||
_formattedJSONResult({ modern = false }) {
|
||||
const indent = " ";
|
||||
if (modern)
|
||||
return JSON.stringify(this._metrics, undefined, indent);
|
||||
return JSON.stringify(this._measuredValuesList, undefined, indent);
|
||||
}
|
||||
|
||||
_formattedCSVResult() {
|
||||
// The CSV format is similar to the details view table. Each measurement is a row with
|
||||
// the name and N columns with the measurement for each iteration:
|
||||
// ```
|
||||
// Measurement,#1,...,#N
|
||||
// TodoMVC-JavaScript-ES5/Total,num,...,num
|
||||
// TodoMVC-JavaScript-ES5/Adding100Items,num,...,num
|
||||
// ...
|
||||
const labels = ["Name"];
|
||||
for (let i = 0; i < params.iterationCount; i++)
|
||||
labels.push(`#${i + 1}`);
|
||||
labels.push("Mean");
|
||||
const metrics = Array.from(Object.values(this._metrics)).filter((metric) => !metric.name.startsWith("Iteration-"));
|
||||
const metricsValues = metrics.map((metric) => [metric.name, ...metric.values, metric.mean].join(","));
|
||||
const csv = [labels.join(","), ...metricsValues];
|
||||
|
||||
return csv.join("\n");
|
||||
}
|
||||
|
||||
copyJsonResults() {
|
||||
navigator.clipboard.writeText(this._formattedJSONResult({ modern: true }));
|
||||
}
|
||||
|
||||
copyCSVResults() {
|
||||
navigator.clipboard.writeText(this._formattedCSVResult());
|
||||
}
|
||||
|
||||
_showSection(hash) {
|
||||
if (this._isRunning) {
|
||||
window.location.hash = "#running";
|
||||
return;
|
||||
} else if (this._hasResults) {
|
||||
if (hash !== "#summary" && hash !== "#details") {
|
||||
window.location.hash = "#summary";
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (hash !== "#home" && hash !== "#about") {
|
||||
window.location.hash = "#home";
|
||||
return;
|
||||
}
|
||||
}
|
||||
window.location.hash = hash || "#home";
|
||||
}
|
||||
}
|
||||
|
||||
const rootStyle = document.documentElement.style;
|
||||
rootStyle.setProperty("--viewport-width", `${params.viewport.width}px`);
|
||||
rootStyle.setProperty("--viewport-height", `${params.viewport.height}px`);
|
||||
|
||||
globalThis.benchmarkClient = new MainBenchmarkClient();
|
||||
323
resources/metric-ui.mjs
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
import { Metric } from "./metric.mjs";
|
||||
|
||||
export const COLORS = Object.freeze(["blue", "blue-light", "green-light", "green", "yellow", "orange", "red", "magenta", "violet", "purple", "blue-dark", "green-dark", "ochre", "rust"]);
|
||||
|
||||
export function renderMetricView(viewParams) {
|
||||
let { metrics, width = 500, trackHeight = 20, subMetricMargin = 35, title = "", colors = COLORS } = viewParams;
|
||||
// Make sure subMetricMargin is set for use in renderSubMetrics.
|
||||
viewParams.subMetricMargin = subMetricMargin;
|
||||
const scatterPlotParams = { width, trackHeight, colors };
|
||||
|
||||
scatterPlotParams.xAxisPositiveOnly = false;
|
||||
scatterPlotParams.xAxisShowZero = true;
|
||||
scatterPlotParams.values = prepareScatterPlotValues(metrics, true);
|
||||
scatterPlotParams.unit = "%";
|
||||
scatterPlotParams.xAxisLabel = "Spread Normalized";
|
||||
const normalizedScatterPlot = renderScatterPlot(scatterPlotParams);
|
||||
|
||||
scatterPlotParams.xAxisPositiveOnly = true;
|
||||
scatterPlotParams.xAxisShowZero = false;
|
||||
scatterPlotParams.values = prepareScatterPlotValues(metrics, false);
|
||||
scatterPlotParams.unit = metrics[0].unit;
|
||||
scatterPlotParams.xAxisLabel = metrics[0].unit;
|
||||
const absoluteScatterPlot = renderScatterPlot(scatterPlotParams);
|
||||
|
||||
const legend = metrics
|
||||
.map(
|
||||
(metric, i) => `
|
||||
<tr >
|
||||
<td class="${colors[i % colors.length]} no-select" >●</td>
|
||||
<td class="label">${metric.shortName}</td>
|
||||
<td class="number">${metric.mean.toFixed(2)}</td>
|
||||
<td>±</td>
|
||||
<td>${metric.deltaString}</td>
|
||||
<td>${metric.unit}</td>
|
||||
</tr>`
|
||||
)
|
||||
.join("");
|
||||
return `
|
||||
<dl class="metric">
|
||||
<dt><h3>${title}<h3></dt>
|
||||
<dd>
|
||||
<div class="metric-chart"">
|
||||
<div onclick="document.body.classList.toggle('relative-charts')">
|
||||
<div class="metric-chart-absolute">
|
||||
${absoluteScatterPlot}
|
||||
</div>
|
||||
<div class="metric-chart-relative">
|
||||
${normalizedScatterPlot}
|
||||
</div>
|
||||
</div>
|
||||
<table class="chart chart-legend">${legend}</table>
|
||||
</div>
|
||||
${renderSubMetrics(viewParams)}
|
||||
</dd>
|
||||
</dl>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderSubMetrics(viewParams) {
|
||||
const { metrics, width, subMetricMargin, colors = COLORS, renderChildren = true } = viewParams;
|
||||
const valuesTable = `
|
||||
<label class="details-toggle">
|
||||
<input type="checkbox"
|
||||
onclick="this.parentNode.nextElementSibling.classList.toggle('visible')" />
|
||||
Table
|
||||
</label>
|
||||
<div class="submetrics">
|
||||
${renderMetricsTable(metrics)}
|
||||
</div>`;
|
||||
const hasChildMetric = metrics.length > 0 && metrics[0].children.length > 0;
|
||||
if (!hasChildMetric || !renderChildren)
|
||||
return valuesTable;
|
||||
|
||||
const subMetricWidth = width - subMetricMargin;
|
||||
const childColors = [...colors];
|
||||
|
||||
const subMetrics = metrics
|
||||
.map((metric) => {
|
||||
// Rotate colors to get different colors for sub-plots.
|
||||
for (let i = 0; i < metric.children.length; i++) {
|
||||
const color = childColors.pop();
|
||||
childColors.unshift(color);
|
||||
}
|
||||
const subMetricParams = {
|
||||
...viewParams,
|
||||
parentMetric: metric,
|
||||
metrics: metric.children,
|
||||
title: metric.name,
|
||||
width: subMetricWidth,
|
||||
colors: childColors,
|
||||
};
|
||||
return renderMetricView(subMetricParams);
|
||||
})
|
||||
.join("");
|
||||
return `${valuesTable}
|
||||
<label class="details-toggle">
|
||||
<input type="checkbox"
|
||||
onclick="this.parentNode.nextElementSibling.classList.toggle('visible')" />
|
||||
Submetrics
|
||||
</label>
|
||||
<div class="submetrics">
|
||||
${subMetrics}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderMetricsTable(metrics, min, max) {
|
||||
let numRows = 0;
|
||||
let columnHeaders = "";
|
||||
let commonPrefixes = metrics[0].name.split(Metric.separator);
|
||||
for (const metric of metrics) {
|
||||
const prefixes = metric.name.split(Metric.separator);
|
||||
for (let i = commonPrefixes.length - 1; i >= 0; i--) {
|
||||
if (commonPrefixes[i] !== prefixes[i])
|
||||
commonPrefixes.pop();
|
||||
}
|
||||
|
||||
}
|
||||
const commonPrefix = commonPrefixes.join(Metric.separator);
|
||||
let commonPrefixHeader = "";
|
||||
if (commonPrefix) {
|
||||
commonPrefixHeader = `
|
||||
<tr>
|
||||
<td></td>
|
||||
<td colspan="${metrics.length}" class="prefix">${commonPrefix}</td>
|
||||
</tr>`;
|
||||
}
|
||||
for (const metric of metrics) {
|
||||
const name = metric.name.substring(commonPrefix.length);
|
||||
columnHeaders += `<th>${name} [${metric.unit}]</th>`;
|
||||
numRows = Math.max(metric.values.length, numRows);
|
||||
}
|
||||
|
||||
let body = "";
|
||||
for (let row = 0; row < numRows; row++) {
|
||||
let columns = "";
|
||||
for (const metric of metrics) {
|
||||
const value = metric.values[row];
|
||||
if (value === undefined)
|
||||
continue;
|
||||
const delta = metric.max - metric.min;
|
||||
const percent = Math.max(Math.min((value - metric.min) / delta, 1), 0) * 100;
|
||||
const percentGradient = `background: linear-gradient(90deg, var(--foreground-alpha) ${percent}%, rgba(0,0,0,0) ${percent}%);`;
|
||||
columns += `<td style="${percentGradient}">${value.toFixed(2)}</td>`;
|
||||
}
|
||||
body += `<tr>
|
||||
<td>${row}</td>
|
||||
${columns}
|
||||
</tr>`;
|
||||
}
|
||||
return `<table class="metrics-table" >
|
||||
<thead onclick="this.classList.toggle('nowrap')" >
|
||||
${commonPrefixHeader}
|
||||
<tr>
|
||||
<th>Iteration</th>
|
||||
${columnHeaders}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${body}
|
||||
<tbody>
|
||||
</table>`;
|
||||
}
|
||||
|
||||
function prepareScatterPlotValues(metrics, normalize = true) {
|
||||
let points = [];
|
||||
// Arrange child-metrics values in a single coordinate system:
|
||||
// - metric 1: x values are in range [0, 1]
|
||||
// - metric 2: y values are in range [1, 2]
|
||||
// - ...
|
||||
// This way each metric data point is on a separate track in the scatter
|
||||
// plot.
|
||||
// If normalize == true:
|
||||
// All x values are normalized by the mean of each metric and
|
||||
// centered on 0.
|
||||
// Example: [90ms, 100ms, 110ms] => [-10%, 0%, +10%]
|
||||
const toPercent = 100;
|
||||
let unit;
|
||||
for (let metricIndex = 0; metricIndex < metrics.length; metricIndex++) {
|
||||
const metric = metrics[metricIndex];
|
||||
// If the mean is 0 we can't normalize values properly.
|
||||
const mean = metric.mean || 1;
|
||||
if (!unit)
|
||||
unit = metric.unit;
|
||||
else if (unit !== metric.unit)
|
||||
throw new Error("All metrics must have the same unit.");
|
||||
let width = metric.delta || 1;
|
||||
let center = mean;
|
||||
if (normalize) {
|
||||
width = (metric.delta / mean) * toPercent;
|
||||
center = 0;
|
||||
}
|
||||
const left = center - width / 2;
|
||||
const y = metricIndex;
|
||||
const label = `Mean: ${metric.valueString}\n` + `Min: ${metric.min.toFixed(2)}${unit}\n` + `Max: ${metric.max.toFixed(2)}${unit}`;
|
||||
const rect = [left, y, label, width];
|
||||
// Add data for individual points:
|
||||
points.push(rect);
|
||||
const values = metric.values;
|
||||
const length = values.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
const value = values[i];
|
||||
let x = value;
|
||||
let normalized = (value / mean - 1) * toPercent;
|
||||
if (normalize)
|
||||
x = normalized;
|
||||
const sign = normalized < 0 ? "-" : "+";
|
||||
normalized = Math.abs(normalized);
|
||||
// Each value is mapped to a y-coordinate in the range of [metricIndex, metricIndex + 1]
|
||||
const valueOffsetY = length === 1 ? 0.5 : i / length;
|
||||
const y = metricIndex + valueOffsetY;
|
||||
let label = `Iteration ${i}: ${value.toFixed(3)}${unit}\n` + `Normalized: ${metric.mean.toFixed(3)}${unit} ${sign} ${normalized.toFixed(2)}%`;
|
||||
const point = [x, y, label];
|
||||
points.push(point);
|
||||
}
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
function renderScatterPlot({ values, width = 500, height, trackHeight, xAxisPositiveOnly = false, xAxisShowZero = false, xAxisLabel, unit = "", colors = COLORS }) {
|
||||
if (!height && !trackHeight)
|
||||
throw new Error("Either height or trackHeight must be specified");
|
||||
let xMin = Infinity;
|
||||
let xMax = 0;
|
||||
let yMin = Infinity;
|
||||
let yMax = 0;
|
||||
for (let value of values) {
|
||||
let [x, y] = value;
|
||||
xMin = Math.min(xMin, x);
|
||||
xMax = Math.max(xMax, x);
|
||||
yMin = Math.min(yMin, y);
|
||||
yMax = Math.max(yMax, y);
|
||||
}
|
||||
if (xAxisPositiveOnly)
|
||||
xMin = Math.max(xMin, 0);
|
||||
// Max delta of values across each axis:
|
||||
const trackCount = Math.ceil(yMax - yMin) || 1;
|
||||
const spreadX = xMax - xMin;
|
||||
// Axis + labels height:
|
||||
const axisHeight = 18;
|
||||
const axisMarginY = 4;
|
||||
const markerSize = 5;
|
||||
const trackMargin = 2;
|
||||
// Recalculate height:
|
||||
if (height)
|
||||
trackHeight = (height - axisHeight - axisMarginY) / trackCount;
|
||||
else
|
||||
height = trackCount * trackHeight + axisHeight + axisMarginY;
|
||||
|
||||
// Horizontal axis position:
|
||||
const axisY = height - axisHeight + axisMarginY;
|
||||
const unitToPosX = width / spreadX;
|
||||
const unitToPosY = trackHeight - trackMargin - markerSize / 2;
|
||||
const points = values.map(renderValue).join("");
|
||||
let xAxisZeroLine = "";
|
||||
if (xAxisShowZero) {
|
||||
const xZeroPos = (0 - xMin) * unitToPosX;
|
||||
xAxisZeroLine = `<line x1="${xZeroPos}" x2="${xZeroPos}" y1="${0}" y2="${axisY}" class="axis"/>`;
|
||||
}
|
||||
return `
|
||||
<svg class="scatter-plot chart"
|
||||
width="${width}" height="${height}"
|
||||
viewBox="${`0 0 ${width} ${height}`}">
|
||||
<g class="horizontal-axis no-select">
|
||||
<line
|
||||
x1="${0}" x2="${width}"
|
||||
y1="${axisY - axisMarginY}" y2="${axisY - axisMarginY}"
|
||||
class="axis" />
|
||||
<text y="${axisY}" x="0" text-anchor="start">${xMin.toFixed(2)}${unit}</text>
|
||||
<text y="${axisY}" x="${width / 2}" text-anchor="middle">${xAxisLabel}</text>
|
||||
<text y="${axisY}" x="${width}" text-anchor="end">${xMax.toFixed(2)}${unit}</text>
|
||||
</g>
|
||||
<defs>
|
||||
<g id="marker">
|
||||
<circle r="${markerSize / 2}" />
|
||||
</g>
|
||||
</defs>
|
||||
<g class="values">
|
||||
${xAxisZeroLine}
|
||||
${points}
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
function renderValue(value) {
|
||||
const [rawX, rawY, label, rawWidth = 0] = value;
|
||||
const trackIndex = rawY | 0;
|
||||
const y = (rawY - yMin) * unitToPosY + markerSize * trackIndex;
|
||||
const cssClass = colors[trackIndex % colors.length];
|
||||
|
||||
if (value.length <= 3) {
|
||||
// Render a simple marker:
|
||||
const x = (rawX - xMin) * unitToPosX;
|
||||
const adjustedY = y + markerSize / 2;
|
||||
return `
|
||||
<use href="#marker" x="${x}" y="${adjustedY}" class="marker ${cssClass}">
|
||||
<title>${label}</title>
|
||||
</use>
|
||||
`;
|
||||
} else {
|
||||
// Render a rect with 4 input values:
|
||||
const x = (rawX - xMin) * unitToPosX + rawWidth / 2;
|
||||
const w = rawWidth * unitToPosX;
|
||||
const centerX = x + w / 2;
|
||||
const top = y;
|
||||
const height = trackHeight - trackMargin;
|
||||
const bottom = top + height;
|
||||
return `
|
||||
<g class="percentile ${cssClass}">
|
||||
<rect x="${x}" y="${top}" width="${w}" height="${height}">
|
||||
<title>${label}</title>
|
||||
</rect>
|
||||
<line x1="${x}" x2="${x}" y1="${top}" y2="${bottom}" />
|
||||
<line
|
||||
x1="${centerX}" x2="${centerX}"
|
||||
y1="${top}" y2="${bottom}"
|
||||
stroke-dasharray="${height / 3}" />
|
||||
<line x1="${x + w}" x2="${x + w}" y1="${top}" y2="${bottom}" />
|
||||
</g>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||