Compare commits

...

90 commits

Author SHA1 Message Date
Camillo Bruni
491acc2d64
Update Speedometer2.1 content to latest runner (#348)
Merge the speedometer2.1-content branch with the latest main branch to get the new developer-menu.
2024-01-30 22:44:39 +01:00
Ryosuke Niwa
9365aa2e52 Fix React, Backbone, and jQuery examples 2023-04-27 21:44:41 -07:00
Ryosuke Niwa
b6e979b873 Add a variant of Speedometer 3.0 test harness that runs contents of Speedometer 2.1 2023-04-27 00:16:58 -07:00
Ryosuke Niwa
19ae86221b
Don't specify the base URL as an absolute path. (#148)
This allows the benchmark to be ran inside another directory in a local HTTP server.
2023-04-20 16:51:30 -07:00
Camillo Bruni
08c4983721
Add Metric.separator (#147)
Use / as new metric separator.
2023-04-19 21:50:17 +02:00
Thorsten Kober
10a3acfb62
TodoMVC preact readme update (#143) 2023-04-18 08:20:54 -07:00
Thorsten Kober
75cddea7ce
Todomvc vue 04 - use component for item (#139) 2023-04-12 11:31:00 -07:00
Thorsten Kober
248bfec3c4
Todomvc svelte 01 (#135) 2023-04-11 16:42:36 -07:00
Thorsten Kober
24b447eeb3
Todomvc angular 01 (#129) 2023-04-11 11:02:11 -07:00
Thorsten Kober
03ea073d46
Todomvc vue 03 - Cleanup (#134) 2023-04-11 10:56:21 -07:00
Thorsten Kober
05b6f7a08d
Todomvc vue Step 01 (#114) 2023-04-07 10:22:11 -07:00
Thorsten Kober
ab819fa534
Todomvc jquery update version (#125) 2023-04-07 10:20:42 -07:00
Brian Grinstead
652fe1bc86
Remove TodoMVC-Ember (#128) 2023-04-05 13:06:35 -07:00
Thorsten Kober
9a4cf52cd6
cleanup (#126) 2023-04-03 18:43:03 -07:00
Thorsten Kober
e9565fbb1f
Ember - Step 2 - format from root / cleanup (#119) 2023-03-30 18:13:18 +02:00
Camillo Bruni
5e6adbfe19
Improve graphs (#123)
- Crop negative values in standard view (no functional change over the current version)
- Show 0-line in normalized view
- Format normalized value tooltip better
- Use camelCase for more variables
2023-03-29 09:25:34 +02:00
Brian Grinstead
bb2ea5f183
Add CSV export (#122) 2023-03-28 15:40:42 -07:00
Brian Grinstead
c1ed921716
make details view fill available space (#121) 2023-03-27 09:02:39 -07:00
Thorsten Kober
1d46644e9b
rename todomvc tests (#118) 2023-03-23 16:24:18 -07:00
Thorsten Kober
612f748595
Todomvc styles - consistent versions (#115) 2023-03-23 14:11:20 -07:00
Thorsten Kober
619b96f901
Todomvc ember step 01 (#117) 2023-03-23 11:19:55 -07:00
Thorsten Kober
bd88f95079
TodoMVC - run format from root (#113) 2023-03-23 08:41:23 -07:00
Thorsten Kober
0744a3eb3a
Todomvc preact 02 (#108) 2023-03-16 15:50:42 -07:00
Thorsten Kober
d75589873f
Todomvc react update (#98) 2023-03-16 15:21:22 -07:00
Thorsten Kober
1afa78682d
Todomvc react redux update (#99) 2023-03-16 15:08:34 -07:00
Camillo Bruni
2e4baeefc4
adding favicon (#111)
Using the the "o" from the speedometer logo.
2023-03-16 20:50:14 +01:00
Camillo Bruni
90562b145f
Use "Enter" instead of "ENTER" for runner KeyboardEvent (#110) 2023-03-16 19:10:12 +01:00
Camillo Bruni
6d04874961
fixing iteration (#109)
- Don't calculate standard deviation for single values
- Center single data points on scatter plots to fix track alignment

This resolves #107
2023-03-16 16:29:24 +01:00
Brian Grinstead
6c31329c83
Emit start-suite and end-suite performance marks outside of the timed region (#106) 2023-03-15 16:52:38 -07:00
Thorsten Kober
1933672b81
Todomvc preact - Phase 1 (#104) 2023-03-15 08:27:53 -07:00
Thorsten Kober
09210bc05e
Todomvc backbone update (#101) 2023-03-14 10:29:04 -07:00
Camillo Bruni
6523f66497
Use Metrics Visualisations from GrandPrix (#42)
- Use scatter-plots for each metrics and submetrcs
- Show tables for each metric
- Use more CSS color variables
- Update scrollbar to match existing color scheme
2023-03-13 14:43:47 +01:00
Camillo Bruni
359605012f
Allow custom section sizes (#92)
This PR makes each runner section independent to allow custom sizes.
This is in preparation for PR #42 to allow more space for the details view.

- Move border layout from `main` to each `section`
- Copy logo into every `section`
- Use grid-layout for header/content/footer in about and details view
2023-03-13 11:18:32 +01:00
Brian Grinstead
f16557016d
Two new tentative editor workloads (#81) 2023-03-10 13:22:44 -08:00
Thorsten Kober
354d5039c4
adjust eslint (#95) 2023-03-10 11:15:46 -08:00
Thorsten Kober
2c78996286
Todomvc es6 webpack updates (#85) 2023-03-09 08:20:13 -08:00
Camillo Bruni
a20855e4a8
Fixing test (#93)
I've accidentally broken the tests by refactoring runner methods.
2023-03-09 17:16:09 +01:00
Thorsten Kober
52d094dd0e
Todomvc es6 updates (#84) 2023-03-09 07:22:32 -08:00
Camillo Bruni
ce1b63420f
Use window.location.hash for navigation (#91)
- Remove history state 
- Prevent accidentally restarting benchmark in `startBenchmark()`
- Add "home" link to about page
- Add explicit logic in `_showSection(...)` for selecting the allowed views
- Use a-href that point to the shown section
- Use CSS rules to show/hide sections (in preparation for making the details full-width)

Visible change: All urls now have a explicit hash (default is `#home`)
2023-03-09 09:40:19 +01:00
Camillo Bruni
ddd75e2206
Improve runner (#90)
- Pre-allocate all mark-label strings to avoid potential gc's during sensitive measurements
- Move code to new separate `_recordTestResults` method in preparation for pr #28
2023-03-09 09:21:51 +01:00
Brian Grinstead
999f4f676b
Initial dev mode param implementation (#89) 2023-03-08 20:33:42 -08:00
Thorsten Kober
33b5e9ae76
Todomvc es5 updates (#82) 2023-03-07 14:58:05 -08:00
Thorsten Kober
6f683630ef
use consistent names (#87) 2023-03-07 13:57:27 -08:00
Camillo Bruni
a068b53a7e
Fix score view (#86)
- Use params directly in MainBenchmarkClient
- Remove params.unit
- Update summary only with score
- Update details view with ms value
- Fix gauge background on chrome by also adding `-webkit-image-set(..)`
2023-03-07 22:03:09 +01:00
Brian Grinstead
24b094d1cc
Use performance.measure api instead of manually tracking with performance.now() (#79) 2023-02-27 12:53:55 -08:00
Thorsten Kober
d8bea6b8d4
remove labs (#80) 2023-02-23 13:32:10 -08:00
Thorsten Kober
bbec60788f
update readme for todoMVC apps (#78) 2023-02-21 15:45:19 -08:00
Camillo Bruni
e884d518b6
Fix the last broken test using _writeMark (#77) 2023-02-21 20:39:28 +01:00
Camillo Bruni
0644776001
Expose frozen globalThis.Suites (#72) 2023-02-21 20:10:16 +01:00
Camillo Bruni
c64ffc8eaa
Remove _writeMark test (#76)
The method has been removed since we're now using performance.now directly.
2023-02-21 20:06:57 +01:00
Camillo Bruni
661426329a
Use params everywhere (#69)
- Add support for filtering multiple suites
- Support startAutomatically in the default runner
- Fix displaying detailed results with unit=ms
- More error messages on invalid url-params
- Add Suites.enable function instead of two separate implementations per runner
2023-02-21 20:05:28 +01:00
Camillo Bruni
cb6cc0652f
Update titles to "Speedometer 3.0" (#75) 2023-02-21 19:56:47 +01:00
Camillo Bruni
d309edfe9d
Minor runner improvements (#74)
- Directly use performance.now and performance.mark
- Reduce a bit of overhead
- Use separate variables for sync / async time measurements to make the code more clear
- Optimize _appendFrame() code by using local variables
2023-02-21 19:52:51 +01:00
Camillo Bruni
207416957e
Add --highlight color CSS variable (#73) 2023-02-21 19:45:00 +01:00
Camillo Bruni
f4eb1ebaa5
Use 2x logo and gauge images (#70)
Make sure we load the hi-res background images in all cases:
- Use srcset for logo
- Use image-set for gauge CSS
2023-02-19 20:48:03 +01:00
Thorsten Kober
bf927da6ba
Remove wait from selenium test (#67) 2023-02-13 19:36:18 -08:00
Thorsten Kober
2fdc20cc2e
Adding prefer-template rule to Eslint (#66)
* run format

* eslint prefer-template

* re-format spacing

* fix string concat

* pr feedback

* rebase main
2023-02-13 11:54:51 -08:00
Camillo Bruni
f20363a3c4
Add buttons to download and copy JSON results (#65)
- Add "Download JSON" button to details view
- Add "Copy JSON" button to details view
- Example JSON result file name: "speedometer_3-2023-02-13T09_18_32.318Z.json"
- Update CSS formatting to make details button all the same width
- Add "title" to results buttons (both summary and details)
2023-02-13 20:44:44 +01:00
Camillo Bruni
1b06e553f9
Use GrandPrix Metric model (#63)
- Use more explicit Metric objects with parent and child-metrics
- Update statistics.mjs to use more ES6 features
2023-02-13 09:44:04 +01:00
Camillo Bruni
7722c8ab06
Prepare layout for more complex details view (#62)
- Use buttons for "about" links
- Make sure about text is readable on all view sizes (adding optional scrollbar)
- Add button-row to allow for more complex buttons in the upcoming details view (see PR "Use Metrics classes from GrandPrix" #42 )
- Add `--foreground-color` and `--text-width` CSS variables
- Hide screen-size warning on startup to avoid flickering
- Fix main view centering by using more detailed calc(...)
2023-02-11 13:35:30 +01:00
Brian Grinstead
0b592ff361
Update LICENSE (#64) 2023-02-10 20:09:27 -08:00
Camillo Bruni
6fcb6e6359
Make progress labels more readable (#60)
- Move current suite label to the left
- Move suite progress to the right
2023-02-09 10:44:10 +01:00
Camillo Bruni
76f4bb1e86
Use Score instead of Runs / Minute (#61)
RPM has always been a bit a confusing term in Speedometer2 as it was never real rpms, rather a scaled value.
To avoid future confusion it is renamed to the more neutral term "Score".
2023-02-09 09:42:13 +01:00
Thorsten Kober
4c22ba45f6
Add Eslint & Prettier (#57) 2023-02-08 13:53:53 -08:00
Camillo Bruni
480c9cee0f
Add command line options to tests/run.mjs (#58)
- Add basic command line options
- Improve error messages in case $BROWSER env variables isn't set
2023-02-07 22:08:31 +01:00
Camillo Bruni
00db41cea3
Add customisable viewport size (#41)
- Introduce params.mjs module that encodes all benchmarking parameters
- Change CSS to rely on --viewport-width and --viewport-height variables for easier resizing
- Update code and tests to rely on the newly added Params object
2023-02-07 22:07:02 +01:00
Thorsten Kober
42b6dd8a8d
Dev setup (#55) 2023-01-30 14:52:46 -08:00
Brian Grinstead
dbff607c76
cast iterationCount from url to number in interactive runner. Fixes #46 (#48) 2023-01-30 09:42:32 -08:00
Brian Grinstead
9566af6fe6
Run mocha test in CI using selenium (#45) 2023-01-26 14:56:57 -08:00
Brian Grinstead
69d5d5bf75
Remove the following tests, as decided in issue #3: (#49)
- EmberJS-Debug
- AngularJS (1)
- Inferno
- Elm
- Flight
2023-01-26 13:37:14 -08:00
Daniel Holbert
c0a1bc139b
Fix typo in CSS property name, in InteractiveRunner.html (#47)
InteractiveRunner.html has a CSS rule which includes a "list-position" property, which is not actually the name of a property.

The intent was surely to use "list-style-position".  Let's just correct it to that.
2023-01-26 13:01:20 -08:00
Houssein Djirdeh
adc3cca860
Adds unit tests for BenchmarkRunner using Mocha (#40) 2023-01-26 09:06:59 -08:00
Doug Thayer
2550aebedb
Unbreak react-stockcharts tests (#44)
I blundered my style fixups and unthinkingly pushed them up without
proper testing.
2023-01-25 22:15:05 -08:00
Doug Thayer
ed6143a8cc
Add react-stockcharts benchmarks (#11)
* Add react-stockcharts tests

* fixup React-Stockcharts for new runner

* Add back React-Stockcharts-SVG

* Added react-stockcharts under tentative directory

* Fix code style of react-stockcharts tests

* Fix up loop index variables
2023-01-25 12:26:17 -08:00
Camillo Bruni
ef8d58c988
Fix interactive runner (#31)
* Make the client callbacks async
* Move the interactive runner into a separate interactive.mjs module
* Update code to use URLSearchParams instead of custom parsing
2023-01-25 13:10:55 +01:00
Camillo Bruni
dce4dfeaa0
Add more Page methods and PageElement (#26)
Migrate all test code to go through Page or PageElement.

- Page defines a fixed interface to interact with a test frame
- PageElement defines a fixed interface to interact with Elements from an iframe

Cleanups:
- Move app initialization and custom code to the specific index.html files
- Update `event.initEvent(...)` code to use the recommended `Event` constructor
- Firefox still requires `initEvent` for the submit events
2022-11-30 19:24:46 +01:00
Camillo Bruni
714b5b4cb2
move methods (#29)
Sort BenchmarRunner run methods in chronological order.
2022-11-23 22:51:52 +01:00
Camillo Bruni
731c9e871d
Async BenchmarkRunner and removing BenchmarkState (#27)
Replace the continuation-based test running with an async-await loop over all suites and tests.
This also removes the necessity of the BenchmarkState class.
2022-11-23 21:07:54 +01:00
Camillo Bruni
e1125d91bc
Add Page object (#23)
Goal: Add a fixed interface to interact with test frames and their DOM nodes.
This will allow us to run more complex scenarios in the future without sacrificing flexibility or performance for the existing tests.

- Add `Page` object
- Refactor tests to interact with the frame only through the Page object
2022-11-22 13:59:43 +01:00
Brian Grinstead
52998e962d
Add development notice (#25) 2022-11-03 16:24:07 -07:00
Camillo Bruni
b840b30f7a
Remove flight-js mailclient (#22)
Remove the flightjs-mailclient app that was not included in the main Suites list.
2022-10-31 18:05:57 +01:00
Camillo Bruni
8787833334
Convert to ES6 Modules (#19)
- Rename files from .js to .mjs (not strictly necessary but it makes clear that we use modules)
- Pull in missing statistics.js file from https://github.com/WebKit/WebKit/blob/main/PerformanceTests/resources/statistics.js
- Restructure UI init code to be all in main.mjs
- Moved free-floating UI code to MainBenchmarkClient
- Added `_isRunning` instance variable to MainBenchmarkClient
- Fix CSS to address removed links
- Use `<button>` instead of links for better keyboard navigation in all browsers
2022-10-31 10:28:25 +01:00
Ryosuke Niwa
1e6af3de5f
Fix indentation after class conversion. (#18) 2022-09-19 10:54:38 -07:00
Camillo Bruni
281b94e7ae
Use ES6 Classes (#17) 2022-09-16 16:05:22 -07:00
Camillo Bruni
694d89c47e
Basic ES6 conversions: let/const and arrow functions (#16)
This is the first, format and function preserving PR to start the ES6.

- Convert var to let/const
- Use arrow functions where possible
2022-09-16 09:53:24 +02:00
Ryosuke Niwa
600ccf0e64
Delete SimplePromise and use regular native Promise. (#14)
Also deleted the unused _waitAndWarmUp.
2022-08-26 13:04:25 -07:00
Brian Grinstead
9bae5f5ef1
Add readme with goals for the benchmark (#13) 2022-08-23 20:37:48 -07:00
Ryosuke Niwa
3ceae4f0e9
Add Speedometer Governance Policy (#10) 2022-07-28 20:09:12 -07:00
Ryosuke Niwa
6b63846bad
Reviewed by Addy Osmani and Jonathan Bedard. (#2)
Updated the copyright.

Apple started contribution in 2013: http://commits.webkit.org/r156073.
Google contributed in 2017: https://commits.webkit.org/r216706 through https://commits.webkit.org/r222534.

* LICENSE
2022-06-29 11:40:19 -07:00
Ryosuke Niwa
a1e842164b
Add Speedometer 2.1 content. (#1)
Reviewed by Yusuke Suzuki, Addy Osmani, and Julian Descottes.

* InteractiveRunner.html: Added.
* index.html Added.
* resources: Added.
2022-06-06 08:50:03 -07:00
596 changed files with 321294 additions and 1 deletions

50
.eslintignore Normal file
View 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
View 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
View 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
View file

@ -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
View file

@ -0,0 +1 @@
engine-strict = true

50
.prettierignore Normal file
View 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
View 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
View 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
View 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
View 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>

View file

@ -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
View 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
View 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
View file

@ -0,0 +1,10 @@
{
"plugins": [
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
]
]
}

181
index.html Normal file
View 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

File diff suppressed because it is too large Load diff

51
package.json Normal file
View 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"
}
}

View 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();
}
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View 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.

View file

@ -0,0 +1,5 @@
# Flight example app
[![Build Status](https://travis-ci.org/flightjs/example-app.png?branch=master)](http://travis-ci.org/flightjs/example-app)
An example Flight application.

View 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;
}
);

View file

@ -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);
});
}
}
);

View file

@ -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);
});
}
}
);

View 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);
});
}
}
);

View 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
});
});
}
}
);

View 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);
});
}
}
);

View file

@ -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);
});
}
}
);

View file

@ -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');
});
}
}
);

View file

@ -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);
});
}
}
);

View file

@ -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();
});
}
}
);

View 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;
}

View 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;
}
);

View 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>&nbsp;</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
}
}
);

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,2 @@
node_modules
.DS_Store

View 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

View file

@ -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

View 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.

View 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.

File diff suppressed because one or more lines are too long

View 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;
};
}
});

View 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})});

View 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);
};
});

View 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)}});

View file

@ -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"
}
}

View file

@ -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'
]);

View file

@ -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;
}
})
});

View file

@ -0,0 +1,3 @@
function implement() {
throw 'Not implemented';
}

View file

@ -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>

View file

@ -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;
};

View file

@ -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;
}

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 905 B

View file

@ -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 '&nbsp;'),
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');
};
}
}());

File diff suppressed because it is too large Load diff

View file

@ -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));
})
});
});

View file

@ -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);
});
});
});

View file

@ -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);
});
});
});

View file

@ -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);
});
});
});

View file

@ -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"

View file

@ -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;
}
);

View file

@ -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;
}
);

View file

@ -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
};
}
);

View file

@ -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
};
}
);

View file

@ -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;
}
);

View 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;
}
);

View 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;
}
);

View file

@ -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
}
};
}
);

View file

@ -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.

View file

@ -0,0 +1,145 @@
# jasmine-flight [![Build Status](https://travis-ci.org/flightjs/jasmine-flight.png?branch=master)](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

View file

@ -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 [![Build Status](https://travis-ci.org/flightjs/jasmine-flight.png?branch=master)](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"
}
}

View file

@ -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));

View file

@ -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()
})

View file

@ -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"
}
}

View file

@ -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"
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View 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 = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': '&quot;',
"'": '&#39;',
"/": '&#x2F;'
};
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;
}())));

File diff suppressed because it is too large Load diff

View 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>

View 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;

View 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"
}
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
resources/gauge@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

213
resources/interactive.mjs Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

BIN
resources/logo@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

792
resources/main.css Normal file
View 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
View 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
View 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>
`;
}
}
}

Some files were not shown because too many files have changed in this diff Show more