diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 00000000..3a5cc31a --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,101 @@ +# Contributing | AngularFire + +Thank you for contributing to the Firebase community! + + - [Have a usage question?](#question) + - [Think you found a bug?](#issue) + - [Have a feature request?](#feature) + - [Want to submit a pull request?](#submit) + - [Need to get set up locally?](#local-setup) + + +## Have a usage question? + +We get lots of those and we love helping you, but GitHub is not the best place for them. Issues +which just ask about usage will be closed. Here are some resources to get help: + +- Start with the [quickstart](/docs/quickstart.md) +- Go through the [guide](/docs/guide/README.md) +- Read the full [API reference](/docs/reference.md) +- Try out some [examples](/README.md#examples) + +If the official documentation doesn't help, try asking a question on the +[Firebase Google Group](https://groups.google.com/forum/#!forum/firebase-talk) or one of our +other [official support channels](https://firebase.google.com/support/). + +**Please avoid double posting across multiple channels!** + + +## Think you found a bug? + +Yeah, we're definitely not perfect! + +Search through [old issues](https://github.com/firebase/angularfire/issues) before submitting a new +issue as your question may have already been answered. + +If your issue appears to be a bug, and hasn't been reported, +[open a new issue](https://github.com/firebase/angularfire/issues/new). Please use the provided bug +report template and include a minimal repro. + +If you are up to the challenge, [submit a pull request](#submit) with a fix! + + +## Have a feature request? + +Great, we love hearing how we can improve our products! After making sure someone hasn't already +requested the feature in the [existing issues](https://github.com/firebase/angularfire/issues), go +ahead and [open a new issue](https://github.com/firebase/angularfire/issues/new). Feel free to remove +the bug report template and instead provide an explanation of your feature request. Provide code +samples if applicable. Try to think about what it will allow you to do that you can't do today? How +will it make current workarounds straightforward? What potential bugs and edge cases does it help to +avoid? + + +## Want to submit a pull request? + +Sweet, we'd love to accept your contribution! [Open a new pull request](https://github.com/firebase/angularfire/pull/new/master) +and fill out the provided form. + +**If you want to implement a new feature, please open an issue with a proposal first so that we can +figure out if the feature makes sense and how it will work.** + +Make sure your changes pass our linter and the tests all pass on your local machine. We've hooked +up this repo with continuous integration to double check those things for you. + +Most non-trivial changes should include some extra test coverage. If you aren't sure how to add +tests, feel free to submit regardless and ask us for some advice. + +Finally, you will need to sign our [Contributor License Agreement](https://cla.developers.google.com/about/google-individual) +before we can accept your pull request. + + +## Need to get set up locally? + +If you'd like to contribute to AngularFire, you'll need to do the following to get your environment +set up. + +### Install Dependencies + +```bash +$ git clone https://github.com/firebase/angularfire.git +$ cd angularfire # go to the angularfire directory +$ npm install -g grunt-cli # globally install grunt task runner +$ npm install # install local npm build / test dependencies +$ grunt install # install Selenium server for end-to-end tests +``` + +### Lint, Build, and Test + +```bash +$ grunt # lint, build, and test + +$ grunt build # lint and build + +$ grunt test # run unit and e2e tests +$ grunt test:unit # run unit tests +$ grunt test:e2e # run e2e tests (via Protractor) + +$ grunt watch # lint, build, and test whenever source files change +``` + +The output files - `angularfire.js` and `angularfire.min.js` - are written to the `/dist/` directory. diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..c3200067 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,69 @@ + + + +### Version info + + + +**Angular:** + +**Firebase:** + +**AngularFire:** + +**Other (e.g. Node, browser, operating system) (if applicable):** + +### Test case + + + + +### Steps to reproduce + + + + +### Expected behavior + + + + +### Actual behavior + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..80efa774 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,31 @@ + + + +### Description + + + +### Code sample + + diff --git a/.gitignore b/.gitignore index 7f7cf59b..a6759600 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ -bower_components/ +dist/ node_modules/ bower_components/ -selenium/ -.idea \ No newline at end of file +tests/coverage/ + +.idea diff --git a/.jshintrc b/.jshintrc index 3db69b61..c3c48f86 100644 --- a/.jshintrc +++ b/.jshintrc @@ -1,22 +1,19 @@ { - "bitwise" : true, - "boss" : true, - "browser" : true, - "curly" : true, - "devel" : true, - "eqnull" : true, - "globals" : { - "angular" : false, - "Firebase" : false, - "FirebaseSimpleLogin" : false - }, - "globalstrict" : true, - "indent" : 2, - "latedef" : true, - "maxlen" : 115, - "noempty" : true, - "nonstandard" : true, - "undef" : true, - "unused" : true, - "trailing" : true + "predef": [ + "angular", + "firebase" + ], + "bitwise": true, + "browser": true, + "curly": true, + "forin": true, + "indent": 2, + "latedef": true, + "node": true, + "noempty": true, + "nonbsp": true, + "strict": true, + "trailing": true, + "undef": true, + "unused": true } diff --git a/.travis.yml b/.travis.yml index 84eae5d6..5afad1f9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,18 +1,25 @@ language: node_js node_js: - - "0.10" -branches: - only: - - master +- stable +sudo: false +addons: + sauce_connect: true +before_install: +- export CHROME_BIN=chromium-browser +- export DISPLAY=:99.0 +- sh -e /etc/init.d/xvfb start install: - - git clone git://github.com/n1k0/casperjs.git ~/casperjs - - export PATH=$PATH:~/casperjs/bin - - npm install -g grunt-cli - - npm install -g bower - - npm install - - bower install +- git clone git://github.com/n1k0/casperjs.git ~/casperjs +- export PATH=$PATH:~/casperjs/bin +- npm install -g grunt-cli +- npm install before_script: - - phantomjs --version - - casperjs --version +- grunt install script: - - grunt +- sh ./tests/travis.sh +after_script: +- cat ./tests/coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js +env: + global: + - secure: mGHp1rQI11OvbBQn3PnBT5kuyo26gFl8U+nNq0Ot4opgSBX9JaHqS8Dx63uALWWU9qjy08/Mn68t/sKhayH1+XrPDIenOy/XEkkSAG60qAAowD9dRo3WaIMSOcWWYDeqdZOAWZ3LiXvjLO4Swagz5ejz7UtY/ws4CcTi2n/fp7c= + - secure: Eao+hPFWKrHb7qUGEzLg7zdTCE//gb3arf5UmI9Z3i+DydSu/AwExXuywJYUj4/JNm/z8zyJ3j1/mdTyyt9VVyrnQNnyGH1b2oCUHkrs1NLwh5Oe4YcqUYROzoEKdDInvmjVJnIfUEM07htGMGvsLsX4MW2tqVHvD2rOwkn8C9s= diff --git a/Gruntfile.js b/Gruntfile.js index 57401d84..ecd819c8 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,138 +1,163 @@ /* global module */ - module.exports = function(grunt) { 'use strict'; grunt.initConfig({ - exec: { - casperjs : { - command : 'casperjs test tests/e2e/' + pkg: grunt.file.readJSON('package.json'), + meta: { + banner: '/*!\n' + + ' * AngularFire is the officially supported AngularJS binding for Firebase. Firebase\n' + + ' * is a full backend so you don\'t need servers to build your Angular app. AngularFire\n' + + ' * provides you with the $firebase service which allows you to easily keep your $scope\n' + + ' * variables in sync with your Firebase backend.\n' + + ' *\n' + + ' * AngularFire 0.0.0\n' + + ' * https://github.com/firebase/angularfire/\n' + + ' * Date: <%= grunt.template.today("mm/dd/yyyy") %>\n' + + ' * License: MIT\n' + + ' */\n' + }, + + // merge files from src/ into angularfire.js + concat: { + app: { + options: { banner: '<%= meta.banner %>' }, + src: [ + 'src/module.js', + 'src/**/*.js' + ], + dest: 'dist/angularfire.js' + } + }, + + // Run shell commands + shell: { + options: { + stdout: true + }, + protractor_install: { + command: 'node ./node_modules/protractor/bin/webdriver-manager update' + }, + npm_install: { + command: 'npm install' + }, + bower_install: { + command: 'bower install' + } + }, + + // Create local server + connect: { + testserver: { + options: { + hostname: 'localhost', + port: 3030 + } } }, + // Minify JavaScript uglify : { + options: { + preserveComments: 'some' + }, app : { files : { - 'angularfire.min.js' : ['angularfire.js'] + 'dist/angularfire.min.js' : ['dist/angularfire.js'] } } }, + // Lint JavaScript jshint : { options : { - 'bitwise' : true, - 'boss' : true, - 'browser' : true, - 'curly' : true, - 'devel' : true, - 'eqnull' : true, - 'globals' : { - 'angular' : false, - 'Firebase' : false, - 'FirebaseSimpleLogin' : false - }, - 'globalstrict' : true, - 'indent' : 2, - 'latedef' : true, - 'maxlen' : 115, - 'noempty' : true, - 'nonstandard' : true, - 'undef' : true, - 'unused' : true, - 'trailing' : true + jshintrc: '.jshintrc', + ignores: ['src/lib/polyfills.js'] }, - all : ['angularfire.js'] + all : ['src/**/*.js'] }, + // Auto-run tasks on file changes watch : { scripts : { - files : 'angularfire.js', - tasks : ['default', 'notify:watch'], + files : ['src/**/*.js', 'tests/unit/**/*.spec.js', 'tests/lib/**/*.js', 'tests/mocks/**/*.js'], + tasks : ['test:unit', 'notify:watch'], options : { - interrupt : true - } - } - }, - - notify: { - watch: { - options: { - title: 'Grunt Watch', - message: 'Build Finished' + interrupt : true, + atBegin: true } } }, + // Unit tests karma: { - unit: { + options: { configFile: 'tests/automatic_karma.conf.js' }, - continuous: { - configFile: 'tests/automatic_karma.conf.js', - singleRun: true, - browsers: ['PhantomJS'] - }, - auto: { - configFile: 'tests/automatic_karma.conf.js', - autowatch: true, - browsers: ['PhantomJS'] - }/*, - "kato": { - configFile: 'tests/automatic_karma.conf.js', - options: { - files: [ - '../bower_components/angular/angular.js', - '../bower_components/angular-mocks/angular-mocks.js', - '../lib/omnibinder-protocol.js', - 'lib/lodash.js', - 'lib/MockFirebase.js', - '../angularfire.js', - 'unit/AngularFire.spec.js' - ] - }, + singlerun: {}, + watch: { autowatch: true, - browsers: ['PhantomJS'] - }*/ + singleRun: false + }, + saucelabs: { + configFile: 'tests/sauce_karma.conf.js' + } }, - changelog: { + // End to end (e2e) tests + protractor: { options: { - dest: 'CHANGELOG.md' + configFile: "tests/local_protractor.conf.js" + }, + singlerun: {}, + saucelabs: { + options: { + configFile: "tests/sauce_protractor.conf.js", + args: { + sauceUser: process.env.SAUCE_USERNAME, + sauceKey: process.env.SAUCE_ACCESS_KEY + } + } + } + }, + + // Desktop notificaitons + notify: { + watch: { + options: { + title: 'Grunt Watch', + message: 'Build Finished' + } } } }); require('load-grunt-tasks')(grunt); - grunt.registerTask('build', ['jshint', 'uglify']); - grunt.registerTask('test', ['exec:casperjs', 'karma:continuous']); + // Installation + grunt.registerTask('install', ['shell:protractor_install']); + grunt.registerTask('update', ['shell:npm_install']); - grunt.registerTask('protractor', 'e2e tests for omnibinder', function () { - var done = this.async(); + // Single run tests + grunt.registerTask('test', ['test:unit', 'test:e2e']); + grunt.registerTask('test:unit', ['karma:singlerun']); + grunt.registerTask('test:e2e', ['concat', 'connect:testserver', 'protractor:singlerun']); - if (!grunt.file.isDir('selenium')) { - grunt.log.writeln('Installing selenium and chromedriver dependency'); - grunt.util.spawn({ - cmd: './node_modules/protractor/bin/install_selenium_standalone' - }, function (err) { - if (err) grunt.log.error(err); + // Travis CI testing + //grunt.registerTask('test:travis', ['build', 'test:unit', 'connect:testserver', 'protractor:saucelabs']); + grunt.registerTask('test:travis', ['build', 'test:unit']); - runProtractor(); - }); - } else { - runProtractor(); - } + // Sauce tasks + grunt.registerTask('sauce:unit', ['karma:saucelabs']); + grunt.registerTask('sauce:e2e', ['concat', 'connect:testserver', 'protractor:saucelabs']); - function runProtractor() { - grunt.util.spawn({ - cmd: './node_modules/protractor/bin/protractor', - args: ['tests/protractorConf.js'] - }, function (err, result, code) { - grunt.log.write(result); - done(err); - }); - } - }); + // Watch tests + grunt.registerTask('test:watch', ['karma:watch']); + grunt.registerTask('test:watch:unit', ['karma:watch']); + + // Build tasks + grunt.registerTask('build', ['concat', 'jshint', 'uglify']); + // Default task grunt.registerTask('default', ['build', 'test']); }; diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..e2d638cb --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Firebase + +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. diff --git a/README.md b/README.md index ceb228eb..b04bf072 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,107 @@ -AngularFire -=========== -AngularFire is an officially supported [AngularJS](http://angularjs.org/) binding -for [Firebase](http://www.firebase.com/?utm_medium=web&utm_source=angularFire). -Firebase is a full backend so you don't need servers to build your Angular app! +# AngularFire _(for AngularJS)_ [![Build Status](https://travis-ci.org/firebase/angularfire.svg?branch=master)](https://travis-ci.org/firebase/angularfire) [![Coverage Status](https://coveralls.io/repos/firebase/angularfire/badge.svg?branch=master&service=github)](https://coveralls.io/github/firebase/angularfire?branch=master) [![Version](https://badge.fury.io/gh/firebase%2Fangularfire.svg)](http://badge.fury.io/gh/firebase%2Fangularfire) -*Please visit the -[Firebase + Angular Quickstart guide](https://www.firebase.com/quickstart/angularjs.html) -for more information*. +**⚠️ Looking for the new AngularFire?** If you're using Angular you'll want to check out [@angular/fire](https://github.com/angular/angularfire). -We also have a [tutorial](https://www.firebase.com/tutorial/#tutorial/angular/0), -[documentation](https://www.firebase.com/docs/angular/index.html) and an -[API reference](https://www.firebase.com/docs/angular/reference.html). +## Status -Join our [Firebase + Angular Google Group](https://groups.google.com/forum/#!forum/firebase-angular) to ask questions, provide feedback, and share apps you've built with Firebase and Angular. +![Status: Frozen](https://img.shields.io/badge/Status-Frozen-yellow) -Development ------------ -[![Build Status](https://travis-ci.org/firebase/angularFire.png)](https://travis-ci.org/firebase/angularFire) -[![Bower version](https://badge.fury.io/bo/angularfire.png)](http://badge.fury.io/bo/angularfire) -[![Built with Grunt](https://cdn.gruntjs.com/builtwith.png)](http://gruntjs.com/) +This repository is no longer under active development. No new features will be added and issues are not actively triaged. Pull Requests which fix bugs are welcome and will be reviewed on a best-effort basis. -If you'd like to hack on AngularFire itself, you'll need -[node.js](http://nodejs.org/download/), [Bower](http://bower.io), and -[CasperJS](https://github.com/n1k0/casperjs): +If you maintain a fork of this repository that you believe is healthier than the official version, we may consider recommending your fork. Please open a Pull Request if you believe that is the case. -```bash -npm install -g phantomjs casperjs -npm install -bower install +[AngularJS will be in LTS until December 31st, 2021](https://blog.angular.io/stable-angularjs-and-long-term-support-7e077635ee9c) after which this library will be deprecated. + +---- + +AngularFire is the officially supported [AngularJS](https://angularjs.org/) binding for +[Firebase](https://firebase.google.com/). Firebase is a backend service that provides data storage, +file storage, authentication, and static website hosting for your Angular app. + +AngularFire is a complement to the core Firebase client. It provides you with several Angular +services: + * `$firebaseObject` - synchronized objects + * `$firebaseArray` - synchronized collections + * `$firebaseStorage` - store and retrieve user-generated content like images, audio, and video + * `$firebaseAuth` - authentication, user management, routing + +Join our [Firebase Google Group](https://groups.google.com/forum/#!forum/firebase-talk) +to ask questions, provide feedback, and share apps you've built with AngularFire. + +## Table of Contents + + * [Getting Started With Firebase](#getting-started-with-firebase) + * [Downloading AngularFire](#downloading-angularfire) + * [Documentation](#documentation) + * [Examples](#examples) + * [Migration Guides](#migration-guides) + * [Contributing](#contributing) + + +## Getting Started With Firebase + +AngularFire requires [Firebase](https://firebase.google.com/) in order to authenticate users and sync +and store data. Firebase is a suite of integrated products designed to help you develop your app, +grow your user base, and earn money. You can [sign up here for a free account](https://console.firebase.google.com/). + + +## Downloading AngularFire + +In order to use AngularFire in your project, you need to include the following files in your HTML: + +```html + + + + + + + + ``` -Use grunt to build and test the code: +You can also install AngularFire via npm and Bower and its dependencies will be downloaded +automatically: ```bash -# Default task - validates with jshint, minifies source and then runs unit tests -grunt +$ npm install angularfire --save +``` -# Watch for changes and run unit test after each change -grunt watch +```bash +$ bower install angularfire --save +``` -# Run tests -grunt test -# Minify source -grunt build -``` +## Documentation + +* [Quickstart](docs/quickstart.md) +* [Guide](docs/guide/README.md) +* [API Reference](docs/reference.md) + + +## Examples + +### Full Examples + +* [Wait And Eat](https://github.com/gordonmzhu/angular-course-demo-app-v2) +* [TodoMVC](https://github.com/tastejs/todomvc/tree/master/examples/firebase-angular) +* [Tic-Tac-Tic-Tac-Toe](https://github.com/jwngr/tic-tac-tic-tac-toe/) +* [Firereader](http://github.com/firebase/firereader) +* [Firepoker](https://github.com/Wizehive/Firepoker) + +### Recipes + +* [Date Object To A Firebase Timestamp Using `$extend`](http://jsfiddle.net/katowulf/syuzw9k1/) +* [Filter a `$FirebaseArray`](http://jsfiddle.net/firebase/ku8uL0pr/) + + +## Migration Guides + +* [Migrating from AngularFire `1.x.x` to `2.x.x`](docs/migration/1XX-to-2XX.md) +* [Migrating from AngularFire `0.9.x` to `1.x.x`](docs/migration/09X-to-1XX.md) + + +## Contributing -License -------- -[MIT](http://firebase.mit-license.org). +If you'd like to contribute to AngularFire, please first read through our [contribution +guidelines](.github/CONTRIBUTING.md). Local setup instructions are available [here](.github/CONTRIBUTING.md#local-setup). diff --git a/angularfire.js b/angularfire.js deleted file mode 100644 index 73deeede..00000000 --- a/angularfire.js +++ /dev/null @@ -1,1012 +0,0 @@ -// AngularFire is an officially supported AngularJS binding for Firebase. -// The bindings let you associate a Firebase URL with a model (or set of -// models), and they will be transparently kept in sync across all clients -// currently using your app. The 2-way data binding offered by AngularJS works -// as normal, except that the changes are also sent to all other clients -// instead of just a server. -// -// AngularFire 0.7.1-pre2 -// http://angularfire.com -// License: MIT - -"use strict"; - -(function() { - - var AngularFire, AngularFireAuth; - - // Define the `firebase` module under which all AngularFire - // services will live. - angular.module("firebase", []).value("Firebase", Firebase); - - // Define the `$firebase` service that provides synchronization methods. - angular.module("firebase").factory("$firebase", ["$q", "$parse", "$timeout", - function($q, $parse, $timeout) { - // The factory returns an object containing the value of the data at - // the Firebase location provided, as well as several methods. It - // takes a single argument: - // - // * `ref`: A Firebase reference. Queries or limits may be applied. - return function(ref) { - var af = new AngularFire($q, $parse, $timeout, ref); - return af.construct(); - }; - } - ]); - - // Define the `orderByPriority` filter that sorts objects returned by - // $firebase in the order of priority. Priority is defined by Firebase, - // for more info see: https://www.firebase.com/docs/ordered-data.html - angular.module("firebase").filter("orderByPriority", function() { - return function(input) { - var sorted = []; - if (input) { - if (!input.$getIndex || typeof input.$getIndex != "function") { - // input is not an angularFire instance - if (angular.isArray(input)) { - // If input is an array, copy it - sorted = input.slice(0); - } else if (angular.isObject(input)) { - // If input is an object, map it to an array - angular.forEach(input, function(prop) { - sorted.push(prop); - }); - } - } else { - // input is an angularFire instance - var index = input.$getIndex(); - if (index.length > 0) { - for (var i = 0; i < index.length; i++) { - var val = input[index[i]]; - if (val) { - val.$id = index[i]; - sorted.push(val); - } - } - } - } - } - return sorted; - }; - }); - - // Shim Array.indexOf for IE compatibility. - if (!Array.prototype.indexOf) { - Array.prototype.indexOf = function (searchElement, fromIndex) { - if (this === undefined || this === null) { - throw new TypeError("'this' is null or not defined"); - } - // Hack to convert object.length to a UInt32 - // jshint -W016 - var length = this.length >>> 0; - fromIndex = +fromIndex || 0; - // jshint +W016 - - if (Math.abs(fromIndex) === Infinity) { - fromIndex = 0; - } - - if (fromIndex < 0) { - fromIndex += length; - if (fromIndex < 0) { - fromIndex = 0; - } - } - - for (;fromIndex < length; fromIndex++) { - if (this[fromIndex] === searchElement) { - return fromIndex; - } - } - - return -1; - }; - } - - // The `AngularFire` object that implements synchronization. - AngularFire = function($q, $parse, $timeout, ref) { - this._q = $q; - this._bound = false; - this._loaded = false; - this._parse = $parse; - this._timeout = $timeout; - - this._index = []; - - // An object storing handlers used for different events. - this._on = { - value: [], - change: [], - loaded: [], - child_added: [], - child_moved: [], - child_changed: [], - child_removed: [] - }; - - if (typeof ref == "string") { - throw new Error("Please provide a Firebase reference instead " + - "of a URL, eg: new Firebase(url)"); - } - this._fRef = ref; - }; - - AngularFire.prototype = { - // This function is called by the factory to create a new explicit sync - // point between a particular model and a Firebase location. - construct: function() { - var self = this; - var object = {}; - - // Set the $id val equal to the Firebase reference's name() function. - object.$id = self._fRef.ref().name(); - - // Establish a 3-way data binding (implicit sync) with the specified - // Firebase location and a model on $scope. To be used from a controller - // to automatically synchronize *all* local changes. It takes three - // arguments: - // - // * `$scope` : The scope with which the bound model is associated. - // * `name` : The name of the model. - // * `defaultFn`: A function that provides a default value if the - // remote value is not set. Optional. - // - // This function also returns a promise, which, when resolved, will be - // provided an `unbind` method, a function which you can call to stop - // watching the local model for changes. - object.$bind = function(scope, name, defaultFn) { - return self._bind(scope, name, defaultFn); - }; - - // Add an object to the remote data. Adding an object is the - // equivalent of calling `push()` on a Firebase reference. It takes - // one argument: - // - // * `item`: The object or primitive to add. - // - // This function returns a promise that will be resolved when the data - // has been successfully written to the server. If the promise is - // resolved, it will be provided with a reference to the newly added - // object or primitive. The key name can be extracted using `ref.name()`. - // If the promise fails, it will resolve to an error. - object.$add = function(item) { - var ref; - var deferred = self._q.defer(); - - function _addCb(err) { - if (err) { - deferred.reject(err); - } else { - deferred.resolve(ref); - } - } - - if (typeof item == "object") { - ref = self._fRef.ref().push(self._parseObject(item), _addCb); - } else { - ref = self._fRef.ref().push(item, _addCb); - } - - return deferred.promise; - }; - - // Save the current state of the object (or a child) to the remote. - // Takes a single optional argument: - // - // * `key`: Specify a child key to save the data for. If no key is - // specified, the entire object's current state will - // be saved. - // - // This function returns a promise that will be resolved when the - // data has been successfully saved to the server. - object.$save = function(key) { - var deferred = self._q.defer(); - - function _saveCb(err) { - if (err) { - deferred.reject(err); - } else { - deferred.resolve(); - } - } - - if (key) { - var obj = self._parseObject(self._object[key]); - self._fRef.ref().child(key).set(obj, _saveCb); - } else { - self._fRef.ref().set(self._parseObject(self._object), _saveCb); - } - - return deferred.promise; - }; - - // Set the current state of the object to the specified value. Calling - // this is the equivalent of calling `set()` on a Firebase reference. - // Takes a single mandatory argument: - // - // * `newValue`: The value which should overwrite data stored at - // this location. - // - // This function returns a promise that will be resolved when the - // data has been successfully saved to the server. - object.$set = function(newValue) { - var deferred = self._q.defer(); - self._fRef.ref().set(self._parseObject(newValue), function(err) { - if (err) { - deferred.reject(err); - } else { - deferred.resolve(); - } - }); - return deferred.promise; - }; - - // Non-destructively update only a subset of keys for the current object. - // This is the equivalent of calling `update()` on a Firebase reference. - // Takes a single mandatory argument: - // - // * `newValue`: The set of keys and values that must be updated for - // this location. - // - // This function returns a promise that will be resolved when the data - // has been successfully saved to the server. - object.$update = function(newValue) { - var deferred = self._q.defer(); - self._fRef.ref().update(self._parseObject(newValue), function(err) { - if (err) { - deferred.reject(err); - } else { - deferred.resolve(); - } - }); - return deferred.promise; - }; - - // Update a value within a transaction. Calling this is the - // equivalent of calling `transaction()` on a Firebase reference. - // - // * `updateFn`: A developer-supplied function which will be passed - // the current data stored at this location (as a - // Javascript object). The function should return the - // new value it would like written (as a Javascript - // object). If "undefined" is returned (i.e. you - // "return;" with no arguments) the transaction will - // be aborted and the data at this location will not - // be modified. - // * `applyLocally`: By default, events are raised each time the - // transaction update function runs. So if it is run - // multiple times, you may see intermediate states. - // You can set this to false to suppress these - // intermediate states and instead wait until the - // transaction has completed before events are raised. - // - // This function returns a promise that will be resolved when the - // transaction function has completed. A successful transaction is - // resolved with the snapshot. If the transaction is aborted, - // the promise will be resolved with null. - object.$transaction = function(updateFn, applyLocally) { - var deferred = self._q.defer(); - self._fRef.ref().transaction(updateFn, - function(err, committed, snapshot) { - if (err) { - deferred.reject(err); - } else if (!committed) { - deferred.resolve(null); - } else { - deferred.resolve(snapshot); - } - }, - applyLocally); - - return deferred.promise; - }; - - // Remove this object from the remote data. Calling this is the - // equivalent of calling `remove()` on a Firebase reference. This - // function takes a single optional argument: - // - // * `key`: Specify a child key to remove. If no key is specified, the - // entire object will be removed from the remote data store. - // - // This function returns a promise that will be resolved when the - // object has been successfully removed from the server. - object.$remove = function(key) { - var deferred = self._q.defer(); - - function _removeCb(err) { - if (err) { - deferred.reject(err); - } else { - deferred.resolve(); - } - } - - if (key) { - self._fRef.ref().child(key).remove(_removeCb); - } else { - self._fRef.ref().remove(_removeCb); - } - - return deferred.promise; - }; - - // Get an AngularFire wrapper for a named child. This function takes - // one mandatory argument: - // - // * `key`: The key name that will point to the child reference to be - // returned. - object.$child = function(key) { - var af = new AngularFire( - self._q, self._parse, self._timeout, self._fRef.ref().child(key) - ); - return af.construct(); - }; - - // Attach an event handler for when the object is changed. You can attach - // handlers for all Firebase events like "child_added", "value", and - // "child_removed". Additionally, the following events, specific to - // AngularFire, can be listened to. - // - // - "change": The provided function will be called whenever the local - // object is modified because the remote data was updated. - // - "loaded": This function will be called *once*, when the initial - // data has been loaded. 'object' will be an empty - // object ({}) until this function is called. - object.$on = function(type, callback) { - if( self._on.hasOwnProperty(type) ) { - self._sendInitEvent(type, callback); - // One exception if made for the 'loaded' event. If we already loaded - // data (perhaps because it was synced), simply fire the callback. - if (type !== "loaded" || !this._loaded) { - self._on[type].push(callback); - } - } else { - throw new Error("Invalid event type " + type + " specified"); - } - return object; - }; - - // Detach an event handler from a specified event type. If no callback - // is specified, all event handlers for the specified event type will - // be detached. - // - // If no type if provided, synchronization for this instance of $firebase - // will be turned off complete. - object.$off = function(type, callback) { - if (self._on.hasOwnProperty(type)) { - if (callback) { - var index = self._on[type].indexOf(callback); - if (index !== -1) { - self._on[type].splice(index, 1); - } - } else { - self._on[type] = []; - } - } else { - self._fRef.off(); - } - }; - - // Authenticate this Firebase reference with a custom auth token. - // Refer to the Firebase documentation on "Custom Login" for details. - // Returns a promise that will be resolved when authentication is - // successfully completed. - object.$auth = function(token) { - var deferred = self._q.defer(); - self._fRef.auth(token, function(err, obj) { - if (err !== null) { - deferred.reject(err); - } else { - deferred.resolve(obj); - } - }, function(rej) { - deferred.reject(rej); - }); - return deferred.promise; - }; - - // Return the current index, which is a list of key names in an array, - // ordered by their Firebase priority. - object.$getIndex = function() { - return angular.copy(self._index); - }; - - // Return the reference used by this object. - object.$getRef = function() { - return self._fRef.ref(); - }; - - self._object = object; - self._getInitialValue(); - - return self._object; - }, - - // This function is responsible for fetching the initial data for the - // given reference. If the data returned from the server is an object or - // array, we'll attach appropriate child event handlers. If the value is - // a primitive, we'll continue to watch for value changes. - _getInitialValue: function() { - var self = this; - var gotInitialValue = function(snapshot) { - var value = snapshot.val(); - if (value === null) { - // NULLs are handled specially. If there's a 3-way data binding - // on a local primitive, then update that, otherwise switch to object - // binding using child events. - if (self._bound) { - var local = self._parseObject(self._parse(self._name)(self._scope)); - switch (typeof local) { - // Primitive defaults. - case "string": - case "undefined": - value = ""; - break; - case "number": - value = 0; - break; - case "boolean": - value = false; - break; - } - } - } - - // Call handlers for the "loaded" event. - if (self._loaded !== true) { - self._loaded = true; - self._broadcastEvent("loaded", value); - if( self._on.hasOwnProperty('child_added')) { - self._iterateChildren(function(key, val, prevChild) { - self._broadcastEvent('child_added', self._makeEventSnapshot(key, val, prevChild)); - }); - } - } - - self._broadcastEvent('value', self._makeEventSnapshot(snapshot.name(), value, null)); - - switch (typeof value) { - // For primitive values, simply update the object returned. - case "string": - case "number": - case "boolean": - self._updatePrimitive(value); - break; - // For arrays and objects, switch to child methods. - case "object": - self._fRef.off("value", gotInitialValue); - // Before switching to child methods, save priority for top node. - if (snapshot.getPriority() !== null) { - self._updateModel("$priority", snapshot.getPriority()); - } - self._getChildValues(); - break; - default: - throw new Error("Unexpected type from remote data " + typeof value); - } - }; - - self._fRef.on("value", gotInitialValue); - }, - - // This function attaches child events for object and array types. - _getChildValues: function() { - var self = this; - // Store the priority of the current property as "$priority". Changing - // the value of this property will also update the priority of the - // object (see _parseObject). - function _processSnapshot(snapshot, prevChild) { - var key = snapshot.name(); - var val = snapshot.val(); - - // If the item already exists in the index, remove it first. - var curIdx = self._index.indexOf(key); - if (curIdx !== -1) { - self._index.splice(curIdx, 1); - } - - // Update index. This is used by $getIndex and orderByPriority. - if (prevChild) { - var prevIdx = self._index.indexOf(prevChild); - self._index.splice(prevIdx + 1, 0, key); - } else { - self._index.unshift(key); - } - - // Update local model with priority field, if needed. - if (snapshot.getPriority() !== null) { - val.$priority = snapshot.getPriority(); - } - self._updateModel(key, val); - } - - // Helper function to attach and broadcast events. - function _handleAndBroadcastEvent(type, handler) { - return function(snapshot, prevChild) { - handler(snapshot, prevChild); - self._broadcastEvent(type, self._makeEventSnapshot(snapshot.name(), snapshot.val(), prevChild)); - }; - } - - function _handleFirebaseEvent(type, handler) { - self._fRef.on(type, _handleAndBroadcastEvent(type, handler)); - } - _handleFirebaseEvent("child_added", _processSnapshot); - _handleFirebaseEvent("child_moved", _processSnapshot); - _handleFirebaseEvent("child_changed", _processSnapshot); - _handleFirebaseEvent("child_removed", function(snapshot) { - // Remove from index. - var key = snapshot.name(); - var idx = self._index.indexOf(key); - self._index.splice(idx, 1); - - // Remove from local model. - self._updateModel(key, null); - }); - self._fRef.on('value', function(snap) { - self._broadcastEvent('value', self._makeEventSnapshot(snap.name(), snap.val())); - }); - }, - - // Called whenever there is a remote change. Applies them to the local - // model for both explicit and implicit sync modes. - _updateModel: function(key, value) { - var self = this; - self._timeout(function() { - if (value == null) { - delete self._object[key]; - } else { - self._object[key] = value; - } - - // Call change handlers. - self._broadcastEvent("change", key); - - // If there is an implicit binding, also update the local model. - if (!self._bound) { - return; - } - - var current = self._object; - var local = self._parse(self._name)(self._scope); - // If remote value matches local value, don't do anything, otherwise - // apply the change. - if (!angular.equals(current, local)) { - self._parse(self._name).assign(self._scope, angular.copy(current)); - } - }); - }, - - // Called whenever there is a remote change for a primitive value. - _updatePrimitive: function(value) { - var self = this; - self._timeout(function() { - // Primitive values are represented as a special object - // {$value: value}. Only update if the remote value is different from - // the local value. - if (!self._object.$value || - !angular.equals(self._object.$value, value)) { - self._object.$value = value; - } - - // Call change handlers. - self._broadcastEvent("change"); - - // If there's an implicit binding, simply update the local scope model. - if (self._bound) { - var local = self._parseObject(self._parse(self._name)(self._scope)); - if (!angular.equals(local, value)) { - self._parse(self._name).assign(self._scope, value); - } - } - }); - }, - - // If event handlers for a specified event were attached, call them. - _broadcastEvent: function(evt, param) { - var cbs = this._on[evt] || []; - if( evt === 'loaded' ) { - this._on[evt] = []; // release memory - } - var self = this; - - function _wrapTimeout(cb, param) { - self._timeout(function() { - cb(param); - }); - } - - if (cbs.length > 0) { - for (var i = 0; i < cbs.length; i++) { - if (typeof cbs[i] == "function") { - _wrapTimeout(cbs[i], param); - } - } - } - }, - - // triggers an initial event for loaded, value, and child_added events (which get immediate feedback) - _sendInitEvent: function(evt, callback) { - var self = this; - if( self._loaded && ['child_added', 'loaded', 'value'].indexOf(evt) > -1 ) { - self._timeout(function() { - var parsedValue = angular.isObject(self._object)? self._parseObject(self._object) : self._object; - switch(evt) { - case 'loaded': - callback(parsedValue); - break; - case 'value': - callback(self._makeEventSnapshot(self._fRef.name(), parsedValue, null)); - break; - case 'child_added': - self._iterateChildren(parsedValue, function(name, val, prev) { - callback(self._makeEventSnapshot(name, val, prev)); - }); - break; - default: // not reachable - } - }); - } - }, - - // assuming data is an object, this method will iterate all - // child keys and invoke callback with (key, value, prevChild) - _iterateChildren: function(data, callback) { - if( this._loaded && angular.isObject(data) ) { - var prev = null; - for(var key in data) { - if( data.hasOwnProperty(key) ) { - callback(key, data[key], prev); - prev = key; - } - } - } - }, - - // creates a snapshot object compatible with _broadcastEvent notifications - _makeEventSnapshot: function(key, value, prevChild) { - if( angular.isUndefined(prevChild) ) { - prevChild = null; - } - return { - snapshot: { - name: key, - value: value - }, - prevChild: prevChild - }; - }, - - // This function creates a 3-way binding between the provided scope model - // and Firebase. All changes made to the local model are saved to Firebase - // and changes to the remote data automatically appear on the local model. - _bind: function(scope, name, defaultFn) { - var self = this; - var deferred = self._q.defer(); - - // _updateModel or _updatePrimitive will take care of updating the local - // model if _bound is set to true. - self._name = name; - self._bound = true; - self._scope = scope; - - // If the local model is an object, call an update to set local values. - var local = self._parse(name)(scope); - if (local !== undefined && typeof local == "object") { - self._fRef.ref().update(self._parseObject(local)); - } - - // We're responsible for setting up scope.$watch to reflect local changes - // on the Firebase data. - var unbind = scope.$watch(name, function() { - // If the new local value matches the current remote value, we don't - // trigger a remote update. - var local = self._parseObject(self._parse(name)(scope)); - if (self._object.$value !== undefined && - angular.equals(local, self._object.$value)) { - return; - } else if (angular.equals(local, self._parseObject(self._object))) { - return; - } - - // If the local model is undefined or the remote data hasn't been - // loaded yet, don't update. - if (local === undefined || !self._loaded) { - return; - } - - // Use update if limits are in effect, set if not. - if (self._fRef.set) { - self._fRef.set(local); - } else { - self._fRef.ref().update(local); - } - }, true); - - // When the scope is destroyed, unbind automatically. - scope.$on("$destroy", function() { - unbind(); - }); - - // Once we receive the initial value, the promise will be resolved. - self._fRef.once("value", function(snap) { - self._timeout(function() { - // HACK / FIXME: Objects require a second event loop run, since we - // switch from value events to child_added. See #209 on Github. - if (typeof snap.val() != "object") { - // If the remote value is not set and defaultFn was provided, - // initialize the local value with the result of defaultFn(). - if (snap.val() == null && typeof defaultFn === 'function') { - scope[name] = defaultFn(); - } - deferred.resolve(unbind); - } else { - self._timeout(function() { - // If the remote value is not set and defaultFn was provided, - // initialize the local value with the result of defaultFn(). - if (snap.val() == null && typeof defaultFn === 'function') { - scope[name] = defaultFn(); - } - deferred.resolve(unbind); - }); - } - }); - }); - - return deferred.promise; - }, - - // Parse a local model, removing all properties beginning with "$" and - // converting $priority to ".priority". - _parseObject: function(obj) { - function _findReplacePriority(item) { - for (var prop in item) { - if (item.hasOwnProperty(prop)) { - if (prop == "$priority") { - item[".priority"] = item.$priority; - delete item.$priority; - } else if (typeof item[prop] == "object") { - _findReplacePriority(item[prop]); - } - } - } - return item; - } - - // We use toJson/fromJson to remove $$hashKey and others. Can be replaced - // by angular.copy, but only for later versions of AngularJS. - var newObj = _findReplacePriority(angular.copy(obj)); - return angular.fromJson(angular.toJson(newObj)); - } - }; - - - // Defines the `$firebaseSimpleLogin` service that provides simple - // user authentication support for AngularFire. - angular.module("firebase").factory("$firebaseSimpleLogin", [ - "$q", "$timeout", "$rootScope", function($q, $t, $rs) { - // The factory returns an object containing the authentication state - // of the current user. This service takes one argument: - // - // * `ref` : A Firebase reference. - // - // The returned object has the following properties: - // - // * `user`: Set to "null" if the user is currently logged out. This - // value will be changed to an object when the user successfully logs - // in. This object will contain details of the logged in user. The - // exact properties will vary based on the method used to login, but - // will at a minimum contain the `id` and `provider` properties. - // - // The returned object will also have the following methods available: - // $login(), $logout(), $createUser(), $changePassword(), $removeUser(), - // and $getCurrentUser(). - return function(ref) { - var auth = new AngularFireAuth($q, $t, $rs, ref); - return auth.construct(); - }; - } - ]); - - AngularFireAuth = function($q, $t, $rs, ref) { - this._q = $q; - this._timeout = $t; - this._rootScope = $rs; - this._loginDeferred = null; - this._getCurrentUserDeferred = []; - this._currentUserData = undefined; - - if (typeof ref == "string") { - throw new Error("Please provide a Firebase reference instead " + - "of a URL, eg: new Firebase(url)"); - } - this._fRef = ref; - }; - - AngularFireAuth.prototype = { - construct: function() { - var object = { - user: null, - $login: this.login.bind(this), - $logout: this.logout.bind(this), - $createUser: this.createUser.bind(this), - $changePassword: this.changePassword.bind(this), - $removeUser: this.removeUser.bind(this), - $getCurrentUser: this.getCurrentUser.bind(this), - $sendPasswordResetEmail: this.sendPasswordResetEmail.bind(this) - }; - this._object = object; - - // Initialize Simple Login. - if (!window.FirebaseSimpleLogin) { - var err = new Error("FirebaseSimpleLogin is undefined. " + - "Did you forget to include firebase-simple-login.js?"); - this._rootScope.$broadcast("$firebaseSimpleLogin:error", err); - throw err; - } - - var client = new FirebaseSimpleLogin(this._fRef, - this._onLoginEvent.bind(this)); - this._authClient = client; - return this._object; - }, - - // The login method takes a provider (for Simple Login) and authenticates - // the Firebase reference with which the service was initialized. This - // method returns a promise, which will be resolved when the login succeeds - // (and rejected when an error occurs). - login: function(provider, options) { - var deferred = this._q.defer(); - var self = this; - - // To avoid the promise from being fulfilled by our initial login state, - // make sure we have it before triggering the login and creating a new - // promise. - this.getCurrentUser().then(function() { - self._loginDeferred = deferred; - self._authClient.login(provider, options); - }); - - return deferred.promise; - }, - - // Unauthenticate the Firebase reference. - logout: function() { - // Tell the simple login client to log us out. - this._authClient.logout(); - - // Forget who we were, so that any getCurrentUser calls will wait for - // another user event. - delete this._currentUserData; - }, - - // Creates a user for Firebase Simple Login. Function 'cb' receives an - // error as the first argument and a Simple Login user object as the second - // argument. Note that this function only creates the user, if you wish to - // log in as the newly created user, call $login() after the promise for - // this method has been fulfilled. - createUser: function(email, password) { - var self = this; - var deferred = this._q.defer(); - - self._authClient.createUser(email, password, function(err, user) { - if (err) { - self._rootScope.$broadcast("$firebaseSimpleLogin:error", err); - deferred.reject(err); - } else { - deferred.resolve(user); - } - }); - - return deferred.promise; - }, - - // Changes the password for a Firebase Simple Login user. Take an email, - // old password and new password as three mandatory arguments. Returns a - // promise. - changePassword: function(email, oldPassword, newPassword) { - var self = this; - var deferred = this._q.defer(); - - self._authClient.changePassword(email, oldPassword, newPassword, - function(err) { - if (err) { - self._rootScope.$broadcast("$firebaseSimpleLogin:error", err); - deferred.reject(err); - } else { - deferred.resolve(); - } - } - ); - - return deferred.promise; - }, - - // Gets a promise for the current user info. - getCurrentUser: function() { - var self = this; - var deferred = this._q.defer(); - - if (self._currentUserData !== undefined) { - deferred.resolve(self._currentUserData); - } else { - self._getCurrentUserDeferred.push(deferred); - } - - return deferred.promise; - }, - - // Remove a user for the listed email address. Returns a promise. - removeUser: function(email, password) { - var self = this; - var deferred = this._q.defer(); - - self._authClient.removeUser(email, password, function(err) { - if (err) { - self._rootScope.$broadcast("$firebaseSimpleLogin:error", err); - deferred.reject(err); - } else { - deferred.resolve(); - } - }); - - return deferred.promise; - }, - - // Send a password reset email to the user for an email + password account. - sendPasswordResetEmail: function(email) { - var self = this; - var deferred = this._q.defer(); - - self._authClient.sendPasswordResetEmail(email, function(err) { - if (err) { - self._rootScope.$broadcast("$firebaseSimpleLogin:error", err); - deferred.reject(err); - } else { - deferred.resolve(); - } - }); - - return deferred.promise; - }, - - // Internal callback for any Simple Login event. - _onLoginEvent: function(err, user) { - // HACK -- calls to logout() trigger events even if we're not logged in, - // making us get extra events. Throw them away. This should be fixed by - // changing Simple Login so that its callbacks refer directly to the - // action that caused them. - if (this._currentUserData === user && err === null) { - return; - } - - var self = this; - if (err) { - if (self._loginDeferred) { - self._loginDeferred.reject(err); - self._loginDeferred = null; - } - self._rootScope.$broadcast("$firebaseSimpleLogin:error", err); - } else { - this._currentUserData = user; - - self._timeout(function() { - self._object.user = user; - if (user) { - self._rootScope.$broadcast("$firebaseSimpleLogin:login", user); - } else { - self._rootScope.$broadcast("$firebaseSimpleLogin:logout"); - } - if (self._loginDeferred) { - self._loginDeferred.resolve(user); - self._loginDeferred = null; - } - while (self._getCurrentUserDeferred.length > 0) { - var def = self._getCurrentUserDeferred.pop(); - def.resolve(user); - } - }); - } - } - }; -})(); diff --git a/angularfire.min.js b/angularfire.min.js deleted file mode 100644 index 41ad72ef..00000000 --- a/angularfire.min.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";!function(){var a,b;angular.module("firebase",[]).value("Firebase",Firebase),angular.module("firebase").factory("$firebase",["$q","$parse","$timeout",function(b,c,d){return function(e){var f=new a(b,c,d,e);return f.construct()}}]),angular.module("firebase").filter("orderByPriority",function(){return function(a){var b=[];if(a)if(a.$getIndex&&"function"==typeof a.$getIndex){var c=a.$getIndex();if(c.length>0)for(var d=0;d>>0;for(b=+b||0,1/0===Math.abs(b)&&(b=0),0>b&&(b+=c,0>b&&(b=0));c>b;b++)if(this[b]===a)return b;return-1}),a=function(a,b,c,d){if(this._q=a,this._bound=!1,this._loaded=!1,this._parse=b,this._timeout=c,this._index=[],this._on={value:[],change:[],loaded:[],child_added:[],child_moved:[],child_changed:[],child_removed:[]},"string"==typeof d)throw new Error("Please provide a Firebase reference instead of a URL, eg: new Firebase(url)");this._fRef=d},a.prototype={construct:function(){var b=this,c={};return c.$id=b._fRef.ref().name(),c.$bind=function(a,c,d){return b._bind(a,c,d)},c.$add=function(a){function c(a){a?e.reject(a):e.resolve(d)}var d,e=b._q.defer();return d="object"==typeof a?b._fRef.ref().push(b._parseObject(a),c):b._fRef.ref().push(a,c),e.promise},c.$save=function(a){function c(a){a?d.reject(a):d.resolve()}var d=b._q.defer();if(a){var e=b._parseObject(b._object[a]);b._fRef.ref().child(a).set(e,c)}else b._fRef.ref().set(b._parseObject(b._object),c);return d.promise},c.$set=function(a){var c=b._q.defer();return b._fRef.ref().set(b._parseObject(a),function(a){a?c.reject(a):c.resolve()}),c.promise},c.$update=function(a){var c=b._q.defer();return b._fRef.ref().update(b._parseObject(a),function(a){a?c.reject(a):c.resolve()}),c.promise},c.$transaction=function(a,c){var d=b._q.defer();return b._fRef.ref().transaction(a,function(a,b,c){a?d.reject(a):d.resolve(b?c:null)},c),d.promise},c.$remove=function(a){function c(a){a?d.reject(a):d.resolve()}var d=b._q.defer();return a?b._fRef.ref().child(a).remove(c):b._fRef.ref().remove(c),d.promise},c.$child=function(c){var d=new a(b._q,b._parse,b._timeout,b._fRef.ref().child(c));return d.construct()},c.$on=function(a,d){if(!b._on.hasOwnProperty(a))throw new Error("Invalid event type "+a+" specified");return b._sendInitEvent(a,d),"loaded"===a&&this._loaded||b._on[a].push(d),c},c.$off=function(a,c){if(b._on.hasOwnProperty(a))if(c){var d=b._on[a].indexOf(c);-1!==d&&b._on[a].splice(d,1)}else b._on[a]=[];else b._fRef.off()},c.$auth=function(a){var c=b._q.defer();return b._fRef.auth(a,function(a,b){null!==a?c.reject(a):c.resolve(b)},function(a){c.reject(a)}),c.promise},c.$getIndex=function(){return angular.copy(b._index)},c.$getRef=function(){return b._fRef.ref()},b._object=c,b._getInitialValue(),b._object},_getInitialValue:function(){var a=this,b=function(c){var d=c.val();if(null===d&&a._bound){var e=a._parseObject(a._parse(a._name)(a._scope));switch(typeof e){case"string":case"undefined":d="";break;case"number":d=0;break;case"boolean":d=!1}}switch(a._loaded!==!0&&(a._loaded=!0,a._broadcastEvent("loaded",d),a._on.hasOwnProperty("child_added")&&a._iterateChildren(function(b,c,d){a._broadcastEvent("child_added",a._makeEventSnapshot(b,c,d))})),a._broadcastEvent("value",a._makeEventSnapshot(c.name(),d,null)),typeof d){case"string":case"number":case"boolean":a._updatePrimitive(d);break;case"object":a._fRef.off("value",b),null!==c.getPriority()&&a._updateModel("$priority",c.getPriority()),a._getChildValues();break;default:throw new Error("Unexpected type from remote data "+typeof d)}};a._fRef.on("value",b)},_getChildValues:function(){function a(a,b){var c=a.name(),e=a.val(),f=d._index.indexOf(c);if(-1!==f&&d._index.splice(f,1),b){var g=d._index.indexOf(b);d._index.splice(g+1,0,c)}else d._index.unshift(c);null!==a.getPriority()&&(e.$priority=a.getPriority()),d._updateModel(c,e)}function b(a,b){return function(c,e){b(c,e),d._broadcastEvent(a,d._makeEventSnapshot(c.name(),c.val(),e))}}function c(a,c){d._fRef.on(a,b(a,c))}var d=this;c("child_added",a),c("child_moved",a),c("child_changed",a),c("child_removed",function(a){var b=a.name(),c=d._index.indexOf(b);d._index.splice(c,1),d._updateModel(b,null)}),d._fRef.on("value",function(a){d._broadcastEvent("value",d._makeEventSnapshot(a.name(),a.val()))})},_updateModel:function(a,b){var c=this;c._timeout(function(){if(null==b?delete c._object[a]:c._object[a]=b,c._broadcastEvent("change",a),c._bound){var d=c._object,e=c._parse(c._name)(c._scope);angular.equals(d,e)||c._parse(c._name).assign(c._scope,angular.copy(d))}})},_updatePrimitive:function(a){var b=this;b._timeout(function(){if(b._object.$value&&angular.equals(b._object.$value,a)||(b._object.$value=a),b._broadcastEvent("change"),b._bound){var c=b._parseObject(b._parse(b._name)(b._scope));angular.equals(c,a)||b._parse(b._name).assign(b._scope,a)}})},_broadcastEvent:function(a,b){function c(a,b){e._timeout(function(){a(b)})}var d=this._on[a]||[];"loaded"===a&&(this._on[a]=[]);var e=this;if(d.length>0)for(var f=0;f-1&&c._timeout(function(){var d=angular.isObject(c._object)?c._parseObject(c._object):c._object;switch(a){case"loaded":b(d);break;case"value":b(c._makeEventSnapshot(c._fRef.name(),d,null));break;case"child_added":c._iterateChildren(d,function(a,d,e){b(c._makeEventSnapshot(a,d,e))})}})},_iterateChildren:function(a,b){if(this._loaded&&angular.isObject(a)){var c=null;for(var d in a)a.hasOwnProperty(d)&&(b(d,a[d],c),c=d)}},_makeEventSnapshot:function(a,b,c){return angular.isUndefined(c)&&(c=null),{snapshot:{name:a,value:b},prevChild:c}},_bind:function(a,b,c){var d=this,e=d._q.defer();d._name=b,d._bound=!0,d._scope=a;var f=d._parse(b)(a);void 0!==f&&"object"==typeof f&&d._fRef.ref().update(d._parseObject(f));var g=a.$watch(b,function(){var c=d._parseObject(d._parse(b)(a));void 0!==d._object.$value&&angular.equals(c,d._object.$value)||angular.equals(c,d._parseObject(d._object))||void 0!==c&&d._loaded&&(d._fRef.set?d._fRef.set(c):d._fRef.ref().update(c))},!0);return a.$on("$destroy",function(){g()}),d._fRef.once("value",function(f){d._timeout(function(){"object"!=typeof f.val()?(null==f.val()&&"function"==typeof c&&(a[b]=c()),e.resolve(g)):d._timeout(function(){null==f.val()&&"function"==typeof c&&(a[b]=c()),e.resolve(g)})})}),e.promise},_parseObject:function(a){function b(a){for(var c in a)a.hasOwnProperty(c)&&("$priority"==c?(a[".priority"]=a.$priority,delete a.$priority):"object"==typeof a[c]&&b(a[c]));return a}var c=b(angular.copy(a));return angular.fromJson(angular.toJson(c))}},angular.module("firebase").factory("$firebaseSimpleLogin",["$q","$timeout","$rootScope",function(a,c,d){return function(e){var f=new b(a,c,d,e);return f.construct()}}]),b=function(a,b,c,d){if(this._q=a,this._timeout=b,this._rootScope=c,this._loginDeferred=null,this._getCurrentUserDeferred=[],this._currentUserData=void 0,"string"==typeof d)throw new Error("Please provide a Firebase reference instead of a URL, eg: new Firebase(url)");this._fRef=d},b.prototype={construct:function(){var a={user:null,$login:this.login.bind(this),$logout:this.logout.bind(this),$createUser:this.createUser.bind(this),$changePassword:this.changePassword.bind(this),$removeUser:this.removeUser.bind(this),$getCurrentUser:this.getCurrentUser.bind(this),$sendPasswordResetEmail:this.sendPasswordResetEmail.bind(this)};if(this._object=a,!window.FirebaseSimpleLogin){var b=new Error("FirebaseSimpleLogin is undefined. Did you forget to include firebase-simple-login.js?");throw this._rootScope.$broadcast("$firebaseSimpleLogin:error",b),b}var c=new FirebaseSimpleLogin(this._fRef,this._onLoginEvent.bind(this));return this._authClient=c,this._object},login:function(a,b){var c=this._q.defer(),d=this;return this.getCurrentUser().then(function(){d._loginDeferred=c,d._authClient.login(a,b)}),c.promise},logout:function(){this._authClient.logout(),delete this._currentUserData},createUser:function(a,b){var c=this,d=this._q.defer();return c._authClient.createUser(a,b,function(a,b){a?(c._rootScope.$broadcast("$firebaseSimpleLogin:error",a),d.reject(a)):d.resolve(b)}),d.promise},changePassword:function(a,b,c){var d=this,e=this._q.defer();return d._authClient.changePassword(a,b,c,function(a){a?(d._rootScope.$broadcast("$firebaseSimpleLogin:error",a),e.reject(a)):e.resolve()}),e.promise},getCurrentUser:function(){var a=this,b=this._q.defer();return void 0!==a._currentUserData?b.resolve(a._currentUserData):a._getCurrentUserDeferred.push(b),b.promise},removeUser:function(a,b){var c=this,d=this._q.defer();return c._authClient.removeUser(a,b,function(a){a?(c._rootScope.$broadcast("$firebaseSimpleLogin:error",a),d.reject(a)):d.resolve()}),d.promise},sendPasswordResetEmail:function(a){var b=this,c=this._q.defer();return b._authClient.sendPasswordResetEmail(a,function(a){a?(b._rootScope.$broadcast("$firebaseSimpleLogin:error",a),c.reject(a)):c.resolve()}),c.promise},_onLoginEvent:function(a,b){if(this._currentUserData!==b||null!==a){var c=this;a?(c._loginDeferred&&(c._loginDeferred.reject(a),c._loginDeferred=null),c._rootScope.$broadcast("$firebaseSimpleLogin:error",a)):(this._currentUserData=b,c._timeout(function(){for(c._object.user=b,b?c._rootScope.$broadcast("$firebaseSimpleLogin:login",b):c._rootScope.$broadcast("$firebaseSimpleLogin:logout"),c._loginDeferred&&(c._loginDeferred.resolve(b),c._loginDeferred=null);c._getCurrentUserDeferred.length>0;){var a=c._getCurrentUserDeferred.pop();a.resolve(b)}}))}}}}(); \ No newline at end of file diff --git a/bower.json b/bower.json index b0733549..b39a270d 100644 --- a/bower.json +++ b/bower.json @@ -1,15 +1,31 @@ { "name": "angularfire", - "version": "0.7.0", - "main": ["./angularfire.js"], - "ignore": ["Gruntfile.js", "package.js", "tests", "README.md", ".travis.yml"], - "dependencies": { - "angular": "~1.2.0", - "firebase": "~1.0.5", - "firebase-simple-login": "~1.3.0" + "description": "The officially supported AngularJS binding for Firebase", + "version": "2.3.0", + "authors": [ + "Firebase (https://firebase.google.com/)" + ], + "homepage": "https://github.com/firebase/angularfire", + "repository": { + "type": "git", + "url": "https://github.com/firebase/angularfire.git" }, - "devDependencies": { - "angular-mocks" : "~1.2.0", - "observe-js": "~0.1.4" + "license": "MIT", + "keywords": [ + "angular", + "angularjs", + "firebase", + "realtime" + ], + "main": "dist/angularfire.js", + "ignore": [ + "**/*", + "!dist/*.js", + "!README.md", + "!LICENSE" + ], + "dependencies": { + "angular": "^1.3.0", + "firebase": "3.x.x" } } diff --git a/changelog.txt b/changelog.txt new file mode 100644 index 00000000..e69de29b diff --git a/docs/guide/README.md b/docs/guide/README.md new file mode 100644 index 00000000..9156e2cd --- /dev/null +++ b/docs/guide/README.md @@ -0,0 +1,9 @@ +# AngularFire Guide + +1. [Introduction to AngularFire](introduction-to-angularfire.md) - Learn about what AngularFire is and how to integrate it into your Angular app. +2. [Synchronized Objects](synchronized-objects.md) - Create synchronized objects and experience three-way data binding. +3. [Synchronized Arrays](synchronized-arrays.md) - Create and modify arrays which stay in sync with the database. +4. [Uploading & Downloading Binary Content](uploading-downloading-binary-content.md) - Store and retrieve content like images, audio, and video. +5. [User Authentication](user-auth.md) - AngularFire handles user authentication and session management for you. +6. [Extending the Services](extending-services.md) - Advanced users can extend the functionality of the built-in AngularFire services. +7. [Beyond AngularFire](beyond-angularfire.md) - AngularFire is not the only way to use Angular and Firebase together. diff --git a/docs/guide/beyond-angularfire.md b/docs/guide/beyond-angularfire.md new file mode 100644 index 00000000..8e3984a5 --- /dev/null +++ b/docs/guide/beyond-angularfire.md @@ -0,0 +1,82 @@ +# Beyond AngularFire | AngularFire Guide + +## Table of Contents + +* [Overview](#overview) +* [Best Practices](#best-practices) +* [Deploying Your App](#deploying-your-app) +* [Next Steps](#next-steps) + + +## Overview + +AngularFire greatly simplifies bindings and abstracts a lot of the internal workings of Angular, +such as how to notify the compiler when changes occur. However, it does not attempt to replicate +the entire Firebase client library's API. + +There are plenty of use cases for dropping down to the SDK level and using it directly. This +section will cover a few best practices and techniques for grabbing data directly from our +database using the JavaScript client library. + +This is easiest to accomplish with an example, so read the comments carefully. + +```js +app.controller("SampleCtrl", ["$scope", "$timeout", function($scope, $timeout) { + // create a reference to our Firebase database + var ref = firebase.database().ref(); + + // read data from the database into a local scope variable + ref.on("value", function(snapshot) { + // Since this event will occur outside Angular's $apply scope, we need to notify Angular + // each time there is an update. This can be done using $scope.$apply or $timeout. We + // prefer to use $timeout as it a) does not throw errors and b) ensures all levels of the + // scope hierarchy are refreshed (necessary for some directives to see the changes) + $timeout(function() { + $scope.data = snapshot.val(); + }); + }); +}]); +``` + +Synchronizing simple data like this is trivial. When we start operating on synchronized arrays +and dealing with bindings, things get a little more interesting. For a comparison of the +bare-bones work needed to synchronize an array, examine +[a naive comparison of AngularFire versus the vanilla Firebase client library](https://gist.github.com/katowulf/a8466f4d66a4cea7af7c), and look at +[Firebase.getAsArray()](https://github.com/katowulf/Firebase.getAsArray) for a more +fully functional synchronized array implementation and the work involved. + + +## Best Practices + +When using the vanilla Firebase client library with Angular, it is best to keep the following things +in mind: + +* **Wrap events in `$timeout()`**: Wrap all server notifications in +`$timeout()` to ensure the Angular compiler is notified of changes. +* **Use `$window.Firebase`**: This allows test units and end-to-end +tests to spy on the Firebase client library and replace it with mock functions. It also avoids the linter warnings about +globals. + + +## Deploying Your App + +Once you are done building your application, you'll need a way to share it with the world. To +deploy your Angular applications free, fast, and without fuss, do it Firebase style! Our +production-grade hosting service serves your content over HTTPS and is backed by a global CDN. +You can deploy your application for free at your very own subdomain of `firebaseapp.com` +or you can host it at any custom domain on one of our paid plans. Check out +[Firebase Hosting](https://firebase.google.com/docs/hosting/) for more information. + + +## Next Steps + +There are many additional resources for learning about using Firebase with Angular applications: + +* Browse the [AngularFire API reference](/docs/reference.md). +* The [`angularfire-seed`](https://github.com/firebase/angularfire-seed) repo contains a template +project to help you get started. +* Check out the [various examples that use AngularFire](/README.md#examples). +* Join our [Firebase mailing list](https://groups.google.com/forum/#!forum/firebase-talk) to +keep up to date with any announcements and learn from the AngularFire community. +* The [`angularfire` tag on Stack Overflow](http://stackoverflow.com/questions/tagged/angularfire) +has answers to a lot of code-related questions. diff --git a/docs/guide/extending-services.md b/docs/guide/extending-services.md new file mode 100644 index 00000000..29d4a9d1 --- /dev/null +++ b/docs/guide/extending-services.md @@ -0,0 +1,161 @@ +# Extending Services | AngularFire Guide + +## Table of Contents + +* [Overview](#overview) +* [Naming Conventions](#naming-conventions) +* [Extending `$firebaseObject`](#extending-firebaseobject) +* [Extending `$firebaseArray`](#extending-firebasearray) + + +## Overview + +**This section is intended for experienced Angular users. [Skip ahead](beyond-angularfire.md) if you are just getting started.** + +Both the `$firebaseArray` and `$firebaseObject` services provide an +`$extend()` method for creating new services that inherit from these base classes. +This allows us to transform data and add additional methods onto our synchronized objects and +arrays. Before we jump into how exactly to do this, let's discuss some naming conventions used +within the AngularFire library. + + +## Naming Conventions + +Methods in `$firebaseArray` and `$firebaseObject` are named using +`$`, `$$` or `_` prefixes, according to the following +convention: + +* `$ prefix`: These are **public** methods that exist as part of the +AngularFire API. They can be overridden using `$extend()`. They should not be removed and must obey the contract specified in the API, as they are used internally by other methods. +* `$$ prefix`: The methods beginning with `$$` should be considered +**protected**. They are called by the synchronization code and should not be +called by other methods, but they may be useful to developers for manipulating data during +add / update / remove events. They can be overridden with `$extend()` +but must obey the contract specified in the API. +* `_ prefix`: Methods and properties beginning with `_` should be considered +**private**. They are internal methods to the AngularFire code and should not +be altered or depended on in any way. They can change or disappear in any future release, +without notice. They are ignored when converting local records to JSON before saving them to the +Firebase database. +* `$id`: This special variable is used to track the remote Firebase key. It's used by the +`$getRecord()` method to find items inside of `$firebaseArray` and is expected to be +set when `$$added` is invoked. +* `$value`: This special variable stores primitive values for remote records. For example, if the +remote value at a path is `"foo"`, and that path is synchronized into a local `$firebaseObject`, +the locally synchronized object will have a JSON structure `{ "$value": "foo" }`. Similarly, if a +remote path does not exist, the local object would have the JSON structure `{ "$value": null }`. +See [Working with Primitives](./synchronized-objects.md#working-with-primitives) for more details. + +By default, data stored on a synchronized object or a record in a synchronized array exists +as a direct attribute of the object. We denote any methods or data which should *not* be +synchronized with the server by prefixing it with one of these characters. They are automatically +removed from JSON data before synchronizing this data back to the database. +Developers may use those prefixes to add additional data / methods to an object or a record +which they do not want synchronized. + + +## Extending `$firebaseObject` + +The following `User` factory retrieves a synchronized user object, and +adds a special `getFullName()` method. + +```js +app.factory("User", ["$firebaseObject", + function($firebaseObject) { + // create a new service based on $firebaseObject + var User = $firebaseObject.$extend({ + // these methods exist on the prototype, so we can access the data using `this` + getFullName: function() { + return this.firstName + " " + this.lastName; + } + }); + + return function(userId) { + var userRef = firebase.database().ref() + .child("users").child(userId); + + // create an instance of User (the new operator is required) + return new User(userRef); + } + } +]); +``` + +The `new` operator is required for child classes created with the `$extend()` method. + +The following special `$$` methods are used by the `$firebaseObject` service +to notify itself of any server changes. They can be overridden to transform how data is stored +locally, and what is returned to the server. Read more about them in the +[API documentation](/docs/reference.md#extending-the-services). + +| Method | Description | +|--------|-------------| +| `$$updated(snapshot)` | Called with a snapshot any time the value in the database changes. It returns a boolean indicating whether any changes were applied. | +| `$$error(Object)` | Called if there is a permissions error accessing remote data. Generally these errors are unrecoverable (the data will no longer by synchronized). | +| `$$defaults(Object)` | A key / value pair that can be used to create default values for any fields which are not found in the server data (i.e. `undefined` fields). By default, they are applied each time the `$$updated` method is invoked. | +| `toJSON()` | If this method exists, it is used by `JSON.stringify()` to parse the data sent back to the server. | + +If you view a `$firebaseObject` in the JavaScript debugger, you may notice a special `$$conf` +variable. This internal property is used to track internal bindings and state. It is non-enumerable (i.e. it won't +be iterated by `for` or by `angular.forEach()`) and is also read-only. It is never +saved back to the server (all `$$` properties are ignored), and it should not be modified or used +by extending services. + + +## Extending `$firebaseArray` + +The following `ListWithTotal` service extends `$firebaseArray` to include a `getTotal()` method. + +```js +app.factory("ListWithTotal", ["$firebaseArray", + function($firebaseArray) { + // create a new service based on $firebaseArray + var ListWithTotal = $firebaseArray.$extend({ + getTotal: function() { + var total = 0; + // the array data is located in this.$list + angular.forEach(this.$list, function(rec) { + total += rec.amount; + }); + return total; + } + }); + + return function(listRef) { + // create an instance of ListWithTotal (the new operator is required) + return new ListWithTotal(listRef); + } + } +]); +``` + +The `new` operator is required for child classes created with the `$extend()` method. + +The following special `$$` methods are called internally whenever AngularFire receives a notification +of a server-side change. They can be overridden to transform how data is stored +locally, and what is returned to the server. Read more about them in the +[API documentation](/docs/reference.md#extending-the-services). + +| Method | Description | +|--------|-------------| +| `$$added(snapshot, prevChildKey)` | Called any time a `child_added` event is received. Returns the new record that should be added to the array. The `$getRecord()` method depends on $$added to set the special `$id` variable on each record to the Firebase key. This is used for finding records in the list during `$$added`, `$$updated`, and `$$deleted` events. It is possible to use fields other than `$id` by also overriding how `$getRecord()` matches keys to record in the array. | +| `$$updated(snapshot)` | Called any time a `child_updated` event is received. Applies the changes and returns `true` if any local data was modified. Uses the `$getRecord()` method to find the correct record in the array for applying updates. Should return `false` if no changes occurred or if the record does not exist in the array. | +| `$$moved(snapshot, prevChildKey)` | Called any time a `child_moved` event is received. Returns `true` if the record should be moved. The actual move event takes place inside the `$$process` method. | +| `$$removed(snapshot)` | Called with a snapshot any time a `child_removed` event is received. Depends on the `$getRecord()` method to find the correct record in the array. Returns `true` if the record should be removed. The actual splicing of the array takes place in the `$$process` method. The only responsibility of `$$removed` is deciding if the remove request is valid and if the record exists. | +| `$$error(errorObject)` | Called if there is a permissions error accessing remote data. Generally these errors are unrecoverable (the data will no longer by synchronized). | + +The methods below are also part of extensible portion of `$firebaseArray`, and are used by the event +methods above, and when saving data back to the Firebase database. + +| Method | Description | +|--------|-------------| +| `$$defaults(Object)` | A key / value pair that can be used to create default values for any fields which are not found in the server data (i.e. `undefined` fields). By default, they are applied each time the `$add()`, `$$added()`, or `$$updated()`, methods are invoked. | +| `toJSON()` | If this method exists on a record **in the array**, it is used to parse the data sent back to the server. Thus, by overriding `$$added` to create a toJSON() method on individual records, one can manipulate what data is sent back to Firebase and how it is processed before saving. | +| `$$process(event, record, prevChildKey)` | This is a mostly internal method and should generally not be overridden. It abstracts some common functionality between the various event types. It's responsible for all inserts, deletes, and splicing of the array element elements, and for calling `$$notify` to trigger notification events. It is called immediately after any server event (`$$added`, `$$updated`, `$$moved` or `$$removed`), assuming those methods do not cancel the event by returning `false` or `null`. | +| `$$notify(event, recordKey)` | This is a mostly internal method and should generally not be overridden. It triggers notification events for listeners established by `$watch` and is called internally by `$$process`. | + +You can read more about extending the `$firebaseObject` and `$firebaseArray` +services in the +[API reference](/docs/reference.md#extending-the-services). + +The sections of this guide so far have taken us on a tour through the functionality provided by the AngularFire library, but there is still more that can be done with the combination of Firebase and Angular. The [next section](beyond-angularfire.md) takes us beyond AngularFire to see what else is possible. diff --git a/docs/guide/introduction-to-angularfire.md b/docs/guide/introduction-to-angularfire.md new file mode 100644 index 00000000..8c59d537 --- /dev/null +++ b/docs/guide/introduction-to-angularfire.md @@ -0,0 +1,233 @@ +# Introduction to Firebase | AngularFire Guide + +## Table of Contents + +* [Overview](#overview) +* [The Role of AngularFire](#the-role-of-angularfire) +* [Installing AngularFire](#installing-angularfire) +* [Handling Asynchronous Operations](#handling-asynchronous-operations) + + +## Overview + +Firebase provides several key advantages for [Angular](https://angular.io/) applications: + +1. **Lightning-fast data synchronization:** Firebase can serve as your entire backend service, not + only persisting your data, but synchronizing it instantly between millions of connected clients. +2. **No backend server:** Utilizing only the Firebase JavaScript SDK and AngularFire, combined with + our [flexible Security Rules](https://firebase.google.com/docs/database/security/) rules, you can + have complete control of your data without any server-side hardware or code. +3. **Built-in authentication:** Firebase provides an [authentication and user management + service](https://firebase.google.com/docs/auth/) which interfaces with OAuth service + providers like Facebook and Twitter, as well as anonymous and email / password authentication + tools. You can even integrate with an existing authentication service using Firebase custom + authentication. +4. **Free hosting:** Every Firebase app comes with [free hosting](https://firebase.google.com/docs/hosting/) + served over a secure SSL connection and backed by a global CDN. You can deploy your static HTML, + JavaScript, and CSS files to the web in seconds. +5. **Magical data bindings:** Our AngularFire library works like *glue* between Angular's two-way + bindings and Firebase's scalable synchronization platform. + + +## The Role of AngularFire + +AngularFire is an [open source library](https://github.com/firebase/angularfire) maintained by the +Firebase team and our amazing community of developers. It provides three-way communication between +your Firebase database and Angular's DOM - JavaScript bindings. + +If you are unfamiliar with Firebase, we suggest you start by reading through the [Firebase web +guide](https://firebase.google.com/docs/database/web/start). It is important to understand the +fundamental principles of how to structure data in your Firebase database and how to read and write +from it before diving into AngularFire. These bindings are meant to complement the core Firebase +client library, not replace it entirely by adding `$` signs in front of the methods. + +AngularFire is also not ideal for synchronizing deeply nested collections inside of collections. In +general, deeply nested collections [should typically be avoided](https://firebase.google.com/docs/database/web/structure-data) +in distributed systems. + +While AngularFire abstracts a lot of complexities involved in synchronizing data, it is not required +to use Angular with Firebase. Alternatives are covered in the [Beyond AngularFire](./beyond-angularfire.md) +section of this guide. + + +## Installing AngularFire + +Adding Firebase to your application is easy. Simply include the Firebase JavaScript SDK and the +AngularFire bindings from our CDN: + +```html + + + + + + + + +``` + +Firebase and AngularFire are also available via npm and Bower as `firebase` and `angularfire`, +respectively. A [Yeoman generator](https://github.com/firebase/generator-angularfire) is also +available. + +Once we have our libraries installed, we can include the AngularFire services by declaring +`firebase` as a module dependency in our application. + +```js +var app = angular.module("sampleApp", ["firebase"]); +``` + +We now will have access to three services provided by AngularFire: `$firebaseObject`, +`$firebaseArray`, and `$firebaseAuth`. To use these services, we need to inject them into a +controller, factory, or service. + +```js +app.controller("SampleController", ["$scope", "$firebaseArray", + function($scope, $firebaseArray) { + // ... + } +]); +``` + +Let's see it in action! The live code example below is a working demo of a rudimentary chat room. +It binds an Angular view to a Firebase backend, synchronizing a list of messages between the DOM, +Angular, and Firebase in realtime. It doesn't seem like much code for all of this, and that's part +of the magic! + +```js +// define our app and dependencies (remember to include firebase!) +var app = angular.module("sampleApp", ["firebase"]); + +// this factory returns a synchronized array of chat messages +app.factory("chatMessages", ["$firebaseArray", + function($firebaseArray) { + // create a reference to the database location where we will store our data + var ref = firebase.database().ref(); + + // this uses AngularFire to create the synchronized array + return $firebaseArray(ref); + } +]); + +app.controller("ChatCtrl", ["$scope", "chatMessages", + // we pass our new chatMessages factory into the controller + function($scope, chatMessages) { + $scope.user = "Guest " + Math.round(Math.random() * 100); + + // we add chatMessages array to the scope to be used in our ng-repeat + $scope.messages = chatMessages; + + // a method to create new messages; called by ng-submit + $scope.addMessage = function() { + // calling $add on a synchronized array is like Array.push(), + // except that it saves the changes to our database! + $scope.messages.$add({ + from: $scope.user, + content: $scope.message + }); + + // reset the message input + $scope.message = ""; + }; + + // if the messages are empty, add something for fun! + $scope.messages.$loaded(function() { + if ($scope.messages.length === 0) { + $scope.messages.$add({ + from: "Firebase Docs", + content: "Hello world!" + }); + } + }); + } +]); +``` + +```html +
+
    +
  • {{ message.from }}: {{ message.content }}
  • +
+
+ + +
+
+``` + +The primary purpose of AngularFire is to manage synchronized data, which is exposed through the +`$firebaseObject` and `$firebaseArray` services. These services are aware of how Angular's +[compile process works](https://docs.angularjs.org/guide/compiler), and notifies it at the correct +points to check `$digest` for changes and update the DOM. If that sounds like a foreign language, +that's okay! AngularFire is taking care of it, so don't worry. + +It's not always necessary to set up AngularFire bindings to interact with the database. This is +particularly true when just writing data, and not synchronizing it locally. Since you already have +a database reference handy, it is perfectly acceptable to simply use the vanilla Firebase client +library API methods. + +```js +var ref = firebase.database().ref(); +// We don't always need AngularFire! +//var obj = $firebaseObject(ref); +// For example, if we just want to increment a counter, which we aren't displaying locally, +// we can just set it using the SDK +ref.child("foo/counter").transaction(function(currentValue) { + return (currentValue || 0) + 1; +}); +``` + + +## Handling Asynchronous Operations + +Data is synchronized with our database *asynchronously*. This means that calls to the remote server +take some time to execute, but the code keeps running in the meantime. Thus, we have to be careful +to wait for the server to return data before we can access it. + +The easiest way to log the data is to print it within the view using Angular's `json` filter. +AngularFire tells the Angular compiler when it has finished loading the data, so there is no need to +worry about when it be available. + +```html +
{{ data | json }}
+``` + +It's also possible to do this directly in the controller by using the +[`$loaded()`](/docs/reference.md#loaded) method. +However, this method should be used with care as it's only called once after initial load. Using it +for anything but debugging is usually a poor practice. + +```js +var ref = firebase.database().ref(); +$scope.data = $firebaseObject(ref); +// this waits for the data to load and then logs the output. Therefore, +// data from the server will now appear in the logged output. Use this with care! +$scope.data.$loaded() + .then(function() { + console.log($scope.data); + }) + .catch(function(err) { + console.error(err); + }); +``` + +When working directly with the SDK, it's important to notify Angular's compiler after the data has +been loaded: + +```js +var ref = firebase.database().ref(); +ref.on("value", function(snapshot) { + // This isn't going to show up in the DOM immediately, because + // Angular does not know we have changed this in memory. + // $scope.data = snapshot.val(); + // To fix this, we can use $scope.$apply() to notify Angular that a change occurred. + $scope.$apply(function() { + $scope.data = snapshot.val(); + }); +}); +``` + +Now that we understand the basics of integrating AngularFire into our application, let's dive deeper +into reading and writing synchronized data with our database. The +[next section](synchronized-objects.md) introduces the `$firebaseObject` service for creating +synchronized objects. diff --git a/docs/guide/synchronized-arrays.md b/docs/guide/synchronized-arrays.md new file mode 100644 index 00000000..0fc39fe8 --- /dev/null +++ b/docs/guide/synchronized-arrays.md @@ -0,0 +1,215 @@ +# Synchronized Arrays | AngularFire Guide + +## Table of Contents + +* [Overview](#overview) +* [API Summary](#api-summary) +* [Meta Fields on the Array](#meta-fields-on-the-array) +* [Modifying the Synchronized Array](#modifying-the-synchronized-array) +* [Full Example](#full-example) + + +## Overview + +Synchronized arrays should be used for any list of objects that will be sorted, iterated, and which +have unique IDs. The synchronized array assumes that items are added using +[`$add()`](/docs/reference.md#addnewdata), and +that they will therefore be keyed using Firebase +[push IDs](https://firebase.google.com/docs/database/web/save-data). + +We create a synchronized array with the `$firebaseArray` service. The array is [sorted in the same +order](https://firebase.google.com/docs/database/web/retrieve-data#sort_data) as the records on the server. In +other words, we can pass a [query](https://firebase.google.com/docs/database/web/retrieve-data#filtering_data) +into the synchronized array, and the records will be sorted according to query criteria. + +While the array isn't technically read-only, it has some special requirements for modifying the +structure (removing and adding items) which we will cover below. Please read through this entire +section before trying any slicing or dicing of the array. + +```js +// define our app and dependencies (remember to include firebase!) +var app = angular.module("sampleApp", ["firebase"]); +// inject $firebaseArray into our controller +app.controller("ProfileCtrl", ["$scope", "$firebaseArray", + function($scope, $firebaseArray) { + var messagesRef = firebase.database().ref().child("messages"); + // download the data from a Firebase reference into a (pseudo read-only) array + // all server changes are applied in realtime + $scope.messages = $firebaseArray(messagesRef); + // create a query for the most recent 25 messages on the server + var query = messagesRef.orderByChild("timestamp").limitToLast(25); + // the $firebaseArray service properly handles database queries as well + $scope.filteredMessages = $firebaseArray(query); + } +]); +``` + +We can now utilize this array as expected with Angular directives. + +```html + +``` + +To add a button for removing messages, we can make use of `$remove()`, passing it the message we +want to remove: + +```html + +``` + +We also have access to the key for the node where each message is located via `$id`: + +```html + +``` + + +## API Summary + +The table below highlights some of the common methods on the synchronized array. The complete list +of methods can be found in the +[API documentation](/docs/reference.md#firebasearray) for +`$firebaseArray`. + +| Method | Description | +| ------------- | ------------- | +| [`$add(data)`](/docs/reference.md#addnewdata) | Creates a new record in the array. Should be used in place of `push()` or `splice()`. | +| [`$remove(recordOrIndex)`](/docs/reference.md#removerecordorindex) | Removes an existing item from the array. Should be used in place of `pop()` or `splice()`. | +| [`$save(recordOrIndex)`](/docs/reference.md#saverecordorindex) | Saves an existing item in the array. | +| [`$getRecord(key)`](/docs/reference.md#getrecordkey) | Given a Firebase database key, returns the corresponding item from the array. It is also possible to find the index with `$indexFor(key)`. | +| [`$loaded()`](/docs/reference.md#loaded-1) | Returns a promise which resolves after the initial records have been downloaded from our database. This is only called once and should be used with care. See [Extending Services](extending-services.md) for more ways to hook into server events. | + + +## Meta Fields on the Array + +Similar to synchronized objects, each item in a synchronized array will contain the following special attributes: + +| Method | Description | +| ------------- | ------------- | +| `$id` | The key for each record. This is equivalent to each record's path in our database as it would be returned by `ref.key()`. | +| `$priority` | The [priority](https://firebase.google.com/docs/database/web/retrieve-data#sorting_and_filtering_data) of each child node is stored here for reference. Changing this value and then calling `$save()` on the record will also change the priority on the server and potentially move the record in the array. | +| `$value` | If the data for this child node is a primitive (number, string, or boolean), then the record itself will still be an object. The primitive value will be stored under `$value` and can be changed and saved like any other field. | + + +## Modifying the Synchronized Array + +The contents of this array are synchronized with a remote server, and AngularFire handles adding, +removing, and ordering the elements. Because of this special arrangement, AngularFire provides the +concurrency safe `$add()`, `$remove()`, and `$save()` methods to modify the array and its elements. + +Using methods like `splice()`, `pop()`, `push()`, `shift()`, and `unshift()` will probably work for +modifying the local content, but those methods are not monitored by AngularFire and changes +introduced won't affect the content or order on the remote server. Therefore, to change the remote +data, the concurrency-safe methods should be used instead. + +```js +var messages = $FirebaseArray(ref); +// add a new record to the list +messages.$add({ + user: "physicsmarie", + text: "Hello world" +}); +// remove an item from the list +messages.$remove(someRecordKey); +// change a message and save it +var item = messages.$getRecord(someRecordKey); +item.user = "alanisawesome"; +messages.$save(item).then(function() { + // data has been saved to our database +}); +``` + + +## Full Example + +Using those methods together, we can synchronize collections between multiple clients, and +manipulate the records in the collection: + +```js +var app = angular.module("sampleApp", ["firebase"]); + +app.factory("chatMessages", ["$firebaseArray", + function($firebaseArray) { + // create a reference to the database where we will store our data + var ref = firebase.database().ref(); + + return $firebaseArray(ref); + } +]); + +app.controller("ChatCtrl", ["$scope", "chatMessages", + function($scope, chatMessages) { + $scope.user = "Guest " + Math.round(Math.random() * 100); + + $scope.messages = chatMessages; + + $scope.addMessage = function() { + // $add on a synchronized array is like Array.push() except it saves to the database! + $scope.messages.$add({ + from: $scope.user, + content: $scope.message, + timestamp: firebase.database.ServerValue.TIMESTAMP + }); + + $scope.message = ""; + }; + + // if the messages are empty, add something for fun! + $scope.messages.$loaded(function() { + if ($scope.messages.length === 0) { + $scope.messages.$add({ + from: "Uri", + content: "Hello!", + timestamp: firebase.database.ServerValue.TIMESTAMP + }); + } + }); + } +]); +``` + +```html +
+

+ Sort by: + +

+ +

Search:

+ +
    + +
  • + {{ message.from }}: {{ message.content }} + + + X +
  • +
+ +
+ + +
+
+``` + +Head on over to the [API reference](/docs/reference.md#firebasearray) +for `$firebaseArray` to see more details for each API method provided by the service. Now that we +have a grasp of synchronizing data with AngularFire, the [next section](uploading-downloading-binary-content.md) of this guide +moves on to a different aspect of building applications: binary storage. diff --git a/docs/guide/synchronized-objects.md b/docs/guide/synchronized-objects.md new file mode 100644 index 00000000..de946887 --- /dev/null +++ b/docs/guide/synchronized-objects.md @@ -0,0 +1,245 @@ +# Synchronized Objects | AngularFire Guide + +## Table of Contents + +* [Overview](#overview) +* [API Summary](#api-summary) +* [Meta Fields on the Object](#meta-fields-on-the-object) +* [Full Example](#full-example) +* [Three-way Data Bindings](#three-way-data-bindings) +* [Working With Primitives](#working-with-primitives) + + +## Overview + +Objects are useful for storing key / value pairs and singular records that are not used as a +collection. Consider the following user profile for `physicsmarie`: + +```js +{ + "profiles": { + "physicsmarie": { + name: "Marie Curie", + dob: "November 7, 1867" + } + } +} +``` + +We could fetch this profile using AngularFire's `$firebaseObject()` service. In addition to several +helper methods prefixed with `$`, the returned object would contain all of the child keys for that +record (i.e. `name` and `dob`). + +```js +// define our app and dependencies (remember to include firebase!) +var app = angular.module("sampleApp", ["firebase"]); +// inject $firebaseObject into our controller +app.controller("ProfileCtrl", ["$scope", "$firebaseObject", + function($scope, $firebaseObject) { + var ref = firebase.database().ref(); + // download physicsmarie's profile data into a local object + // all server changes are applied in realtime + $scope.profile = $firebaseObject(ref.child('profiles').child('physicsmarie')); + } +]); +``` + +The data will be requested from the server and, when it returns, AngularFire will notify the Angular +compiler to render the new content. So we can use this in our views normally. For example, the code +below would print the content of the profile in JSON format. + +```html +
{{ profile | json }}
+``` + +Changes can be saved back to the server using the +[`$save()`](/docs/reference.md#save) method. +This could, for example, be attached to an event in the DOM view, such as `ng-click` or `ng-change`. + +```html + +``` + + +## API Summary + +The synchronized object is created with several special $ properties, all of which are listed in the following table: + +| Method | Description | +| ------------- | ------------- | +| [`$save()`](/docs/reference.md#save) | Synchronizes local changes back to the remote database. | +| [`$remove()`](/docs/reference.md#remove) | Removes the object from the database, deletes the local object's keys, and sets the local object's `$value` to `null`. It's important to note that the object still exists locally, it is simply empty and we are now treating it as a primitive with a value of `null`. | +| [`$loaded()`](/docs/reference.md#loaded) | Returns a promise which is resolved when the initial server data has been downloaded. | +| [`$bindTo()`](/docs/reference.md#bindtoscope-varname) | Creates a three-way data binding. Covered below in the [Three-way Data Bindings](#three-way-data-bindings) section. | + + +## Meta Fields on the Object + +The synchronized object is created with several special `$` properties, all of which are listed in the following table: + +| Method | Description | +| ------------- | ------------- | +| [`$id`](/docs/reference.md#id) | The key for this record. This is equivalent to this object's path in our database as it would be returned by `ref.key()`. | +| [`$priority`](/docs/reference.md#priority) | The priority of each child node is stored here for reference. Changing this value and then calling `$save()` on the record will also change the object's priority on the server. | +| [`$value`](/docs/reference.md#value) | If the data in our database is a primitive (number, string, or boolean), the `$firebaseObject()` service will still return an object. The primitive value will be stored under `$value` and can be changed and saved like any other child node. See [Working with Primitives](#working-with-primitives) for more details. | + + +## Full Example + +Putting all of that together, we can generate a page for editing user profiles: + +```js +var app = angular.module("sampleApp", ["firebase"]); + +// a factory to create a re-usable profile object +// we pass in a username and get back their synchronized data +app.factory("Profile", ["$firebaseObject", + function($firebaseObject) { + return function(username) { + // create a reference to the database node where we will store our data + var ref = firebase.database().ref("rooms").push(); + var profileRef = ref.child(username); + + // return it as a synchronized object + return $firebaseObject(profileRef); + } + } +]); + +app.controller("ProfileCtrl", ["$scope", "Profile", + function($scope, Profile) { + // put our profile in the scope for use in DOM + $scope.profile = Profile("physicsmarie"); + + // calling $save() on the synchronized object syncs all data back to our database + $scope.saveProfile = function() { + $scope.profile.$save().then(function() { + alert('Profile saved!'); + }).catch(function(error) { + alert('Error!'); + }); + }; + } +]); +``` + +```html +
+ +

Edit {{ profile.$id }}

+ + +
+ + + + + + + +
+
+``` + + +## Three-way Data Bindings + +Synchronizing changes from the server is pretty magical. However, shouldn't an awesome tool like +AngularFire have some way to detect local changes as well so we don't have to call `$save()`? Of +course. We call this a *three-way data binding*. + +Simply call `$bindTo()` on a synchronized object and now any changes in the DOM are pushed to +Angular, and then automatically to our database. And inversely, any changes on the server get pushed +into Angular and straight to the DOM. + +Let's revise our previous example to get rid of the pesky save button and the `$save()` method: + +```js +var app = angular.module("sampleApp", ["firebase"]); + +// a factory to create a re-usable Profile object +// we pass in a username and get back their synchronized data as an object +app.factory("Profile", ["$firebaseObject", + function($firebaseObject) { + return function(username) { + // create a reference to the database node where we will store our data + var ref = firebase.database().ref("rooms").push(); + var profileRef = ref.child(username); + + // return it as a synchronized object + return $firebaseObject(profileRef); + } + } +]); + +app.controller("ProfileCtrl", ["$scope", "Profile", + function($scope, Profile) { + // create a three-way binding to our Profile as $scope.profile + Profile("physicsmarie").$bindTo($scope, "profile"); + } +]); +``` + +```html +
+ +

Edit {{ profile.$id }}

+ + + + + + + + +
+``` + +In this example, we've used `$bindTo()` to automatically synchronize data between the database and +`$scope.profile`. We don't need an `ng-submit` to call `$save()` anymore. AngularFire takes care of +all this automatically! + +**While three-way data bindings can be extremely convenient, be careful of trying to use them +against deeply nested tree structures. For performance reasons, stick to practical uses like +synchronizing key / value pairs that aren't changed simultaneously by several users. Do not try to +use `$bindTo()` to synchronize collections or lists of data.** + + +## Working With Primitives + +Consider the following data structure in Firebase: + +```js +{ + "foo": "bar" +} +``` + +If we attempt to synchronize `foo/` into a `$firebaseObject`, the special `$value` key is created to +store the primitive. This key only exists when the path contains no child nodes. For a path that +doesn't exist, `$value` would be set to `null`. + +```js +var ref = firebase.database().ref().child("push"); +var obj = new $firebaseObject(ref); +obj.$loaded().then(function() { + console.log(obj.$value); // "bar" +}); + +// change the value at path foo/ to "baz" +obj.$value = "baz"; +obj.$save(); + +// delete the value and see what is returned +obj.$remove().then(function() { + console.log(obj.$value); // null! +}); +``` + +Head on over to the [API reference](/docs/reference.md#firebaseobject) +for `$firebaseObject` to see more details for each API method provided by the service. But not all +of your data is going to fit nicely into a plain JavaScript object. Many times you will have lists +of data instead. In those cases, you should use AngularFire's `$firebaseArray` service, which we +will discuss in the [next section](synchronized-arrays.md). diff --git a/docs/guide/uploading-downloading-binary-content.md b/docs/guide/uploading-downloading-binary-content.md new file mode 100644 index 00000000..93fe0cb5 --- /dev/null +++ b/docs/guide/uploading-downloading-binary-content.md @@ -0,0 +1,152 @@ +# Uploading & Downloading Binary Content | AngularFire Guide + +## Table of Contents + +* [Overview](#overview) +* [API Summary](#api-summary) +* [Uploading Files](#uploading-files) +* [Displaying Images with the `firebase-src` Directive](#displaying-images-with-the-firebase-src-directive) +* [Retrieving Files from the Template](#retrieving-files-from-the-template) + +## Overview + +Firebase provides [a hosted binary storage service](https://firebase.google.com/docs/storage/) +which enables you to store and retrieve user-generated content like images, audio, and +video directly from the Firebase client SDK. + +Binary files are stored in a Cloud Storage bucket, not in the Realtime Database. +The files in your bucket are stored in a hierarchical structure, just like +in the Realtime Database. + +To use the Cloud Storage for Firebase binding, first [create a Storage reference](https://firebase.google.com/docs/storage/web/create-reference). +Then, using this reference, pass it into the `$firebaseStorage` service: + +```js +// define our app and dependencies (remember to include firebase!) +angular + .module("sampleApp", [ + "firebase" + ]) + .controller("SampleCtrl", SampleCtrl); + +// inject $firebaseStorage into our controller +function SampleCtrl($firebaseStorage) { + // create a Storage reference for the $firebaseStorage binding + var storageRef = firebase.storage().ref("userProfiles/physicsmarie"); + var storage = $firebaseStorage(storageRef); +} +SampleCtrl.$inject = ["$firebaseStorage"]; +``` + +## API Summary + +The Cloud Storage for Firebase service is created with several special `$` methods, all of which are listed in the following table: + +| Method | Description | +| ------------- | ------------- | +| [`$put(file, metadata)`](/docs/reference.md#putfile-metadata) | Uploads file to configured path with optional metadata. Returns an AngularFire wrapped [`UploadTask`](/docs/reference.md#upload-task). | +| [`$putString(string, format, metadata)`](/docs/reference.md#putstringstring-format-metadata) | Uploads a upload a raw, base64, or base64url encoded string with optional metadata. Returns an AngularFire wrapped [`UploadTask`](/docs/reference.md#upload-task). | +| [`$getDownloadURL()`](/docs/reference.md#getdownloadurl) | Returns a `Promise` fulfilled with the download URL for the file stored at the configured path. | +| [`$getMetadata()`](/docs/reference.md#getmetadata) | Returns a `Promise` fulfilled with the metadata of the file stored at the configured path. | +| [`$updateMetadata(metadata)`](/docs/reference.md#updatemetadatametadata) | Returns a `Promise` containing the updated metadata. | +| [`$delete()`](/docs/reference.md#delete) | Permanently deletes the file stored at the configured path. Returns a `Promise` that is resolved when the delete completes. | +| [`$toString()`](/docs/reference.md#tostring) | Returns a string version of the bucket path stored as a `gs://` scheme. | + + +## Uploading files +To upload files, use either the `$put()` or `$putString()` methods. These methods +return an [[`UploadTask`](/docs/reference.md#upload-task)(https://firebase.google.com/docs/reference/js/firebase.storage#uploadtask) which is wrapped by AngularFire to handle the `$digest` loop. + +```js +function SampleCtrl($firebaseStorage) { + // create a Storage reference for the $firebaseStorage binding + var storageRef = firebase.storage().ref('userProfiles/physicsmarie'); + var storage = $firebaseStorage(storageRef); + var file = // get a file from the template (see Retrieving files from template section below) + var uploadTask = storage.$put(file); + // of upload via a RAW, base64, or base64url string + var stringUploadTask = storage.$putString('5b6p5Y+344GX44G+44GX44Gf77yB44GK44KB44Gn44Go44GG77yB', 'base64'); +} +SampleCtrl.$inject = ["$firebaseStorage"]; +``` + +### Upload Task API Summary + +| Method | Description | +| ------------- | ------------- | +| [`$progress(callback)`](/docs/reference.md#progresscallback) | Calls the provided callback function whenever there is an update in the progress of the file uploading. | +| [`$error(callback)`](/docs/reference.md#errorcallback) | Calls the provided callback function when there is an error uploading the file. | +| [`$complete(callback)`](/docs/reference.md#completecallback) | Calls the provided callback function when the upload is complete. | +| [`$cancel()`](/docs/reference.md#cancel) | Cancels the upload. | +| [`$pause()`](/docs/reference.md#pause) | Pauses the upload. | +| [`$snapshot()`](/docs/reference.md#snapshot) | Returns the [current immutable view of the task](https://firebase.google.com/docs/storage/web/upload-files#monitor_upload_progress) at the time the event occurred. | +| [`then(callback)`](/docs/reference.md#then) | An [`UploadTask`](/docs/reference.md#upload-task) implements a `Promise` like interface. This callback is called when the upload is complete. | +| [`catch(callback)`](/docs/reference.md#catch) | An [`UploadTask`](/docs/reference.md#upload-task) implements a `Promise` like interface. This callback is called when an error occurs. | + +## Displaying Images with the `firebase-src` Directive + +AngularFire provides a directive that displays a file with any `src`-compatible element. Instead of using the tradional `src` attribute, use `firebase-src`: + +```html + + + +``` + +## Retrieving Files from the Template + +AngularFire does not provide a directive for retrieving an uploaded file. However, +the directive below provides a baseline to work off: + +```js +angular + .module("sampleApp", [ + "firebase" + ]) + .directive("fileUpload", FileUploadDirective); + +function FileUploadDirective() { + return { + restrict: "E", + transclude: true, + scope: { + onChange: "=" + }, + template: '', + link: function (scope, element, attrs) { + element.bind("change", function () { + scope.onChange(element.children()[0].files); + }); + } + } +} +``` + +To use this directive, create a controller to bind the `onChange()` method: + +```js +function UploadCtrl($firebaseStorage) { + var ctrl = this; + var storageRef = firebase.storage().ref("userProfiles/physicsmarie"); + var storage = $firebaseStorage(storageRef); + ctrl.fileToUpload = null; + ctrl.onChange = function onChange(fileList) { + ctrl.fileToUpload = fileList[0]; + }; +} +``` + +Then specify your template to use the directive: + +```html +
+ + Upload + +
+``` + +Head on over to the [API reference](/docs/reference.md#firebasestorage) +for `$firebaseStorage` to see more details for each API method provided by the service. Now that we +have a grasp of managing binary content with AngularFire, the [next section](user-auth.md) of this guide +moves on to a new topic: authentication. diff --git a/docs/guide/user-auth.md b/docs/guide/user-auth.md new file mode 100644 index 00000000..d127f8f4 --- /dev/null +++ b/docs/guide/user-auth.md @@ -0,0 +1,420 @@ +# User Auth | AngularFire Guide + +## Table of Contents + +* [Overview](#overview) +* [Signing Users In](#signing-users-in) +* [Managing Users](#managing-users) +* [Retrieving Authentication State](#retrieving-authentication-state) +* [User-Based Security](#user-based-security) +* [Authenticating With Routers](#authenticating-with-routers) + - [`ngRoute` Example](#ngroute-example) + - [`ui-router` Example](#ui-router-example) + + +## Overview + +Firebase provides [a hosted authentication service](https://firebase.google.com/docs/auth/) which +provides a completely client-side solution to user management and authentication. It supports +anonymous authentication, email / password sign in, and sign in via several OAuth providers, including +Facebook, GitHub, Google, and Twitter. + +Each provider has to be configured individually and also enabled from the **Auth** tab of +your [Firebase Console](https://console.firebase.google.com). Select a provider from the table below +to learn more. + +| Provider | Description | +|----------|-------------| +| [Custom](https://firebase.google.com/docs/auth/web/custom-auth) | Generate your own authentication tokens. Use this to integrate with existing authentication systems. You can also use this to authenticate server-side workers. | +| [Email & Password](https://firebase.google.com/docs/auth/web/password-auth) | Let Firebase manage passwords for you. Register and authenticate users by email & password. | +| [Anonymous](https://firebase.google.com/docs/auth/web/anonymous-auth) | Build user-centric functionality without requiring users to share their personal information. Anonymous authentication generates a unique identifier for each user that lasts as long as their session. | +| [Facebook](https://firebase.google.com/docs/auth/web/facebook-login) | Authenticate users with Facebook by writing only client-side code. | +| [Twitter](https://firebase.google.com/docs/auth/web/twitter-login) | Authenticate users with Twitter by writing only client-side code. | +| [GitHub](https://firebase.google.com/docs/auth/web/github-auth) | Authenticate users with GitHub by writing only client-side code. | +| [Google](https://firebase.google.com/docs/auth/web/google-signin) | Authenticate users with Google by writing only client-side code. | + +AngularFire provides a service named `$firebaseAuth` which wraps the authentication methods provided +by the Firebase client library. It can be injected into any controller, service, or factory. + +```js +// define our app and dependencies (remember to include firebase!) +var app = angular.module("sampleApp", ["firebase"]); + +// inject $firebaseAuth into our controller +app.controller("SampleCtrl", ["$scope", "$firebaseAuth", + function($scope, $firebaseAuth) { + var auth = $firebaseAuth(); + } +]); +``` + + +## Signing Users In + +The `$firebaseAuth` service has methods for each authentication type. For example, to authenticate +an anonymous user, you can use `$signInAnonymously()`: + +```js +var app = angular.module("sampleApp", ["firebase"]); + +app.controller("SampleCtrl", ["$scope", "$firebaseAuth", + function($scope, $firebaseAuth) { + var auth = $firebaseAuth(); + + $scope.signIn = function() { + $scope.firebaseUser = null; + $scope.error = null; + + auth.$signInAnonymously().then(function(firebaseUser) { + $scope.firebaseUser = firebaseUser; + }).catch(function(error) { + $scope.error = error; + }); + }; + } +]); +``` + +```html +
+ + +

Signed in user: {{ firebaseUser.uid }}

+

Error: {{ error }}

+
+``` + + +## Managing Users + +The `$firebaseAuth` service also provides [a full suite of methods](/docs/reference.md#firebaseauth) +for managing users. This includes methods for creating and removing users, changing an users's email +or password, and sending email verification and password reset emails. The following example gives +you a taste of just how easy this is: + +```js +var app = angular.module("sampleApp", ["firebase"]); + +// let's create a re-usable factory that generates the $firebaseAuth instance +app.factory("Auth", ["$firebaseAuth", + function($firebaseAuth) { + return $firebaseAuth(); + } +]); + +// and use it in our controller +app.controller("SampleCtrl", ["$scope", "Auth", + function($scope, Auth) { + $scope.createUser = function() { + $scope.message = null; + $scope.error = null; + + // Create a new user + Auth.$createUserWithEmailAndPassword($scope.email, $scope.password) + .then(function(firebaseUser) { + $scope.message = "User created with uid: " + firebaseUser.uid; + }).catch(function(error) { + $scope.error = error; + }); + }; + + $scope.deleteUser = function() { + $scope.message = null; + $scope.error = null; + + // Delete the currently signed-in user + Auth.$deleteUser().then(function() { + $scope.message = "User deleted"; + }).catch(function(error) { + $scope.error = error; + }); + }; + } +]); +``` + +```html +
+ Email: + Password: + +

+ + + +

+ + + +

Message: {{ message }}

+

Error: {{ error }}

+
+``` + + +## Retrieving Authentication State + +Whenever a user is authenticated, you can use the synchronous [`$getAuth()`](/docs/reference.md#getauth) +method to retrieve the client's current authentication state. This includes the authenticated user's +`uid` (a user identifier which is unique across all providers), the `providerId` used to +authenticate (e.g. `google.com`, `facebook.com`), as well as other properties +[listed here](https://firebase.google.com/docs/reference/js/firebase.User#properties). Additional +variables are included for each specific provider and are covered in the provider-specific links in +the table above. + +In addition to the synchronous `$getAuth()` method, there is also an asynchronous +[`$onAuthStateChanged()`](/docs/reference.md#onauthstatechangedcallback-context) method which fires a +user-provided callback every time authentication state changes. This is often more convenient than +using `$getAuth()` since it gives you a single, consistent place to handle updates to authentication +state, including users signing in or out and sessions expiring. + +Pulling some of these concepts together, we can create a sign in form with dynamic content based on +the user's current authentication state: + +```js +var app = angular.module("sampleApp", ["firebase"]); + +app.factory("Auth", ["$firebaseAuth", + function($firebaseAuth) { + return $firebaseAuth(); + } +]); + +app.controller("SampleCtrl", ["$scope", "Auth", + function($scope, Auth) { + $scope.auth = Auth; + + // any time auth state changes, add the user data to scope + $scope.auth.$onAuthStateChanged(function(firebaseUser) { + $scope.firebaseUser = firebaseUser; + }); + } +]); +``` + +```html +
+
+

Hello, {{ firebaseUser.displayName }}

+ +
+
+

Welcome, please sign in.

+ +
+
+``` + +The `ng-show` and `ng-hide` directives dynamically change out the content based on the +authentication state, by checking to see if `firebaseUser` is not `null`. The sign in and sign out +methods were bound directly from the view using `ng-click`. + + +## User-Based Security + +Authenticating users is only one piece of making an application secure. It is critical to configure +Security and Firebase Rules before going into production. These declarative rules dictate when and +how data may be read or written. + +Within our [Firebase and Security Rules](https://firebase.google.com/docs/database/security/), the +predefined `auth` variable is `null` before authentication takes place. Once a user is authenticated, +it will contain the following attributes: + +| Key | Description | +|-----|-------------| +| `uid` | A user ID, guaranteed to be unique across all providers. | +| `provider` | The authentication method used (e.g. "anonymous" or "google.com"). | +| `token` | The contents of the authentication token (an OpenID JWT). | + +The contents of `auth.token` will contain the following information: + +``` +{ + "email": "foo@bar.com", // The email corresponding to the authenticated user + "email_verified": false, // Whether or not the above email is verified + "exp": 1465366314, // JWT expiration time + "iat": 1465279914, // JWT issued-at time + "sub": "g8u5h1i3t51b5", // JWT subject (same as auth.uid) + "auth_time": 1465279914, // When the original authentication took place + "firebase": { // Firebase-namespaced claims + "identities": { // Authentication identities + "github.com": [ // Provider + "8513515" // ID of the user on the above provider + ] + } + } +} +``` + +We can then use the `auth` variable within our rules. For example, we can grant everyone read access +to all data, but only write access to their own data, our rules would look like this: + +```js +{ + "rules": { + // public read access + ".read": true, + "users": { + "$uid": { + // write access only to your own data + ".write": "$uid === auth.uid", + } + } + } +} +``` + +For a more in-depth explanation of this important feature, check out the web guide on +[user-based security](https://firebase.google.com/docs/database/security/user-security). + + +## Authenticating With Routers + +Checking to make sure the client has authenticated can be cumbersome and lead to a lot of `if` / +`else` logic in our controllers. In addition, apps which use authentication often have issues upon +initial page load with the signed out state flickering into view temporarily before the +authentication check completes. We can abstract away these complexities by taking advantage of the +`resolve()` method of Angular routers. + +AngularFire provides two helper methods to use with Angular routers. The first is +[`$waitForSignIn()`](/docs/reference.md#waitforsignin) +which returns a promise fulfilled with the current authentication state. This is useful when you +want to grab the authentication state before the route is rendered. The second helper method is +[`$requireSignIn()`](/docs/reference.md#requiresignin) +which resolves the promise successfully if a user is authenticated and rejects otherwise. This is +useful in cases where you want to require a route to have an authenticated user. You can catch the +rejected promise and redirect the unauthenticated user to a different page, such as the sign in page. +Both of these methods work well with the `resolve()` methods of `ngRoute` and `ui-router`. + +### `ngRoute` Example + +```js +// for ngRoute +app.run(["$rootScope", "$location", function($rootScope, $location) { + $rootScope.$on("$routeChangeError", function(event, next, previous, error) { + // We can catch the error thrown when the $requireSignIn promise is rejected + // and redirect the user back to the home page + if (error === "AUTH_REQUIRED") { + $location.path("/home"); + } + }); +}]); + +app.config(["$routeProvider", function($routeProvider) { + $routeProvider.when("/home", { + // the rest is the same for ui-router and ngRoute... + controller: "HomeCtrl", + templateUrl: "views/home.html", + resolve: { + // controller will not be loaded until $waitForSignIn resolves + // Auth refers to our $firebaseAuth wrapper in the factory below + "currentAuth": ["Auth", function(Auth) { + // $waitForSignIn returns a promise so the resolve waits for it to complete + return Auth.$waitForSignIn(); + }] + } + }).when("/account", { + // the rest is the same for ui-router and ngRoute... + controller: "AccountCtrl", + templateUrl: "views/account.html", + resolve: { + // controller will not be loaded until $requireSignIn resolves + // Auth refers to our $firebaseAuth wrapper in the factory below + "currentAuth": ["Auth", function(Auth) { + // $requireSignIn returns a promise so the resolve waits for it to complete + // If the promise is rejected, it will throw a $routeChangeError (see above) + return Auth.$requireSignIn(); + }] + } + }); +}]); + +app.controller("HomeCtrl", ["currentAuth", function(currentAuth) { + // currentAuth (provided by resolve) will contain the + // authenticated user or null if not signed in +}]); + +app.controller("AccountCtrl", ["currentAuth", function(currentAuth) { + // currentAuth (provided by resolve) will contain the + // authenticated user or throw a $routeChangeError (see above) if not signed in +}]); + +app.factory("Auth", ["$firebaseAuth", + function($firebaseAuth) { + return $firebaseAuth(); + } +]); +``` + +### `ui-router` Example + +```js +// for ui-router +app.run(["$rootScope", "$state", function($rootScope, $state) { + $rootScope.$on("$stateChangeError", function(event, toState, toParams, fromState, fromParams, error) { + // We can catch the error thrown when the $requireSignIn promise is rejected + // and redirect the user back to the home page + if (error === "AUTH_REQUIRED") { + $state.go("home"); + } + }); +}]); + +app.config(["$stateProvider", function ($stateProvider) { + $stateProvider + .state("home", { + // the rest is the same for ui-router and ngRoute... + controller: "HomeCtrl", + templateUrl: "views/home.html", + resolve: { + // controller will not be loaded until $waitForSignIn resolves + // Auth refers to our $firebaseAuth wrapper in the factory below + "currentAuth": ["Auth", function(Auth) { + // $waitForSignIn returns a promise so the resolve waits for it to complete + return Auth.$waitForSignIn(); + }] + } + }) + .state("account", { + // the rest is the same for ui-router and ngRoute... + controller: "AccountCtrl", + templateUrl: "views/account.html", + resolve: { + // controller will not be loaded until $requireSignIn resolves + // Auth refers to our $firebaseAuth wrapper in the factory below + "currentAuth": ["Auth", function(Auth) { + // $requireSignIn returns a promise so the resolve waits for it to complete + // If the promise is rejected, it will throw a $stateChangeError (see above) + return Auth.$requireSignIn(); + }] + } + }); +}]); + +app.controller("HomeCtrl", ["currentAuth", function(currentAuth) { + // currentAuth (provided by resolve) will contain the + // authenticated user or null if not signed in +}]); + +app.controller("AccountCtrl", ["currentAuth", function(currentAuth) { + // currentAuth (provided by resolve) will contain the + // authenticated user or throw a $stateChangeError (see above) if not signed in +}]); + +app.factory("Auth", ["$firebaseAuth", + function($firebaseAuth) { + return $firebaseAuth(); + } +]); +``` +Keep in mind that, even when using `ng-annotate` or `grunt-ngmin` to minify code, that these tools +cannot peer inside of functions. So even though we don't need the array notation to declare our +injected dependencies for our controllers, services, etc., we still need to use an array and +explicitly state our dependencies for the routes, since they are inside of a function. + +We have covered the three services AngularFire provides: +[`$firebaseObject`](/docs/reference.md#firebaseobject), +[`$firebaseArray`](/docs/reference.md#firebasearray), and +[`$firebaseAuth`](/docs/reference.md#firebaseauth). +In the [next section](extending-services.md) we will discuss the advanced topic of extending the +functionality of the `$firebaseObject` and `$firebaseArray` services. diff --git a/docs/migration/09X-to-1XX.md b/docs/migration/09X-to-1XX.md new file mode 100644 index 00000000..ff4cdf0b --- /dev/null +++ b/docs/migration/09X-to-1XX.md @@ -0,0 +1,228 @@ +# Migrating from AngularFire `0.9.x` to `1.x.x` + +This migration guide will walk through some of the major breaking changes with code samples and +guidance to upgrade your existing application from AngularFire version `0.9.x` to `1.x.x`. + + +## Removal of `$firebase` + +The largest breaking change in AngularFire `1.0.0` is the removal of the `$firebase` service. The +service did not provide any capabilities beyond what already existed in the vanilla Firebase SDK. +However, sometimes you do need to quickly write data from your database without first going through +the process of creating a synchronized array or object. Let's walk through some examples of +migrating away from the now defunct `$firebase` service. + +To write data to your database, we should now use the Firebase SDK's `set()` method instead of the +removed `$set()` method. + +### AngularFire `0.9.X` +```js +app.controller("SampleCtrl", ["$scope", "$firebase", + function($scope, $firebase) { + var profileRef = new Firebase("https://.firebaseio.com/profiles/annie"); + var profileSync = $firebase(profileRef); + profileSync.$set({ age: 24, gender: "female" }).then(function() { + console.log("Profile set successfully!"); + }).catch(function(error) { + console.log("Error:", error); + }); + } +]); +``` + +### AngularFire `1.X.X` +```js +app.controller("SampleCtrl", ["$scope", + function($scope) { + var profileRef = new Firebase("https://.firebaseio.com/profiles/annie"); + profileRef.set({ age: 24, gender: "female" }, function(error) { + if (error) { + console.log("Error:", error); + } else { + console.log("Profile set successfully!"); + } + }); + } +]); +``` + +We should similarly use the Firebase SDK's `remove()` method to easily replace the `$remove()` +method provided by the `$firebase` service. + +### AngularFire `0.9.X` +```js +app.controller("SampleCtrl", ["$scope", "$firebase", + function($scope, $firebase) { + var profileRef = new Firebase("https://.firebaseio.com/profiles/bobby"); + var profileSync = $firebase(profileRef); + profileSync.$remove().then(function() { + console.log("Set successful!"); + }).catch(function(error) { + console.log("Error:", error); + }); + } +]); +``` + +### AngularFire `1.X.X` +```js +app.controller("SampleCtrl", ["$scope", + function($scope) { + var profileRef = new Firebase("https://.firebaseio.com/profiles/bobby"); + profileRef.remove(function(error) { + if (error) { + console.log("Error:", error); + } else { + console.log("Profile removed successfully!"); + } + }); + } +]); +``` + +Replacements for the `$asArray()` and `$asObject()` methods are given below. + + +## Replacement of `$asObject()` with `$firebaseObject` + +Due to the removal of `$firebase`, the process of creating an instance of a synchronized object has +changed. Instead of creating an instance of the `$firebase` service and calling its `$asObject()` +method, use the renamed `$firebaseObject` service directly. + +### AngularFire `0.9.X` +```js +app.controller("SampleCtrl", ["$scope", "$firebase", + function($scope, $firebase) { + var ref = new Firebase("https://.firebaseio.com"); + var sync = $firebase(ref); + var obj = sync.$asObject(); + } +]); +``` + +### AngularFire `1.X.X` +```js +// Inject $firebaseObject instead of $firebase +app.controller("SampleCtrl", ["$scope", "$firebaseObject", + function($scope, $firebaseObject) { + var ref = new Firebase("https://.firebaseio.com"); + // Pass the Firebase reference to $firebaseObject directly + var obj = $firebaseObject(ref); + } +]); +``` + +## Replacement of `$asArray()` with `$firebaseArray` + +Due to the removal of `$firebase`, the process of creating an instance of a synchronized array has +changed. Instead of creating an instance of the `$firebase` service and calling its `$asArray()` +method, use the renamed `$firebaseArray` service directly. + +### AngularFire `0.9.X` +```js +app.controller("SampleCtrl", ["$scope", "$firebase", + function($scope, $firebase) { + var ref = new Firebase("https://.firebaseio.com"); + var sync = $firebase(ref); + var list = sync.$asArray(); + } +]); +``` + +### AngularFire `1.X.X` +```js +// Inject $firebaseArray instead of $firebase +app.controller("SampleCtrl", ["$scope", "$firebaseArray", + function($scope, $firebaseArray) { + var ref = new Firebase("https://.firebaseio.com"); + // Pass the Firebase reference to $firebaseArray + var list = $firebaseArray(ref); + } +]); +``` + + +## Replacement of `$inst()` with `$ref()` + +Due to the removal of `$firebase`, the `$inst()` methods off of the old `$FirebaseObject` and +`$FirebaseArray` factories were no longer meaningful. They have been replaced with `$ref()` methods +off of the new `$firebaseObject` and `$firebaseArray` services which return the underlying +`Firebase` reference used to instantiate an instance of the services. + +### AngularFire `0.9.X` +```js +// $FirebaseObject +var objSync = $firebase(ref); +var obj = sync.$asObject(); +objSync === obj.$inst(); // true +// $FirebaseArray +var listSync = $firebase(ref); +var list = sync.$asArray(); +listSync === list.$inst(); // true +``` + +### AngularFire `1.X.X` +```js +// $firebaseObject +var obj = $firebaseObject(ref); +obj.$ref() === ref; // true +// $firebaseArray +var list = $firebaseArray(ref); +list.$ref() === ref; // true +``` + + +## Changes to argument lists for user management methods + +The previously deprecated ability to pass in credentials to the user management methods of +`$firebaseAuth` as individual arguments has been removed in favor of a single credentials argument + +### AngularFire `0.9.X` +```js +var auth = $firebaseAuth(ref); +auth.$changePassword("foo@bar.com", "somepassword", "otherpassword").then(function() { + console.log("Password changed successfully!"); +}).catch(function(error) { + console.error("Error: ", error); +}); +``` + +### AngularFire `1.X.X` +```js +var auth = $firebaseAuth(ref); +auth.$changePassword({ + email: "foo@bar.com", + oldPassword: "somepassword", + newPassword: "otherpassword" +}).then(function() { + console.log("Password changed successfully!"); +}).catch(function(error) { + console.error("Error: ", error); +}); +``` + + +## Replacement of `$sendPasswordResetEmail()` with `$resetPassword()` + +The `$sendPasswordResetEmail()` method has been removed in favor of the functionally equivalent +`$resetPassword()` method. + +### AngularFire `0.9.X` +```js +var auth = $firebaseAuth(ref); +auth.$sendPasswordResetEmail("foo@bar.com").then(function() { + console.log("Password reset email sent successfully!"); +}).catch(function(error) { + console.error("Error: ", error); +}); +``` + +### AngularFire `1.X.X` +```js +var auth = $firebaseAuth(ref); +auth.$resetPassword("foo@bar.com").then(function() { + console.log("Password reset email sent successfully!"); +}).catch(function(error) { + console.error("Error: ", error); +}); +``` diff --git a/docs/migration/1XX-to-2XX.md b/docs/migration/1XX-to-2XX.md new file mode 100644 index 00000000..c77d1c3f --- /dev/null +++ b/docs/migration/1XX-to-2XX.md @@ -0,0 +1,64 @@ +# Migrating from AngularFire `1.x.x` to `2.x.x` + +This migration document covers all the major breaking changes mentioned in the [AngularFire `2.0.0` +change log](https://github.com/firebase/angularfire/releases/tag/v2.0.0). + +**Note:** If you're using Angular 2 this is not the guide for you! This is for upgrading AngularFire +`1.x.x` (for classic Angular) to AngularFire `2.x.x`. See [AngularFire 2](https://github.com/angular/angularfire2) +to use Firebase with Angular 2. + + +## Upgrade to the Firebase `3.x.x` SDK + +Ensure you're using a `3.x.x` version of the Firebase SDK in your project. Version `2.x.x` of the +Firebase SDK is no longer supported with AngularFire version `2.x.x`. + +| SDK Version | AngularFire Version Supported | +|-------------|-------------------------------| +| 3.x.x | 2.x.x | +| 2.x.x | 1.x.x | + +Consult the Firebase [web / Node.js migration guide](https://firebase.google.com/support/guides/firebase-web) +for details on how to upgrade to the Firebase `3.x.x` SDK. + + +## `$firebaseAuth` Method Renames / Signature Changes + +The `$firebaseAuth` service now accepts an optional Firebase `auth` instance instead of a Firebase +Database reference. + +```js +// Old +$firebaseAuth(ref); + +// New +$firebaseAuth(); +// Or if you need to explicitly provide an auth instance +$firebaseAuth(firebase.auth()); +``` + +Several authentication methods have been renamed and / or have different method signatures: + +| Old Method | New Method | Notes | +|------------|------------|------------------| +| `$authAnonymously(options)` | `$signInAnonymously()` | No longer takes any arguments | +| `$authWithPassword(credentials)` | `$signInWithEmailAndPassword(email, password)` | | +| `$authWithCustomToken(token)` | `$signInWithCustomToken(token)` | | +| `$authWithOAuthPopup(provider[, options])` | `$signInWithPopup(provider)` | `options` can be provided by passing a configured `firebase.database.AuthProvider` instead of a `provider` string | +| `$authWithOAuthRedirect(provider[, options])` | `$signInWithRedirect(provider)` | `options` can be provided by passing a configured `firebase.database.AuthProvider` instead of a `provider` string | +| `$authWithOAuthToken(provider, token)` | `$signInWithCredential(credential)` | Tokens must now be transformed into provider specific credentials. This is discussed more in the [Firebase Authentication guide](https://firebase.google.com/docs/auth/#key_functions). | +| `$createUser(credentials)` | `$createUserWithEmailAndPassword(email, password)` | | +| `$removeUser(credentials)` | `$deleteUser()` | Deletes the currently signed-in user | +| `$changeEmail(credentials)` | `$updateEmail(newEmail)` | Changes the email of the currently signed-in user | +| `$changePassword(credentials)` | `$updatePassword(newPassword)` | Changes the password of the currently signed-in user | +| `$resetPassword(credentials)` | `$sendPasswordResetEmail(email)` | | +| `$unauth()` | `$signOut()` | Now returns a `Promise` | +| `$onAuth(callback)` | `$onAuthStateChanged(callback)` | | +| `$requireAuth()` | `$requireSignIn()` | | +| `$waitForAuth()` | `$waitForSignIn()` | | + +## Auth Payload Format Changes + +Although all your promises and `$getAuth()` calls will continue to function, the auth payload will +differ slightly. Ensure that your code is expecting the new payload that is documented in the +[Firebase Authentication guide](https://firebase.google.com/docs/auth/). diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 00000000..40d74423 --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,222 @@ +# Quickstart | AngularFire + +AngularFire is the officially supported AngularJS binding for Firebase. The combination of Angular +and Firebase provides a three-way data binding between your HTML, your JavaScript, and the Firebase +database. + + +## 1. Create an Account +The first thing we need to do is [sign up for a free Firebase account](https://firebase.google.com/). +A brand new Firebase project will automatically be created for you which you will use in conjunction +with AngularFire to authenticate users and store and sync data. + + +## 2. Add Script Dependencies + +In order to use AngularFire in a project, include the following script tags: + +```html + + + + + + + + +``` + +Firebase and AngularFire are also available via npm and Bower as `firebase` and `angularfire`, +respectively. A [Yeoman generator](https://github.com/firebase/generator-angularfire) is also +available. + + +## 3. Initialize the Firebase SDK + +We'll need to initialize the Firebase SDK before we can use it. You can find more details on the +[web](https://firebase.google.com/docs/web/setup) or +[Node](https://firebase.google.com/docs/server/setup) setup guides. + +```js + +``` + + +## 4. Inject the AngularFire Services + +Before we can use AngularFire with dependency injection, we need to register `firebase` as a module +in our application. + +```js +var app = angular.module("sampleApp", ["firebase"]); +``` + +Now the `$firebaseObject`, `$firebaseArray`, and `$firebaseAuth` services are available to be +injected into any controller, service, or factory. + +```js +app.controller("SampleCtrl", function($scope, $firebaseObject) { + var ref = firebase.database().ref(); + // download the data into a local object + $scope.data = $firebaseObject(ref); + // putting a console.log here won't work, see below +}); +``` + +In the example above, `$scope.data` is going to be populated from the remote server. This is an +asynchronous call, so it will take some time before the data becomes available in the controller. +While it might be tempting to put a `console.log` on the next line to read the results, the data +won't be downloaded yet, so the object will appear to be empty. Read the section on +[Asynchronous Operations](guide/introduction-to-angularfire.md#handling-asynchronous-operations) for more details. + + +## 5. Add Three-Way, Object Bindings + +Angular is known for its two-way data binding between JavaScript models and the DOM, and Firebase +has a lightning-fast, realtime database. For synchronizing simple key / value pairs, AngularFire can +be used to *glue* the two together, creating a "three-way data binding" which automatically +synchronizes any changes to your DOM, your JavaScript, and the Firebase database. + +To set up this three-way data binding, we use the `$firebaseObject` service introduced above to +create a synchronized object, and then call `$bindTo()`, which binds it to a `$scope` variable. + +```js +var app = angular.module("sampleApp", ["firebase"]); +app.controller("SampleCtrl", function($scope, $firebaseObject) { + var ref = firebase.database().ref().child("data"); + // download the data into a local object + var syncObject = $firebaseObject(ref); + // synchronize the object with a three-way data binding + // click on `index.html` above to see it used in the DOM! + syncObject.$bindTo($scope, "data"); +}); +``` + +```html + + + + + +

You said: {{ data.text }}

+ + +``` + + +## 6. Synchronize Collections as Arrays + +Three-way data bindings are amazing for simple key / value data. However, there are many times when +an array would be more practical, such as when managing a collection of messages. This is done using +the `$firebaseArray` service. + +We synchronize a list of messages into a read-only array by using the `$firebaseArray` service and +then assigning the array to `$scope`: + +```js +var app = angular.module("sampleApp", ["firebase"]); +app.controller("SampleCtrl", function($scope, $firebaseArray) { + var ref = firebase.database().ref().child("messages"); + // create a synchronized array + // click on `index.html` above to see it used in the DOM! + $scope.messages = $firebaseArray(ref); +}); +``` + +```html + + +
    +
  • {{ message.text }}
  • +
+ + +``` + +Because the array is synchronized with server data and being modified concurrently by the client, it +is possible to lose track of the fluid array indices and corrupt the data by manipulating the wrong +records. Therefore, the placement of items in the list should never be modified directly by using +array methods like `push()` or `splice()`. + +Instead, AngularFire provides a set of methods compatible with manipulating synchronized arrays: +`$add()`, `$save()`, and `$remove()`. + +```js +var app = angular.module("sampleApp", ["firebase"]); +app.controller("SampleCtrl", function($scope, $firebaseArray) { + var ref = firebase.database().ref().child("messages"); + // create a synchronized array + $scope.messages = $firebaseArray(ref); + // add new items to the array + // the message is automatically added to our Firebase database! + $scope.addMessage = function() { + $scope.messages.$add({ + text: $scope.newMessageText + }); + }; + // click on `index.html` above to see $remove() and $save() in action +}); +``` + +```html + + +
    +
  • + + + + +
  • +
+ +
+ + +
+ + +``` + + +## 7. Add Authentication + +Firebase provides a [hosted authentication service](https://firebase.google.com/docs/auth/) which +offers a completely client-side solution to account management and authentication. It supports +anonymous authentication, email / password login, and login via several OAuth providers, including +Facebook, GitHub, Google, and Twitter. + +AngularFire provides a service named `$firebaseAuth` which wraps the authentication methods provided +by the Firebase client library. It can be injected into any controller, service, or factory. + +```js +app.controller("SampleCtrl", function($scope, $firebaseAuth) { + var auth = $firebaseAuth(); + + // login with Facebook + auth.$signInWithPopup("facebook").then(function(firebaseUser) { + console.log("Signed in as:", firebaseUser.uid); + }).catch(function(error) { + console.log("Authentication failed:", error); + }); +}); +``` + + +## 8. Next Steps + +This was just a quick run through of the basics of AngularFire. For a more in-depth explanation of +how to use the library as well as a handful of live code examples, [continue reading the AngularFire +Guide](guide/README.md). + +To deploy your Angular applications free, fast, and without fuss, do it Firebase style! Check out +[Firebase Hosting](https://firebase.google.com/docs/hosting/) for more information. diff --git a/docs/reference.md b/docs/reference.md new file mode 100644 index 00000000..ee08c0cd --- /dev/null +++ b/docs/reference.md @@ -0,0 +1,1551 @@ +# API Reference | AngularFire + +## Table of Contents + +* [Initialization](#initialization) +* [`$firebaseObject`](#firebaseobject) + * [`$remove()`](#remove) + * [`$save()`](#save) + * [`$loaded()`](#loaded) + * [`$ref()`](#ref) + * [`$bindTo(scope, varName)`](#bindtoscope-varname) + * [`$watch(callback, context)`](#watchcallback-context) + * [`$destroy()`](#destroy) + * [`$resolved`](#resolved) +* [`$firebaseArray`](#firebasearray) + * [`$add(newData)`](#addnewdata) + * [`$remove(recordOrIndex)`](#removerecordorindex) + * [`$save(recordOrIndex)`](#saverecordorindex) + * [`$getRecord(key)`](#getrecordkey) + * [`$keyAt(recordOrIndex)`](#keyatrecordorindex) + * [`$indexFor(key)`](#indexforkey) + * [`$loaded()`](#loaded-1) + * [`$ref()`](#ref-1) + * [`$watch(cb[, context])`](#watchcb-context) + * [`$destroy()`](#destroy-1) + * [`$resolved`](#resolved-1) +* [`$firebaseAuth`](#firebaseauth) + * Authentication + * [`$signInWithCustomToken(authToken)`](#signinwithcustomtokenauthtoken) + * [`$signInAnonymously()`](#signinanonymously) + * [`$signInWithEmailAndPassword(email, password)`](#signinwithemailandpasswordemail-password) + * [`$signInWithPopup(provider)`](#signinwithpopupprovider) + * [`$signInWithRedirect(provider[, options])`](#signinwithredirectprovider-options) + * [`$signInWithCredential(credential)`](#signinwithcredentialcredential) + * [`$getAuth()`](#getauth) + * [`$onAuthStateChanged(callback[, context])`](#onauthstatechangedcallback-context) + * [`$signOut()`](#signout) + * User Management + * [`$createUserWithEmailAndPassword(email, password)`](#createuserwithemailandpasswordemail-password) + * [`$updatePassword(password)`](#updatepasswordnewpassword) + * [`$updateEmail(email)`](#updateemailnewemail) + * [`$deleteUser()`](#deleteuser) + * [`$sendPasswordResetEmail(email)`](#sendpasswordresetemailemail) + * Router Helpers + * [`$waitForSignIn()`](#waitforsignin) + * [`$requireSignIn(requireEmailVerification)`](#requiresigninrequireemailverification) +* [`$firebaseStorage`](#firebasestorage) + * [`$put(file, metadata)`](#putfile-metadata) + * [`$putString(string, format, metadata)`](#putstringstring-format-metadata) + * [`$getDownloadURL()`](#getdownloadurl) + * [`$getMetadata()`](#getmetadata) + * [`$updateMetadata(metadata)`](#updatemetadatametadata) + * [`$delete()`](#delete) + * [`$toString()`](#tostring) + * [Upload Task](#upload-task) + * [`$progress(callback)`](#progresscallback) + * [`$complete(callback)`](#completecallback) + * [`$error(callback)`](#errorcallback) + * [`$cancel()`](#cancel) + * [`$pause()`](#pause) + * [`$snapshot()`](#snapshot) + * [`then(callback)`](#then) + * [`catch(callback)`](#catch) +* [Extending the Services](#extending-the-services) + * [Extending `$firebaseObject`](#extending-firebaseobject) + * [Extending `$firebaseArray`](#extending-firebasearray) + * [Passing a Class into $extend](#passing-a-class-into-extend) + * [Decorating the Services](#decorating-the-services) + * [Creating AngularFire Services](#creating-angularfire-services) +* [SDK Compatibility](#sdk-compatibility) +* [Browser Compatibility](#browser-compatibility) + + +## Initialization + +```js +var app = angular.module("app", ["firebase"]); +app.config(function() { + var config = { + apiKey: "", // Your Firebase API key + authDomain: "", // Your Firebase Auth domain ("*.firebaseapp.com") + databaseURL: "", // Your Firebase Database URL ("https://*.firebaseio.com") + storageBucket: "" // Your Cloud Storage for Firebase bucket ("*.appspot.com") + }; + firebase.initializeApp(config); +}); +``` + + +## $firebaseObject + +The `$firebaseObject` service takes an optional [`firebase.database.Reference`](https://firebase.google.com/docs/reference/js/#firebase.database.Reference) or +[`firebase.database.Query`](https://firebase.google.com/docs/reference/js/#firebase.database.Query) and returns a JavaScript object which contains the data at the +provided location in Firebase and some extra AngularFire-specific fields. If no `Reference` or `Query` is provided, then the root of the Firebase Database will be used. +Note that the data will +not be available immediately since retrieving it is an asynchronous operation. You can use the +`$loaded()` promise to get notified when the data has loaded. + +This service automatically keeps local objects in sync with any changes made to the remote Firebase database. +**However, note that any changes to that object will *not* automatically result in any changes +to the remote data**. All such changes will have to be performed by updating the object directly and +then calling `$save()` on the object, or by utilizing `$bindTo()` (see more below). + +```js +app.controller("MyCtrl", ["$scope", "$firebaseObject", + function($scope, $firebaseObject) { + var ref = firebase.database().ref(); + + var obj = $firebaseObject(ref); + + // to take an action after the data loads, use the $loaded() promise + obj.$loaded().then(function() { + console.log("loaded record:", obj.$id, obj.someOtherKeyInData); + + // To iterate the key/value pairs of the object, use angular.forEach() + angular.forEach(obj, function(value, key) { + console.log(key, value); + }); + }); + + // To make the data available in the DOM, assign it to $scope + $scope.data = obj; + + // For three-way data bindings, bind it to the scope instead + obj.$bindTo($scope, "data"); + } +]); +``` + +#### $id + +The key where this record is stored. The same as `obj.$ref().key`. + +#### $priority + +The priority for this record according to the last update we received. Modifying this value +and then calling `$save()` will also update the server's priority. + +**IMPORTANT NOTE**: Because Angular's `$watch()` function ignores keys prefixed with `$`, changing +this field inside the `$bindTo()` function will not trigger an update unless a field without a `$` +prefix is also updated. It is best to avoid using `$bindTo()` for editing `$` variables and just +rely on the `$save()` method. + +#### $value + +If the value in the database is a primitive (boolean, string, or number) then the value will +be stored under this `$value` key. Modifying this value and then calling `$save()` will also +update the server's value. + +Note that any time other keys exist, this one will be ignored. To change an object to +a primitive value, delete the other keys and add this key to the object. As a shortcut, we can use: + +```js +var obj = $firebaseObject(ref); // an object with data keys +$firebaseUtils.updateRec(obj, newPrimitiveValue); // updateRec will delete the other keys for us +``` + +**IMPORTANT NOTE**: Because Angular's `$watch()` function ignores keys prefixed with `$`, changing +this field inside the `$bindTo()` function will not trigger an update unless a field without a `$` +prefix is also updated. It is best to avoid using `$bindTo()` for editing `$` variables and just +rely on the `$save()` method. + +### $remove() + +Removes the entire object locally and from the database. This method returns a promise that will be +fulfilled when the data has been removed from the server. The promise will be resolved with a +`Firebase` reference for the exterminated record. + +```js +var obj = $firebaseObject(ref); +obj.$remove().then(function(ref) { + // data has been deleted locally and in the database +}, function(error) { + console.log("Error:", error); +}); +``` + +### $save() + +If changes are made to data, then calling `$save()` will push those changes to the server. This +method returns a promise that will resolve with this object's `Firebase` reference when the write +is completed. + +```js +var obj = $firebaseObject(ref); +obj.foo = "bar"; +obj.$save().then(function(ref) { + ref.key === obj.$id; // true +}, function(error) { + console.log("Error:", error); +}); +``` + +### $loaded() + +Returns a promise which is resolved asynchronously when the initial object data has been downloaded +from the database. The promise resolves to the `$firebaseObject` itself. + +```js +var obj = $firebaseObject(ref); +obj.$loaded() + .then(function(data) { + console.log(data === obj); // true + }) + .catch(function(error) { + console.error("Error:", error); + }); +``` + +As a shortcut, the `resolve()` / `reject()` methods can optionally be passed directly into `$loaded()`: + +```js +var obj = $firebaseObject(ref); +obj.$loaded( + function(data) { + console.log(data === obj); // true + }, + function(error) { + console.error("Error:", error); + } +); +``` + +### $ref() + +Returns the `Firebase` reference used to create this object. + +```js +var obj = $firebaseObject(ref); +obj.$ref() === ref; // true +``` + +### $bindTo(scope, varName) + +Creates a three-way binding between a scope variable and the database data. When the `scope` data is +updated, changes are pushed to the database, and when changes occur in the database, they are pushed +instantly into `scope`. This method returns a promise that resolves after the initial value is +pulled from the database and set in the `scope` variable. + +```js +var ref = firebase.database().ref(); // assume value here is { foo: "bar" } +var obj = $firebaseObject(ref); + +obj.$bindTo($scope, "data").then(function() { + console.log($scope.data); // { foo: "bar" } + $scope.data.foo = "baz"; // will be saved to the database + ref.set({ foo: "baz" }); // this would update the database and $scope.data +}); +``` + +We can now bind to any property on our object directly in the HTML, and have it saved +instantly to the database. Security and Firebase Rules can be used for validation to ensure +data is formatted correctly at the server. + +```html + + +``` + +Only one scope variable can be bound at a time. If a second attempts to bind to the same +`$firebaseObject` instance, the promise will be rejected and the bind will fail. + +**IMPORTANT NOTE**: Angular does not report variables prefixed with `$` to any `$watch()` listeners. +a simple workaround here is to use a variable prefixed with `_`, which will not be saved to the +server, but will trigger `$watch()`. + +```js +var obj = $firebaseObject(ref); +obj.$bindTo($scope, "widget").then(function() { + $scope.widget.$priority = 99; + $scope.widget._updated = true; +}) +``` + +If `$destroy()` is emitted by `scope` (this happens when a controller is destroyed), then this +object is automatically unbound from `scope`. It can also be manually unbound using the +`unbind()` method, which is passed into the promise callback. + +```js +var obj = $firebaseObject(ref); +obj.$bindTo($scope, "data").then(function(unbind) { + // unbind this later + //unbind(); +}); +``` + +### $watch(callback, context) + +Registers an event listener which will be notified any time there is a change to the data. Returns +an unregister function that, when invoked, will stop notifying the callback of changes. + +```js +var obj = $firebaseObject(ref); +var unwatch = obj.$watch(function() { + console.log("data changed!"); +}); + +// at some time in the future, we can unregister using +unwatch(); +``` + +### $destroy() + +Calling this method cancels event listeners and frees memory used by this object (deletes the +local data). Changes are no longer synchronized to or from the database. + +### $resolved + +Attribute which represents the loaded state for this object. Its value will be `true` if the initial +object data has been downloaded from the database; otherwise, its value will be `false`. This +attribute is complementary to the `$loaded()` method. If the `$loaded()` promise is completed +(either with success or rejection), then `$resolved` will be `true`. `$resolved` will be +`false` before that. + +Knowing if the object has been resolved is useful to conditionally show certain parts of your view: + +```js +$scope.obj = $firebaseObject(ref); +``` + +```html + +
+ ... +
+ + +
+ ... +
+``` + + +## $firebaseArray + +The `$firebaseArray` service takes an optional [`firebase.database.Reference`](https://firebase.google.com/docs/reference/js/#firebase.database.Reference) or +[`firebase.database.Query`](https://firebase.google.com/docs/reference/js/#firebase.database.Query) and returns a JavaScript array which contains the data at the +provided location in Firebase and some extra AngularFire-specific fields. If no `Reference` or `Query` is provided, then the root of the Firebase Database will be used. Note that the data will not be available immediately since retrieving +it is an asynchronous operation. You can use the `$loaded()` promise to get notified when the data +has loaded. + +This service automatically keeps this local array in sync with any changes made to the remote +database. This is a **PSEUDO READ-ONLY ARRAY** suitable for use in directives like `ng-repeat` +and with Angular filters (which expect an array). + +While using read attributes and methods like `length` and `toString()` will work great on this array, +you should avoid directly manipulating the array. Methods like `splice()`, `push()`, `pop()`, +`shift()`, `unshift()`, and `reverse()` will cause the local data to become out of sync with the +server. Instead, utilize the `$add()`, `$remove()`, and `$save()` methods provided by the service to +change the structure of the array. To get the id of an item in a $firebaseArray within `ng-repeat`, call `$id` on that item. + +``` js +// JavaScript +app.controller("MyCtrl", ["$scope", "$firebaseArray", + function($scope, $firebaseArray) { + var ref = firebase.database().ref(); + var list = $firebaseArray(ref); + + // add an item + list.$add({ foo: "bar" }).then(...); + + // remove an item + list.$remove(2).then(...); + + // make the list available in the DOM + $scope.list = list; + } +]); +``` + +``` html + +
  • {{ item | json }}
  • +``` + +The `$firebaseArray` service can also take a +[query](https://firebase.google.com/docs/database/web/retrieve-data) to only sync +a subset of data. + +``` js +app.controller("MyCtrl", ["$scope", "$firebaseArray", + function($scope, $firebaseArray) { + var ref = firebase.database().ref(); + var messagesRef = ref.child("messages"); + var query = messagesRef.orderByChild("timestamp").limitToLast(10); + + var list = $firebaseArray(query); + } +]); +``` + +Note that, while the array itself should not be modified, it is practical to change specific +elements of the array and save them back to the remote database: + +```js +// JavaScript +var list = $firebaseArray(ref); +list[2].foo = "bar"; +list.$save(2); +``` + +```html + +
  • + +
  • +``` + +### $add(newData) + +Creates a new record in the database and adds the record to our local synchronized array. + +This method returns a promise which is resolved after data has been saved to the server. +The promise resolves to the `Firebase` reference for the newly added record, providing +easy access to its key. + +```js +var list = $firebaseArray(ref); +list.$add({ foo: "bar" }).then(function(ref) { + var id = ref.key; + console.log("added record with id " + id); + list.$indexFor(id); // returns location in the array +}); +``` + +### $remove(recordOrIndex) + +Remove a record from the database and from the local array. This method returns a promise that +resolves after the record is deleted at the server. It will contain a `Firebase` reference to +the deleted record. It accepts either an array index or a reference to an item that +exists in the array. + +```js +var list = $firebaseArray(ref); +var item = list[2]; +list.$remove(item).then(function(ref) { + ref.key === item.$id; // true +}); +``` + +### $save(recordOrIndex) + +The array itself cannot be modified, but records in the array can be updated and saved back +to the database individually. This method saves an existing, modified local record back to the database. +It accepts either an array index or a reference to an item that exists in the array. + +```js +$scope.list = $firebaseArray(ref); +``` + +```html +
  • + +
  • +``` + +This method returns a promise which is resolved after data has been saved to the server. +The promise resolves to the `Firebase` reference for the saved record, providing easy +access to its key. + +```js +var list = $firebaseArray(ref); +list[2].foo = "bar"; +list.$save(2).then(function(ref) { + ref.key === list[2].$id; // true +}); +``` + +### $getRecord(key) + +Returns the record from the array for the given key. If the key is not found, returns `null`. +This method utilizes `$indexFor(key)` to find the appropriate record. + +```js +var list = $firebaseArray(ref); +var rec = list.$getRecord("foo"); // record with $id === "foo" or null +``` + +### $keyAt(recordOrIndex) + +Returns the key for a record in the array. It accepts either an array index or +a reference to an item that exists in the array. + +```js +// assume records "alpha", "bravo", and "charlie" +var list = $firebaseArray(ref); +list.$keyAt(1); // bravo +list.$keyAt( list[1] ); // bravo +``` + +### $indexFor(key) + +The inverse of `$keyAt()`, this method takes a key and finds the associated record in the array. +If the record does not exist, -1 is returned. + +```js +// assume records "alpha", "bravo", and "charlie" +var list = $firebaseArray(ref); +list.$indexFor("alpha"); // 0 +list.$indexFor("bravo"); // 1 +list.$indexFor("zulu"); // -1 +``` + +### $loaded() + +Returns a promise which is resolved asynchronously when the initial array data has been downloaded +from the database. The promise resolves to the `$firebaseArray`. + +```js +var list = $firebaseArray(ref); +list.$loaded() + .then(function(x) { + x === list; // true + }) + .catch(function(error) { + console.log("Error:", error); + }); +``` + +The resolve/reject methods may also be passed directly into $loaded: + +```js +var list = $firebaseArray(ref); +list.$loaded( + function(x) { + x === list; // true + }, function(error) { + console.error("Error:", error); + }); +``` + +### $ref() + +Returns the `Firebase` reference used to create this array. + +```js +var list = $firebaseArray(ref); +sync === list.$ref(); // true +``` + +### $watch(cb[, context]) + +Any callback passed here will be invoked each time data in the array is updated from the server. +The callback receives an object with the following keys: + + * `event`: The database event type which fired (`child_added`, `child_moved`, `child_removed`, or `child_changed`). + * `key`: The ID of the record that triggered the event. + * `prevChild`: If event is `child_added` or `child_moved`, this contains the previous record's key + or `null` if `key` belongs to the first record in the collection. + +```js +var list = $firebaseArray(ref); + +list.$watch(function(event) { + console.log(event); +}); + +// logs { event: "child_removed", key: "foo" } +list.$remove("foo"); + +// logs { event: "child_added", key: "", prevId: "" } +list.$add({ hello: "world" }); +``` + +A common use case for this would be to customize the sorting for a synchronized array. Since +each time an add or update arrives from the server, the data could become out of order, we +can re-sort on each event. We don't have to worry about excessive re-sorts slowing down Angular's +compile process, or creating excessive DOM updates, because the events are already batched +nicely into a single `$apply` event (we gather them up and trigger the events in batches before +telling `$digest` to dirty check). + +```js +var list = $firebaseArray(ref); + +// sort our list +list.sort(compare); + +// each time the server sends records, re-sort +list.$watch(function() { list.sort(compare); }); + +// custom sorting routine (sort by last name) +function compare(a, b) { + return a.lastName.localeCompare(b.lastName); +} +``` + +### $destroy() + +Stop listening for events and free memory used by this array (empties the local copy). +Changes are no longer synchronized to or from the database. + +### $resolved + +Attribute which represents the loaded state for this array. Its value will be `true` if the initial +array data has been downloaded from the database; otherwise, its value will be `false`. This +attribute is complementary to the `$loaded()` method. If the `$loaded()` promise is completed +(either with success or rejection), then `$resolved` will be `true`. `$resolved` will be +`false` before that. + +Knowing if the array has been resolved is useful to conditionally show certain parts of your view: + +```js +$scope.list = $firebaseArray(ref); +``` + +```html + +
    + ... +
    + + +
    + ... +
    +``` + + +## $firebaseAuth + +AngularFire includes support for [user authentication and management](/docs/guide/user-auth.md) +with the `$firebaseAuth` service. + +The `$firebaseAuth` factory takes an optional Firebase auth instance (`firebase.auth()`) as its only +argument. Note that the authentication state is global to your application, even if multiple +`$firebaseAuth` objects are created unless you use multiple Firebase apps. + +```js +app.controller("MyAuthCtrl", ["$scope", "$firebaseAuth", + function($scope, $firebaseAuth) { + $scope.authObj = $firebaseAuth(); + } +]); +``` + +The authentication object returned by `$firebaseAuth` contains several methods for authenticating +users, responding to changes in authentication state, and managing user accounts for email / +password users. + +### $signInWithCustomToken(authToken) + +Authenticates the client using a [custom authentication token](https://firebase.google.com/docs/auth/web/custom-auth). +This function takes two arguments: an authentication token or a Firebase Secret and an object containing optional +client arguments, such as configuring session persistence. + +```js +$scope.authObj.$signInWithCustomToken("").then(function(firebaseUser) { + console.log("Signed in as:", firebaseUser.uid); +}).catch(function(error) { + console.error("Authentication failed:", error); +}); +``` + +This method returns a promise which is resolved or rejected when the authentication attempt is +completed. If successful, the promise will be fulfilled with an object containing the payload of +the authentication token. If unsuccessful, the promise will be rejected with an `Error` object. + +Read our [Custom Authentication guide](https://firebase.google.com/docs/auth/web/custom-auth) +for more details about generating your own custom authentication tokens. + +### $signInAnonymously() + +Authenticates the client using a new, temporary guest account. + +```js +$scope.authObj.$signInAnonymously().then(function(firebaseUser) { + console.log("Signed in as:", firebaseUser.uid); +}).catch(function(error) { + console.error("Authentication failed:", error); +}); +``` + +This method returns a promise which is resolved or rejected when the authentication attempt is +completed. If successful, the promise will be fulfilled with an object containing authentication +data about the signed-in user. If unsuccessful, the promise will be rejected with an `Error` object. + +Read [our documentation on anonymous authentication](https://firebase.google.com/docs/auth/web/anonymous-auth) +for more details about anonymous authentication. + +### $signInWithEmailAndPassword(email, password) + +Authenticates the client using an email / password combination. This function takes two +arguments: an object containing `email` and `password` attributes corresponding to the user account +and an object containing optional client arguments, such as configuring session persistence. + +```js +$scope.authObj.$signInWithEmailAndPassword("my@email.com", "password").then(function(firebaseUser) { + console.log("Signed in as:", firebaseUser.uid); +}).catch(function(error) { + console.error("Authentication failed:", error); +}); +``` + +This method returns a promise which is resolved or rejected when the authentication attempt is +completed. If successful, the promise will be fulfilled with an object containing authentication +data about the signed-in user. If unsuccessful, the promise will be rejected with an `Error` object. + +Read [our documentation on email / password authentication](https://firebase.google.com/docs/auth/web/password-auth) +for more details about email / password authentication. + +### $signInWithPopup(provider) + +Authenticates the client using a popup-based OAuth flow. This function takes a single argument: a +a string or provider object representing the OAuth provider to authenticate with. It returns a +promise which is resolved or rejected when the authentication attempt is completed. If successful, +the promise will be fulfilled with an object containing authentication data about the signed-in +user. If unsuccessful, the promise will be rejected with an `Error` object. + +Valid values for the string version of the argument are `"facebook"`, `"github"`, `"google"`, and +`"twitter"`: + +```js +$scope.authObj.$signInWithPopup("google").then(function(result) { + console.log("Signed in as:", result.user.uid); +}).catch(function(error) { + console.error("Authentication failed:", error); +}); +``` + +Alternatively, you can request certain scopes or custom parameters from the OAuth provider by +passing a provider object (such as `new firebase.auth.GoogleAuthProvider()`) configured with +additional options: + +```js +var provider = new firebase.auth.GoogleAuthProvider(); +provider.addScope("https://www.googleapis.com/auth/plus.login"); +provider.setCustomParameters({ + login_hint: "user@example.com" +}); + +$scope.authObj.$signInWithPopup(provider).then(function(result) { + console.log("Signed in as:", result.user.uid); +}).catch(function(error) { + console.error("Authentication failed:", error); +}); +``` + +Firebase currently supports [Facebook](https://firebase.google.com/docs/auth/web/facebook-login), +[GitHub](https://firebase.google.com/docs/auth/web/github-auth), +[Google](https://firebase.google.com/docs/auth/web/google-signin), +and [Twitter](https://firebase.google.com/docs/auth/web/twitter-login) authentication. Refer to the +linked documentation in the previous sentence for information about configuring each provider. + +### $signInWithRedirect(provider[, options]) + +Authenticates the client using a redirect-based OAuth flow. This function takes a single argument: a +string or provider object representing the OAuth provider to authenticate with. It returns a +rejected promise with an `Error` object if the authentication attempt fails. Upon successful +authentication, the browser will be redirected as part of the OAuth authentication flow. As such, +the returned promise will never be fulfilled. Instead, you should use the `$onAuthStateChanged()` +method to detect when the authentication has been successfully completed. + +Valid values for the string version of the argument are `"facebook"`, `"github"`, `"google"`, and +`"twitter"`: + +```js +$scope.authObj.$signInWithRedirect("google").then(function() { + // Never called because of page redirect + // Instead, use $onAuthStateChanged() to detect successful authentication +}).catch(function(error) { + console.error("Authentication failed:", error); +}); +``` + +Alternatively, you can request certain scopes or custom parameters from the OAuth provider by +passing a provider object (such as `new firebase.auth.GoogleAuthProvider()`) configured with +additional options: + +```js +var provider = new firebase.auth.GoogleAuthProvider(); +provider.addScope("https://www.googleapis.com/auth/plus.login"); +provider.setCustomParameters({ + login_hint: "user@example.com" +}); + +$scope.authObj.$signInWithRedirect(provider).then(function(result) { + // Never called because of page redirect + // Instead, use $onAuthStateChanged() to detect successful authentication +}).catch(function(error) { + console.error("Authentication failed:", error); +}); +``` + +Firebase currently supports [Facebook](https://firebase.google.com/docs/auth/web/facebook-login), +[GitHub](https://firebase.google.com/docs/auth/web/github-auth), +[Google](https://firebase.google.com/docs/auth/web/google-signin), +and [Twitter](https://firebase.google.com/docs/auth/web/twitter-login) authentication. Refer to the +linked documentation in the previous sentence for information about configuring each provider. + +### $signInWithCredential(credential) + +Authenticates the client using a credential. This function takes a single argument: the credential +object. Credential objects are created from a provider-specific set of user data, such as their +email / password combination or an OAuth access token. + +```js +// Email / password authentication with credential +var credential = firebase.auth.EmailAuthProvider.credential(email, password); + +$scope.authObj.$signInWithCredential(credential).then(function(firebaseUser) { + console.log("Signed in as:", firebaseUser.uid); +}).catch(function(error) { + console.error("Authentication failed:", error); +}); +``` + +```js +// Facebook authentication with credential +var credential = firebase.auth.FacebookAuthProvider.credential( + // `event` come from the Facebook SDK's auth.authResponseChange() callback + event.authResponse.accessToken +); + +$scope.authObj.$signInWithCredential(credential).then(function(firebaseUser) { + console.log("Signed in as:", firebaseUser.uid); +}).catch(function(error) { + console.error("Authentication failed:", error); +}); +``` + +This method returns a promise which is resolved or rejected when the authentication attempt is +completed. If successful, the promise will be fulfilled with an object containing authentication +data about the signed-in user. If unsuccessful, the promise will be rejected with an `Error` object. + +Firebase currently supports `$signInWithCredential()` for the +[email / password](https://firebase.google.com/docs/reference/node/firebase.auth.EmailAuthProvider#.credential), +[Facebook](https://firebase.google.com/docs/reference/node/firebase.auth.FacebookAuthProvider#.credential), +[GitHub](https://firebase.google.com/docs/reference/node/firebase.auth.GithubAuthProvider#.credential), +[Google](https://firebase.google.com/docs/reference/node/firebase.auth.GoogleAuthProvider#.credential), +and [Twitter](https://firebase.google.com/docs/reference/node/firebase.auth.TwitterAuthProvider#.credential) +authentication providers. Refer to the linked documentation in the previous sentence for information +about creating a credential for each provider. + +### $getAuth() + +Synchronously retrieves the current authentication state of the client. If the user is +authenticated, an object containing the fields `uid` (the unique user ID), `provider` (string +identifying the provider), `auth` (the authentication token payload), and `expires` (expiration +time in seconds since the Unix epoch) - and more, depending upon the provider used to authenticate - +will be returned. Otherwise, the return value will be `null`. + +```js +var firebaseUser = $scope.authObj.$getAuth(); + +if (firebaseUser) { + console.log("Signed in as:", firebaseUser.uid); +} else { + console.log("Signed out"); +} +``` + +### $onAuthStateChanged(callback[, context]) + +Listens for changes to the client's authentication state. The provided `callback` will fire when +the client's authenticate state changes. If authenticated, the callback will be passed an object +containing the fields `uid` (the unique user ID), `provider` (string identifying the provider), +`auth` (the authentication token payload), and `expires` (expiration time in seconds since the Unix +epoch) - and more, depending upon the provider used to authenticate. Otherwise, the callback will +be passed `null`. + +```js +$scope.authObj.$onAuthStateChanged(function(firebaseUser) { + if (firebaseUser) { + console.log("Signed in as:", firebaseUser.uid); + } else { + console.log("Signed out"); + } +}); +``` + +This method can also take an optional second argument which, if provided, will be used as `this` +when calling your callback. + +This method returns a function which can be used to unregister the provided `callback`. Once the +`callback` is unregistered, changes in authentication state will not cause the `callback` to fire. + +```js +var offAuth = $scope.authObj.$onAuthStateChanged(callback); + +// ... sometime later, unregister the callback +offAuth(); +``` + +### $signOut() + +Signs out a client. It takes no arguments and returns an empty `Promise` when the client has been +signed out. Upon fulfillment, the `$onAuthStateChanged()` callback(s) will be triggered. + +```html + + {{ firebaseUser.displayName }} | Sign out + +``` + +### $createUserWithEmailAndPassword(email, password) + +Creates a new user account using an email / password combination. This function returns a promise +that is resolved with an object containing user data about the created user. + +```js +$scope.authObj.$createUserWithEmailAndPassword("my@email.com", "mypassword") + .then(function(firebaseUser) { + console.log("User " + firebaseUser.uid + " created successfully!"); + }).catch(function(error) { + console.error("Error: ", error); + }); +``` + +Note that this function both creates the new user and authenticates as the new user. + +### $updatePassword(newPassword) + +Changes the password of the currently signed-in user. This function returns a promise that is +resolved when the password has been successfully changed on the Firebase Authentication servers. + +```js +$scope.authObj.$updatePassword("newPassword").then(function() { + console.log("Password changed successfully!"); +}).catch(function(error) { + console.error("Error: ", error); +}); +``` + +### $updateEmail(newEmail) + +Changes the email of the currently signed-in user. This function returns a promise that is resolved +when the email has been successfully changed on the Firebase Authentication servers. + +```js +$scope.authObj.$updateEmail("new@email.com").then(function() { + console.log("Email changed successfully!"); +}).catch(function(error) { + console.error("Error: ", error); +}); +``` + +### $deleteUser() + +Deletes the currently authenticated user. This function returns a promise that is resolved when the +user has been successfully removed on the Firebase Authentication servers. + +```js +$scope.authObj.$deleteUser().then(function() { + console.log("User removed successfully!"); +}).catch(function(error) { + console.error("Error: ", error); +}); +``` + +Note that removing a user also logs that user out and will therefore fire any `onAuthStateChanged()` +callbacks that you have created. + +### $sendPasswordResetEmail(email) + +Sends a password-reset email to the owner of the account, containing a token that may be used to +authenticate and change the user's password. This function returns a promise that is resolved when +the email notification has been sent successfully. + +```js +$scope.authObj.$sendPasswordResetEmail("my@email.com").then(function() { + console.log("Password reset email sent successfully!"); +}).catch(function(error) { + console.error("Error: ", error); +}); +``` + +### $waitForSignIn() + +Helper method which returns a promise fulfilled with the current authentication state. This is +intended to be used in the `resolve()` method of Angular routers. See the +["Using Authentication with Routers"](/docs/guide/user-auth.md#authenticating-with-routers) +section of our AngularFire guide for more information and a full example. + +### $requireSignIn(requireEmailVerification) + +Helper method which returns a promise fulfilled with the current authentication state if the user +is authenticated and, if specified, has a verified email address, but otherwise rejects the promise. +This is intended to be used in the `resolve()` method of Angular routers to prevent unauthenticated +users from seeing authenticated pages momentarily during page load. See the +["Using Authentication with Routers"](/docs/guide/user-auth.md#authenticating-with-routers) +section of our AngularFire guide for more information and a full example. + +## $firebaseStorage + +AngularFire includes support for [binary storage](/docs/guide/uploading-downloading-binary-content.md) +with the `$firebaseStorage` service. + +The `$firebaseStorage` service takes a [Storage](https://firebase.google.com/docs/storage/) reference. + +```js +app.controller("MyCtrl", ["$scope", "$firebaseStorage", + function($scope, $firebaseStorage) { + var storageRef = firebase.storage().ref("images/dog"); + $scope.storage = $firebaseStorage(storageRef); + } +]); +``` + +The storage object returned by `$firebaseStorage` contains several methods for uploading and +downloading binary content, as well as managing the content's metadata. + +### $put(file, metadata) + +[Uploads a `Blob` object](https://firebase.google.com/docs/storage/web/upload-files) to the specified storage reference's path with an optional metadata parameter. +Returns an [`UploadTask`](#upload-task) wrapped by AngularFire. + + +```js +var htmlFile = new Blob([""], { type : "text/html" }); +var uploadTask = $scope.storage.$put(htmlFile, { contentType: "text/html" }); +``` + +### $putString(string, format, metadata) + +[Uploads a raw, `base64` string, or `base64url` string](https://firebase.google.com/docs/storage/web/upload-files#upload_from_a_string) to the specified storage reference's path with an optional metadata parameter. +Returns an [`UploadTask`](#upload-task) wrapped by AngularFire. + +```js +var base64String = "5b6p5Y+344GX44G+44GX44Gf77yB44GK44KB44Gn44Go44GG77yB"; +// Note: valid values for format are "raw", "base64", "base64url", and "data_url". +var uploadTask = $scope.storage.$putString(base64String, "base64", { contentType: "image/gif" }); +``` + +### $getDownloadURL() + +Returns a promise fulfilled with [the download URL](https://firebase.google.com/docs/storage/web/download-files#download_data_via_url) for the file stored at the configured path. + +```js +$scope.storage.$getDownloadURL().then(function(url) { + $scope.url = url; +}); +``` + +### $getMetadata() + +Returns a promise fulfilled with [the metadata of the file](https://firebase.google.com/docs/storage/web/file-metadata#get_file_metadata) stored at the configured path. File +metadata contains common properties such as `name`, `size`, and `contentType` +(often referred to as MIME type) in addition to some less common ones like `contentDisposition` and `timeCreated`. + +```js +$scope.storage.$getMetadata().then(function(metadata) { + $scope.metadata = metadata; +}); +``` + +### $updateMetadata(metadata) + +[Updates the metadata of the file](https://firebase.google.com/docs/storage/web/file-metadata#update_file_metadata) stored at the configured path. +Returns a promise fulfilled with the updated metadata. + +```js +var updateData = { contenType: "text/plain" }; +$scope.storage.$updateMetadata(updateData).then(function(updatedMetadata) { + $scope.updatedMetadata = updatedMetadata; +}); +``` + +### $delete() + +Permanently [deletes the file stored](https://firebase.google.com/docs/storage/web/delete-files) at the configured path. Returns a promise that is resolved when the delete completes. + +```js +$scope.storage.$delete().then(function() { + console.log("successfully deleted!"); +}); +``` + +### $toString() + +Returns a [string version of the bucket path](https://firebase.google.com/docs/reference/js/firebase.storage.Reference#toString) stored as a `gs://` scheme. + +```js +// gs:///// +var asString = $scope.storage.$toString(); +``` + +### Upload Task + +The [`$firebaseStorage()`](#firebasestorage) service returns an AngularFire wrapped [`UploadTask`](https://firebase.google.com/docs/reference/js/firebase.storage#uploadtask) when uploading binary content +using the [`$put()`](#putfile-metadata) and [`$putString()`](#putstringstring-format-metadata) methods. This task is used for [monitoring](https://firebase.google.com/docs/storage/web/upload-files#monitor_upload_progress) +and [managing](https://firebase.google.com/docs/storage/web/upload-files#manage_uploads) uploads. + +```js +var htmlFile = new Blob([""], { type : "text/html" }); +var uploadTask = $scope.storage.$put(htmlFile, { contentType: "text/html" }); +``` + +#### $progress(callback) + +Calls the provided callback function whenever there is an update in the progress of the file uploading. The callback +passes back an [`UploadTaskSnapshot`](https://firebase.google.com/docs/reference/js/firebase.storage.UploadTaskSnapshot). + +```js +var uploadTask = $scope.storage.$put(file); +uploadTask.$progress(function(snapshot) { + var percentUploaded = (snapshot.bytesTransferred / snapshot.totalBytes) * 100; + console.log(percentUploaded); +}); +``` + +#### $complete(callback) + +Calls the provided callback function when the upload is complete. Passes back the completed [`UploadTaskSnapshot`](https://firebase.google.com/docs/reference/js/firebase.storage.UploadTaskSnapshot). + +```js +var uploadTask = $scope.storage.$put(file); +uploadTask.$complete(function(snapshot) { + console.log(snapshot.downloadURL); +}); +``` + +#### $error(callback) + +Calls the provided callback function when there is an error uploading the file. + +```js +var uploadTask = $scope.storage.$put(file); +uploadTask.$error(function(error) { + console.error(error); +}); +``` + +#### $cancel() + +[Cancels](https://firebase.google.com/docs/reference/js/firebase.storage.UploadTask#cancel) the current upload. +Has no effect on a completed upload. Returns `true` if cancel had effect. + +```js +var uploadTask = $scope.storage.$put(file); +var hadEffect = uploadTask.$cancel(); +``` + +#### $pause() + +[Pauses](https://firebase.google.com/docs/reference/js/firebase.storage.UploadTask#pause) the current upload. +Has no effect on a completed upload. Returns `true` if pause had effect. + +```js +var uploadTask = $scope.storage.$put(file); +var hadEffect = uploadTask.$pause(); +``` + +#### $snapshot() + +Returns the [current immutable view of the task](https://firebase.google.com/docs/reference/js/firebase.storage.UploadTaskSnapshot) at the time the event occurred. + +```js +var uploadTask = $scope.storage.$put(file); +$scope.bytesTransferred = uploadTask.$snapshot.bytesTransferred; +``` + +#### then() +An `UploadTask` implements a promise like interface. The callback is called when the upload is complete. The callback +passes back an [UploadTaskSnapshot](https://firebase.google.com/docs/reference/js/firebase.storage.UploadTaskSnapshot). + +```js +var uploadTask = $scope.storage.$put(file); +uploadTask.then(function(snapshot) { + console.log(snapshot.downloadURL); +}); +``` + +#### catch() +An `UploadTask` implements a promise like interface. The callback is called when an error occurs. + +```js +var uploadTask = $scope.storage.$put(file); +uploadTask.catch(function(error) { + console.error(error); +}); +``` + +## Extending the Services + +There are several powerful techniques for transforming the data downloaded and saved +by `$firebaseArray` and `$firebaseObject`. **These techniques should only be attempted +by advanced Angular users who know their way around the code.** + +### Extending $firebaseObject + +You can create a new factory from a `$firebaseObject`. It can add additional methods or override any existing method. + +```js +var ColorFactory = $firebaseObject.$extend({ + getMyFavoriteColor: function() { + return this.favoriteColor + ", no green!"; // obscure Monty Python reference + } +}); + +var factory = new ColorFactory(ref); +var favColor = factory.getMyFavoriteColor(); +``` + +This technique can also be used to transform how data is stored and saved by overriding the +following private methods: + + - **$$updated**: Called with a snapshot any time a `value` event is received from the database, must apply the updates and return true if any changes occurred. + - **$$error**: Passed an `Error` any time a security error occurs. These are generally not recoverable. + - **$$notify**: Sends notifications to any listeners registered with `$watch()`. + - **toJSON**: As with any object, if a `toJSON()` method is provided, it will be used by `JSON.stringify()` to parse the JSON content before it is saved to the database. + - **$$defaults**: A key / value pair that can be used to create default values for any fields which are not found in the server data (i.e. undefined fields). By default, they are applied each time `$$updated` is invoked. If that method is overridden, it would need to implement this behavior. + +```js +// Add a counter to our object... +var FactoryWithCounter = $firebaseObject.$extend({ + // add a method to the prototype that returns our counter + getUpdateCount: function() { return this._counter; }, + + // each time an update arrives from the server, apply the change locally + $$updated: function(snap) { + // apply the changes using the super method + var changed = $firebaseObject.prototype.$$updated.apply(this, arguments); + + // add / increment a counter each time there is an update + if( !this._counter ) { this._counter = 0; } + this._counter++; + + // return whether or not changes occurred + return changed; + } +}); +``` + +### Extending $firebaseArray + +You can create a new factory from a `$firebaseArray`. It can add additional methods or override any existing method. + +```js +app.factory("ArrayWithSum", function($firebaseArray) { + return $firebaseArray.$extend({ + sum: function() { + var total = 0; + angular.forEach(this.$list, function(rec) { + total += rec.x; + }); + return total; + } + }); +}) +``` + +We can then use this factory with by instantiating it: + +```js +var list = new ArrayWithSum(ref); +list.$loaded().then(function() { + console.log("List has " + list.sum() + " items"); +}); +``` + +This technique can be used to transform how data is stored by overriding the +following private methods: + + - **$$added**: Called with a snapshot and prevChild any time a `child_added` event occurs. + - **$$updated**: Called with a snapshot any time a `child_changed` event occurs. + - **$$moved**: Called with a snapshot and prevChild any time `child_moved` event occurs. + - **$$removed**: Called with a snapshot any time a `child_removed` event occurs. + - **$$error**: Passed an `Error` any time a security error occurs. These are generally not recoverable. + - **$$getKey**: Tells AngularFire what the unique ID is for each record (the default just returns `this.$id`). + - **$$notify**: Notifies any listeners registered with $watch; normally this method would not be modified. + - **$$process**: Handles the actual splicing of data into and out of the array. Normally this method would not be modified. + - **$$defaults**: A key / value pair that can be used to create default values for any fields which are not found in the server data (i.e. undefined fields). By default, they are applied each time `$$added` or `$$updated` are invoked. If those methods are overridden, they would need to implement this behavior. + +To illustrate, let's create a factory that creates `Widget` instances, and transforms dates: + +```js +// an object to return in our WidgetFactory +app.factory("Widget", function($firebaseUtils) { + function Widget(snapshot) { + // store the record id so AngularFire can identify it + this.$id = snapshot.key; + + // apply the data + this.update(snapshot); + } + + Widget.prototype = { + update: function(snapshot) { + var oldData = angular.extend({}, this.data); + + // apply changes to this.data instead of directly on `this` + this.data = snapshot.val(); + + // add a parsed date to our widget + this._date = new Date(this.data.date); + + // determine if anything changed, note that angular.equals will not check + // $value or $priority (since it excludes anything that starts with $) + // so be careful when using angular.equals() + return !angular.equals(this.data, oldData); + }, + + getDate: function() { + return this._date; + }, + + toJSON: function() { + // since we changed where our data is stored, we need to tell AngularFire how + // to get the JSON version of it. We can use $firebaseUtils.toJSON() to remove + // private variables, copy the data into a shippable format, and do validation + return $firebaseUtils.toJSON(this.data); + } + }; + + return Widget; +}); + +// now let's create a synchronized array factory that uses our Widget +app.factory("WidgetFactory", function($firebaseArray, Widget) { + return $firebaseArray.$extend({ + // change the added behavior to return Widget objects + $$added: function(snap) { + // instead of creating the default POJO (plain old JavaScript object) + // we will return an instance of the Widget class each time a child_added + // event is received from the server + return new Widget(snap); + }, + + // override the update behavior to call Widget.update() + $$updated: function(snap) { + // we need to return true/false here or $watch listeners will not get triggered + // luckily, our Widget.prototype.update() method already returns a boolean if + // anything has changed + return this.$getRecord(snap.key.update(snap); + } + }); +}); +``` + +### Passing a Class into $extend + +Instead of just a list of functions, we can also pass in a class constructor to inherit methods from +`$firebaseArray`. The prototype for this class will be preserved, and it will inherit +from `$firebaseArray`. + +**This is an extremely advanced feature. Do not use this unless you know that you need it** + +This class constructor is expected to call `$firebaseArray`'s constructor (i.e. the super constructor). + +The following factory adds an update counter which is incremented each time `$$added()` +or `$$updated()` is called: + +```js +app.factory("ArrayWithCounter", function($firebaseArray, Widget) { + // $firebaseArray and $firebaseObject constructors both accept a single argument, a `Firebase` ref + function ArrayWithCounter(ref) { + // initialize a counter + this.counter = 0; + + // call the super constructor + return $firebaseArray.call(this, ref); + } + + // override the add behavior to return a Widget + ArrayWithCounter.prototype.$$added = function(snap) { + return new Widget(snap); + }; + + // override the update behavior to call Widget.update() + ArrayWithCounter.prototype.$$updated = function(snap) { + var widget = this.$getRecord(snap.key; + return widget.update(); + }; + + // pass our constructor to $extend, which will automatically extract the + // prototype methods and call the constructor appropriately + return $firebaseArray.$extend(ArrayWithCounter); +}); +``` + +### Decorating the Services + +In general, it will be more useful to extend the services to create new services than +to use this technique. However, it is also possible to modify `$firebaseArray` or +`$firebaseObject` globally by using Angular's `$decorate()` method. + +```js +app.config(function($provide) { + $provide.decorator("$firebaseObject", function($delegate, $firebaseUtils) { + var _super = $delegate.prototype.$$updated; + + // override any instance of $firebaseObject to look for a date field + // and transforms it to a Date object. + $delegate.prototype.$$updated = function(snap) { + var changed = _super.call(this, snap); + if( this.hasOwnProperty("date") ) { + this._dateObj = new Date(this.date); + } + return changed; + }; + + // add a method that fetches the date object we just created + $delegate.prototype.getDate = function() { + return this._dateObj; + }; + + // variables starting with _ are ignored by AngularFire so we don't need + // to worry about the toJSON method here + + return $delegate; + }); +}); +``` + +### Creating AngularFire Services + +With the ability to extend the AngularFire services, services can be built to represent +our synchronized collections with a minimal amount of code. For example, we can create +a `User` factory: + +```js +// create a User factory with a getFullName() method +app.factory("UserFactory", function($firebaseObject) { + return $firebaseObject.$extend({ + getFullName: function() { + // concatenate first and last name + return this.first_name + " " + this.last_name; + } + }); +}); +``` + +And create a new instance: + +```js +// create a User object from our Factory +app.factory("User", function(UserFactory) { + var ref = firebase.database().ref(); + var usersRef = ref.child("users"); + return function(userid) { + return new UserFactory(usersRef.child(userid)); + } +}); +``` + +Similarly, we can extend `$firebaseArray` by creating a `Message` object: + +```js +app.factory("Message", function($firebaseArray) { + function Message(snap) { + // store the user ID so AngularFire can identify the records + // in this case, we store it in a custom location, so we'll need + // to override $$getKey + this.message_id = snap.key; + this.message = snap.val(); + } + Message.prototype = { + update: function(snap) { + // store a string into this.message (instead of the default $value) + if( snap.val() !== this.message ) { + this.message = snap.val(); + return true; + } + return false; + }, + toJSON: function() { + // tell AngularFire what data to save, in this case a string + return this.message; + } + }; + + return Message; +}); +``` + +We can then use that to extend the `$firebaseArray` factory: + +```js +app.factory("MessageFactory", function($firebaseArray, Message) { + return $firebaseArray.$extend({ + // override the $createObject behavior to return a Message object + $$added: function(snap) { + return new Message(snap); + }, + + // override the $$updated behavior to call a method on the Message + $$updated: function(snap) { + var msg = this.$getRecord(snap.key); + return msg.update(snap); + }, + + // our messages store the unique id in a special location, so tell $firebaseArray + // how to find the id for each record + $$getKey: function(rec) { + return rec.message_id; + } + }); +}); +``` + +And finally, we can put it all together into a synchronized list of messages: + +```js +app.factory("MessageList", function(MessageFactory) { + return function(ref) { + return new MessageFactory(ref); + } +}); +``` + + +## SDK Compatibility + +This documentation is for AngularFire 2.x.x with Firebase SDK 3.x.x. + +| SDK Version | AngularFire Version Supported | +|-------------|-------------------------------| +| 3.x.x | 2.x.x | +| 2.x.x | 1.x.x | + + +## Browser Compatibility + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    BrowserVersion SupportedWith Polyfill
    Internet Explorer9+9+ (Angular 1.3 only supports 9+)
    Firefox4.03.0?
    Chrome75?
    Safari5.1.4?
    Opera11.6?
    + +Polyfills are automatically included to support older browsers. See `src/polyfills.js` for links +and details. diff --git a/index.js b/index.js new file mode 100644 index 00000000..4b40cfaa --- /dev/null +++ b/index.js @@ -0,0 +1,9 @@ +// Make sure dependencies are loaded on the window +require('angular'); +require('firebase'); + +// Load the Angular module which uses window.angular and window.Firebase +require('./dist/angularfire'); + +// Export the module name from the Angular module +module.exports = 'firebase'; diff --git a/lib/omnibinder-protocol.js b/lib/omnibinder-protocol.js deleted file mode 100644 index 45169bca..00000000 --- a/lib/omnibinder-protocol.js +++ /dev/null @@ -1,66 +0,0 @@ -angular.module('omniFire', []). - factory('objectChange', function () { - return function (name, type, value, oldValue) { - return { - name: name, - type: type, - value: value, - oldValue: oldValue - }; - }; - }). - factory('arrayChange', function () { - return function (index, removed, addedCount, added) { - return { - index: index, - removed: removed, - addedCount: addedCount, - added: added - }; - }; - }). - service('firebinder', ['arrayChange', 'objectChange', - function (arrayChange, objectChange) { - var self = this; - - this.subscribe = function (binder) { - binder.fbRef = new Firebase(binder.query.url); - binder.index = []; - binder.isLocal = false; - - typeof binder.query.limit === 'number' && - binder.fbRef.limit(binder.query.limit); - - typeof binder.query.startAt === 'number' && - binder.fbRef.startAt(binder.query.startAt); - - binder.fbRef.on('child_added', function (snapshot, prev) { - self.onChildAdded(binder, snapshot, prev); - }); - }; - - this.onChildAdded = function onChildAdded (binder, snapshot, prev) { - if (binder.isLocal) return binder.isLocal = !binder.isLocal; - - var key = snapshot.name(), - currIndex = binder.index.indexOf(key), - changeObject = arrayChange(null, [], 1, [snapshot.val()]), - prevIndex; - - if (currIndex !== -1) { - binder.index.splice(currIndex, 1); - } - - if (prev) { - prevIndex = binder.index.indexOf(prev); - changeObject.index = prevIndex + 1; - binder.index.splice(changeObject.index, 0, key); - } - else { - changeObject.index = 0; - binder.index.unshift(key); - } - - binder.onProtocolChange.call(binder, [changeObject]); - } - }]); diff --git a/lib/omnibinder.js b/lib/omnibinder.js deleted file mode 100644 index ce1bb566..00000000 --- a/lib/omnibinder.js +++ /dev/null @@ -1,161 +0,0 @@ -angular.module("OmniBinder", []).factory("obBinder", [ "$timeout", "$q", "$parse", "$window", "obSyncEvents", "obBinderTypes", "obModelWriter", "obObserver", function($timeout, $q, $parse, $window, obSyncEvents, obBinderTypes, obModelWriter, obObserver) { - function Binder(scope, model, protocol, options) { - if (options = options || {}, !protocol) throw new Error("protocol is required"); - if (!scope) throw new Error("scope is required"); - if (!model) throw new Error("model is required"); - if (options.key && "string" != typeof options.key) throw new Error("key must be a string"); - this.protocol = protocol, this.scope = scope, this.model = model, this.query = options.query, - this.type = options.type, this.key = options.key, this.bindModel(this.type, scope, model), - this.protocol.subscribe(this), this.ignoreNModelChanges = 0, this.ignoreNProtocolChanges = 0; - } - return Binder.prototype.bindModel = function(type, scope, model) { - switch (type) { - case obBinderTypes.COLLECTION: - this.observer = obObserver.observeCollection(this, scope[model], this.onModelChange); - } - }, Binder.prototype.onModelChange = function(changes) { - for (var numAffectedItems = 0, delta = { - changes: changes - }, i = 0; i < changes.length; i++) numAffectedItems += changes.name && 1 || changes[i].addedCount + (changes[i].removed && changes[i].removed.length) || 0; - return delta.changes.length ? this.ignoreNModelChanges ? this.ignoreNModelChanges -= numAffectedItems : (this.protocol.processChanges(this, delta), - void 0) : void 0; - }, Binder.prototype.onProtocolChange = function(changes) { - if (delta = { - changes: changes - }, changes.length) if (this.ignoreNProtocolChanges) { - newChanges = []; - for (var i = 0; i < changes.length; i++) changes[i].force && newChanges.push(changes[i]), - this.ignoreNProtocolChanges--; - if (!newChanges.length) return; - delta.changes = newChanges, obModelWriter.processChanges(this, delta); - } else obModelWriter.processChanges(this, delta); - }, Binder.prototype.val = function() { - var getter = $parse(this.model); - return getter(this.scope); - }, function() { - var binder = Object.create(Binder.prototype); - return Binder.apply(binder, arguments), binder; - }; -} ]), angular.module("OmniBinder").factory("obBinderTypes", [ function() { - return { - COLLECTION: "collection", - OBJECT: "object", - BOOLEAN: "boolean", - STRING: "string", - NUMBER: "number", - BINARY: "binary", - BINARY_STREAM: "binaryStream" - }; -} ]), function() { - var DeltaFactory = function() {}; - DeltaFactory.prototype.addChange = function(change) { - if (!change.type) throw new Error("Change must contain a type"); - this.changes.push(change); - }, DeltaFactory.prototype.updateObject = function(object) { - this.object = object, angular.forEach(this.changes, function(change, i, list) { - list[i].object = object; - }); - }, angular.module("OmniBinder").factory("obDelta", function() { - return function(change) { - var delta = Object.create(DeltaFactory.prototype); - return DeltaFactory.call(delta), delta.changes = [], change && delta.addChange(change), - delta; - }; - }); -}(), angular.module("OmniBinder").service("obModelWriter", [ "$parse", "obBinderTypes", "obSyncEvents", function($parse, obBinderTypes) { - this.applyArrayChange = function(binder, change) { - var model = $parse(binder.model)(binder.scope); - if (change.added) { - var firstChange = change.added.shift(); - for (model.splice(change.index, change.removed ? change.removed.length : 0, firstChange); next = change.added.shift(); ) change.index++, - model.splice(change.index, 0, next); - } else model.splice(change.index, change.removed ? change.removed.length : 0); - binder.ignoreNModelChanges += (change.removed && change.removed.length || 0) + change.addedCount, - $parse(binder.model).assign(binder.scope, model), binder.scope.$$phase || binder.scope.$apply(); - }, this.applyObjectChange = function(binder, change) { - function findObject(keyName, key) { - var obj, collection = binder.scope[binder.model]; - return angular.forEach(collection, function(item) { - obj || (item[keyName] === key ? obj = item : "undefined" == typeof item[keyName] && (obj = item)); - }), obj; - } - if (binder.key) { - var obj = findObject(binder.key, change.object[binder.key]); - if (!obj) throw new Error("Could not find object with key" + change.object[binder.key]); - switch (change.type) { - case "update": - obj[change.name] !== change.object[change.name] && binder.ignoreNModelChanges++, - obj[change.name] = change.object[change.name]; - break; - - case "delete": - binder.ignoreNModelChanges++, delete obj[change.name]; - break; - - case "new": - obj[change.name] !== change.object[change.name] && binder.ignoreNModelChanges++, - obj[change.name] = change.object[change.name]; - } - binder.scope.$$phase || binder.scope.$apply(); - } - }, this.processChanges = function(binder, delta) { - angular.forEach(delta.changes, function(change) { - switch (binder.type) { - case obBinderTypes.COLLECTION: - "number" == typeof change.index ? this.applyArrayChange(binder, change) : "string" == typeof change.name && this.applyObjectChange(binder, change); - } - }, this); - }; -} ]), angular.module("OmniBinder").factory("obArrayChange", function() { - return function(addedCount, removed, index) { - return { - addedCount: addedCount, - removed: removed, - index: index - }; - }; -}).factory("obOldObject", function() { - return function(change) { - var oldObject = angular.copy(change.object); - return oldObject[change.name] = change.oldValue, oldObject; - }; -}).service("obObserver", [ "obArrayChange", "obOldObject", function(obArrayChange, obOldObject) { - this.observeObjectInCollection = function(context, collection, object, callback) { - function onObjectObserved(changes) { - function pushSplice(change) { - var oldObject = obOldObject(change), index = collection.indexOf(change.object), change = obArrayChange(1, [ oldObject ], index); - splices.push(change); - } - var splices = []; - context.key ? callback.call(context, changes) : (angular.forEach(changes, pushSplice), - callback.call(context, splices)); - } - this.observers[object] = onObjectObserved, Object.observe(object, onObjectObserved); - }, this.observers = {}, this.observeCollection = function(context, collection, callback) { - function observeOne(obj) { - self.observeObjectInCollection(context, collection, obj, callback); - } - function onArrayChange(changes) { - angular.forEach(changes, watchNewObjects), callback.call(context, changes); - } - function watchNewObjects(change) { - for (var i = change.index, lastIndex = change.addedCount + change.index; lastIndex > i; ) observeOne(collection[i]), - i++; - change.removed.length && angular.forEach(change.removed, function(obj) { - Object.unobserve(obj, self.observers[obj]); - }); - } - var observer, self = this; - return angular.forEach(collection, observeOne), observer = new ArrayObserver(collection, onArrayChange); - }; -} ]), angular.module("OmniBinder").value("obSyncEvents", { - NEW: "new", - UPDATED: "update", - DELETED: "deleted", - RECONFIGURED: "reconfigured", - READ: "read", - MOVE: "move", - NONE: "none", - INIT: "init", - UNKNOWN: "unknown" -}); \ No newline at end of file diff --git a/lib/omnibinder.min.js b/lib/omnibinder.min.js deleted file mode 100644 index 84380184..00000000 --- a/lib/omnibinder.min.js +++ /dev/null @@ -1 +0,0 @@ -angular.module("OmniBinder",[]).factory("obBinder",["$timeout","$q","$parse","$window","obSyncEvents","obBinderTypes","obModelWriter","obObserver",function(a,b,c,d,e,f,g,h){function i(a,b,c,d){if(d=d||{},!c)throw new Error("protocol is required");if(!a)throw new Error("scope is required");if(!b)throw new Error("model is required");if(d.key&&"string"!=typeof d.key)throw new Error("key must be a string");this.protocol=c,this.scope=a,this.model=b,this.query=d.query,this.type=d.type,this.key=d.key,this.bindModel(this.type,a,b),this.protocol.subscribe(this),this.ignoreNModelChanges=0,this.ignoreNProtocolChanges=0}return i.prototype.bindModel=function(a,b,c){switch(a){case f.COLLECTION:this.observer=h.observeCollection(this,b[c],this.onModelChange)}},i.prototype.onModelChange=function(a){for(var b=0,c={changes:a},d=0;dc;)d(b[c]),c++;a.removed.length&&angular.forEach(a.removed,function(a){Object.unobserve(a,h.observers[a])})}var g,h=this;return angular.forEach(b,d),g=new ArrayObserver(b,e)}}]),angular.module("OmniBinder").value("obSyncEvents",{NEW:"new",UPDATED:"update",DELETED:"deleted",RECONFIGURED:"reconfigured",READ:"read",MOVE:"move",NONE:"none",INIT:"init",UNKNOWN:"unknown"}); \ No newline at end of file diff --git a/package.json b/package.json index 48fb34bb..21745bc6 100644 --- a/package.json +++ b/package.json @@ -1,35 +1,65 @@ { "name": "angularfire", - "version": "0.7.0", - "description": "An officially supported AngularJS binding for Firebase.", - "main": "angularfire.js", + "description": "The officially supported AngularJS binding for Firebase", + "version": "2.3.0", + "author": "Firebase (https://firebase.google.com/)", + "homepage": "https://github.com/firebase/angularfire", "repository": { "type": "git", - "url": "https://github.com/firebase/angularFire.git" + "url": "https://github.com/firebase/angularfire.git" }, "bugs": { - "url": "https://github.com/firebase/angularFire/issues" + "url": "https://github.com/firebase/angularfire/issues" + }, + "license": "MIT", + "keywords": [ + "angular", + "angularjs", + "firebase", + "realtime" + ], + "main": "index.js", + "files": [ + "index.js", + "dist/**", + "LICENSE", + "README.md", + "package.json" + ], + "dependencies": { + "firebase": "3.x.x" + }, + "peerDependencies": { + "angular": "^1.3.0", + "firebase": "3.x.x" }, "devDependencies": { - "grunt": "~0.4.1", - "grunt-contrib-uglify": "~0.2.2", - "grunt-notify": "~0.2.7", - "grunt-contrib-watch": "~0.5.1", - "grunt-contrib-jshint": "~0.6.2", - "grunt-karma": "~0.6.2", - "grunt-exec": "~0.4.2", - "grunt-conventional-changelog": "~1.0.0", - "load-grunt-tasks": "~0.2.0", - "karma-jasmine": "~0.1.3", - "karma-script-launcher": "~0.1.0", - "karma-firefox-launcher": "~0.1.0", - "karma-html2js-preprocessor": "~0.1.0", - "karma-requirejs": "~0.1.0", - "karma-coffee-preprocessor": "~0.1.0", - "karma-phantomjs-launcher": "~0.1.0", - "karma": "~0.10.4", - "karma-chrome-launcher": "~0.1.0", - "protractor": "~0.12.1", - "lodash": "~2.4.1" + "angular": "^1.3.0", + "angular-mocks": "^1.6.0", + "coveralls": "^2.11.2", + "grunt": "^1.0.1", + "grunt-cli": "^1.2.0", + "grunt-contrib-concat": "^1.0.1", + "grunt-contrib-connect": "^1.0.2", + "grunt-contrib-jshint": "^0.11.0", + "grunt-contrib-uglify": "^2.0.0", + "grunt-contrib-watch": "^1.0.0", + "grunt-karma": "^2.0.0", + "grunt-notify": "^0.4.1", + "grunt-protractor-runner": "^4.0.0", + "grunt-shell-spawn": "^0.3.1", + "jasmine-core": "^2.2.0", + "jasmine-spec-reporter": "^2.1.0", + "karma": "^1.3.0", + "karma-chrome-launcher": "^2.0.0", + "karma-coverage": "^1.1.1", + "karma-failed-reporter": "0.0.3", + "karma-firefox-launcher": "^1.0.0", + "karma-html2js-preprocessor": "^1.1.0", + "karma-jasmine": "^1.1.0", + "karma-sauce-launcher": "^1.1.0", + "karma-spec-reporter": "^0.0.26", + "load-grunt-tasks": "^3.1.0", + "protractor": "^4.0.13" } } diff --git a/src/auth/FirebaseAuth.js b/src/auth/FirebaseAuth.js new file mode 100644 index 00000000..1b30251e --- /dev/null +++ b/src/auth/FirebaseAuth.js @@ -0,0 +1,348 @@ +(function() { + 'use strict'; + var FirebaseAuth; + + // Define a service which provides user authentication and management. + angular.module('firebase.auth').factory('$firebaseAuth', [ + '$q', '$firebaseUtils', function($q, $firebaseUtils) { + /** + * This factory returns an object allowing you to manage the client's authentication state. + * + * @param {Firebase.auth.Auth} auth A Firebase auth instance to authenticate. + * @return {object} An object containing methods for authenticating clients, retrieving + * authentication state, and managing users. + */ + return function(auth) { + auth = auth || firebase.auth(); + + var firebaseAuth = new FirebaseAuth($q, $firebaseUtils, auth); + return firebaseAuth.construct(); + }; + } + ]); + + FirebaseAuth = function($q, $firebaseUtils, auth) { + this._q = $q; + this._utils = $firebaseUtils; + + if (typeof auth === 'string') { + throw new Error('The $firebaseAuth service accepts a Firebase auth instance (or nothing) instead of a URL.'); + } else if (typeof auth.ref !== 'undefined') { + throw new Error('The $firebaseAuth service accepts a Firebase auth instance (or nothing) instead of a Database reference.'); + } + + this._auth = auth; + this._initialAuthResolver = this._initAuthResolver(); + }; + + FirebaseAuth.prototype = { + construct: function() { + this._object = { + // Authentication methods + $signInWithCustomToken: this.signInWithCustomToken.bind(this), + $signInAnonymously: this.signInAnonymously.bind(this), + $signInWithEmailAndPassword: this.signInWithEmailAndPassword.bind(this), + $signInWithPopup: this.signInWithPopup.bind(this), + $signInWithRedirect: this.signInWithRedirect.bind(this), + $signInWithCredential: this.signInWithCredential.bind(this), + $signOut: this.signOut.bind(this), + + // Authentication state methods + $onAuthStateChanged: this.onAuthStateChanged.bind(this), + $getAuth: this.getAuth.bind(this), + $requireSignIn: this.requireSignIn.bind(this), + $waitForSignIn: this.waitForSignIn.bind(this), + + // User management methods + $createUserWithEmailAndPassword: this.createUserWithEmailAndPassword.bind(this), + $updatePassword: this.updatePassword.bind(this), + $updateEmail: this.updateEmail.bind(this), + $deleteUser: this.deleteUser.bind(this), + $sendPasswordResetEmail: this.sendPasswordResetEmail.bind(this), + + // Hack: needed for tests + _: this + }; + + return this._object; + }, + + + /********************/ + /* Authentication */ + /********************/ + + /** + * Authenticates the Firebase reference with a custom authentication token. + * + * @param {string} authToken An authentication token or a Firebase Secret. A Firebase Secret + * should only be used for authenticating a server process and provides full read / write + * access to the entire Firebase. + * @return {Promise} A promise fulfilled with an object containing authentication data. + */ + signInWithCustomToken: function(authToken) { + return this._q.when(this._auth.signInWithCustomToken(authToken)); + }, + + /** + * Authenticates the Firebase reference anonymously. + * + * @return {Promise} A promise fulfilled with an object containing authentication data. + */ + signInAnonymously: function() { + return this._q.when(this._auth.signInAnonymously()); + }, + + /** + * Authenticates the Firebase reference with an email/password user. + * + * @param {String} email An email address for the new user. + * @param {String} password A password for the new email. + * @return {Promise} A promise fulfilled with an object containing authentication data. + */ + signInWithEmailAndPassword: function(email, password) { + return this._q.when(this._auth.signInWithEmailAndPassword(email, password)); + }, + + /** + * Authenticates the Firebase reference with the OAuth popup flow. + * + * @param {object|string} provider A firebase.auth.AuthProvider or a unique provider ID like 'facebook'. + * @return {Promise} A promise fulfilled with an object containing authentication data. + */ + signInWithPopup: function(provider) { + return this._q.when(this._auth.signInWithPopup(this._getProvider(provider))); + }, + + /** + * Authenticates the Firebase reference with the OAuth redirect flow. + * + * @param {object|string} provider A firebase.auth.AuthProvider or a unique provider ID like 'facebook'. + * @return {Promise} A promise fulfilled with an object containing authentication data. + */ + signInWithRedirect: function(provider) { + return this._q.when(this._auth.signInWithRedirect(this._getProvider(provider))); + }, + + /** + * Authenticates the Firebase reference with an OAuth token. + * + * @param {firebase.auth.AuthCredential} credential The Firebase credential. + * @return {Promise} A promise fulfilled with an object containing authentication data. + */ + signInWithCredential: function(credential) { + return this._q.when(this._auth.signInWithCredential(credential)); + }, + + /** + * Unauthenticates the Firebase reference. + */ + signOut: function() { + if (this.getAuth() !== null) { + return this._q.when(this._auth.signOut()); + } else { + return this._q.when(); + } + }, + + + /**************************/ + /* Authentication State */ + /**************************/ + /** + * Asynchronously fires the provided callback with the current authentication data every time + * the authentication data changes. It also fires as soon as the authentication data is + * retrieved from the server. + * + * @param {function} callback A callback that fires when the client's authenticate state + * changes. If authenticated, the callback will be passed an object containing authentication + * data according to the provider used to authenticate. Otherwise, it will be passed null. + * @param {string} [context] If provided, this object will be used as this when calling your + * callback. + * @return {Promise} A promised fulfilled with a function which can be used to + * deregister the provided callback. + */ + onAuthStateChanged: function(callback, context) { + var fn = this._utils.debounce(callback, context, 0); + var off = this._auth.onAuthStateChanged(fn); + + // Return a method to detach the `onAuthStateChanged()` callback. + return off; + }, + + /** + * Synchronously retrieves the current authentication data. + * + * @return {Object} The client's authentication data. + */ + getAuth: function() { + return this._auth.currentUser; + }, + + /** + * Helper onAuthStateChanged() callback method for the two router-related methods. + * + * @param {boolean} rejectIfAuthDataIsNull Determines if the returned promise should be + * resolved or rejected upon an unauthenticated client. + * @param {boolean} rejectIfEmailNotVerified Determines if the returned promise should be + * resolved or rejected upon a client without a verified email address. + * @return {Promise} A promise fulfilled with the client's authentication state or + * rejected if the client is unauthenticated and rejectIfAuthDataIsNull is true. + */ + _routerMethodOnAuthPromise: function(rejectIfAuthDataIsNull, rejectIfEmailNotVerified) { + var self = this; + + // wait for the initial auth state to resolve; on page load we have to request auth state + // asynchronously so we don't want to resolve router methods or flash the wrong state + return this._initialAuthResolver.then(function() { + // auth state may change in the future so rather than depend on the initially resolved state + // we also check the auth data (synchronously) if a new promise is requested, ensuring we resolve + // to the current auth state and not a stale/initial state + var authData = self.getAuth(), res = null; + if (rejectIfAuthDataIsNull && authData === null) { + res = self._q.reject("AUTH_REQUIRED"); + } + else if (rejectIfEmailNotVerified && !authData.emailVerified) { + res = self._q.reject("EMAIL_VERIFICATION_REQUIRED"); + } + else { + res = self._q.when(authData); + } + return res; + }); + }, + + /** + * Helper method to turn provider names into AuthProvider instances + * + * @param {object} stringOrProvider Provider ID string to AuthProvider instance + * @return {firebdase.auth.AuthProvider} A valid AuthProvider instance + */ + _getProvider: function (stringOrProvider) { + var provider; + if (typeof stringOrProvider == "string") { + var providerID = stringOrProvider.slice(0, 1).toUpperCase() + stringOrProvider.slice(1); + provider = new firebase.auth[providerID+"AuthProvider"](); + } else { + provider = stringOrProvider; + } + return provider; + }, + + /** + * Helper that returns a promise which resolves when the initial auth state has been + * fetched from the Firebase server. This never rejects and resolves to undefined. + * + * @return {Promise} A promise fulfilled when the server returns initial auth state. + */ + _initAuthResolver: function() { + var auth = this._auth; + + return this._q(function(resolve) { + var off; + function callback() { + // Turn off this onAuthStateChanged() callback since we just needed to get the authentication data once. + off(); + resolve(); + } + off = auth.onAuthStateChanged(callback); + }); + }, + + /** + * Utility method which can be used in a route's resolve() method to require that a route has + * a logged in client. + * + * @param {boolean} requireEmailVerification Determines if the route requires a client with a + * verified email address. + * @returns {Promise} A promise fulfilled with the client's current authentication + * state or rejected if the client is not authenticated. + */ + requireSignIn: function(requireEmailVerification) { + return this._routerMethodOnAuthPromise(true, requireEmailVerification); + }, + + /** + * Utility method which can be used in a route's resolve() method to grab the current + * authentication data. + * + * @returns {Promise} A promise fulfilled with the client's current authentication + * state, which will be null if the client is not authenticated. + */ + waitForSignIn: function() { + return this._routerMethodOnAuthPromise(false, false); + }, + + /*********************/ + /* User Management */ + /*********************/ + /** + * Creates a new email/password user. Note that this function only creates the user, if you + * wish to log in as the newly created user, call $authWithPassword() after the promise for + * this method has been resolved. + * + * @param {string} email An email for this user. + * @param {string} password A password for this user. + * @return {Promise} A promise fulfilled with the user object, which contains the + * uid of the created user. + */ + createUserWithEmailAndPassword: function(email, password) { + return this._q.when(this._auth.createUserWithEmailAndPassword(email, password)); + }, + + /** + * Changes the password for an email/password user. + * + * @param {string} password A new password for the current user. + * @return {Promise<>} An empty promise fulfilled once the password change is complete. + */ + updatePassword: function(password) { + var user = this.getAuth(); + if (user) { + return this._q.when(user.updatePassword(password)); + } else { + return this._q.reject("Cannot update password since there is no logged in user."); + } + }, + + /** + * Changes the email for an email/password user. + * + * @param {String} email The new email for the currently logged in user. + * @return {Promise<>} An empty promise fulfilled once the email change is complete. + */ + updateEmail: function(email) { + var user = this.getAuth(); + if (user) { + return this._q.when(user.updateEmail(email)); + } else { + return this._q.reject("Cannot update email since there is no logged in user."); + } + }, + + /** + * Deletes the currently logged in user. + * + * @return {Promise<>} An empty promise fulfilled once the user is removed. + */ + deleteUser: function() { + var user = this.getAuth(); + if (user) { + return this._q.when(user.delete()); + } else { + return this._q.reject("Cannot delete user since there is no logged in user."); + } + }, + + + /** + * Sends a password reset email to an email/password user. + * + * @param {string} email An email address to send a password reset to. + * @return {Promise<>} An empty promise fulfilled once the reset password email is sent. + */ + sendPasswordResetEmail: function(email) { + return this._q.when(this._auth.sendPasswordResetEmail(email)); + } + }; +})(); diff --git a/src/auth/firebaseAuthService.js b/src/auth/firebaseAuthService.js new file mode 100644 index 00000000..864580f7 --- /dev/null +++ b/src/auth/firebaseAuthService.js @@ -0,0 +1,12 @@ +(function() { + "use strict"; + + function FirebaseAuthService($firebaseAuth) { + return $firebaseAuth(); + } + FirebaseAuthService.$inject = ['$firebaseAuth']; + + angular.module('firebase.auth') + .factory('$firebaseAuthService', FirebaseAuthService); + +})(); diff --git a/src/database/FirebaseArray.js b/src/database/FirebaseArray.js new file mode 100644 index 00000000..18889ef1 --- /dev/null +++ b/src/database/FirebaseArray.js @@ -0,0 +1,758 @@ +(function() { + 'use strict'; + /** + * Creates and maintains a synchronized list of data. This is a pseudo-read-only array. One should + * not call splice(), push(), pop(), et al directly on this array, but should instead use the + * $remove and $add methods. + * + * It is acceptable to .sort() this array, but it is important to use this in conjunction with + * $watch(), so that it will be re-sorted any time the server data changes. Examples of this are + * included in the $watch documentation. + * + * Internally, the $firebase object depends on this class to provide several $$ (i.e. protected) + * methods, which it invokes to notify the array whenever a change has been made at the server: + * $$added - called whenever a child_added event occurs + * $$updated - called whenever a child_changed event occurs + * $$moved - called whenever a child_moved event occurs + * $$removed - called whenever a child_removed event occurs + * $$error - called when listeners are canceled due to a security error + * $$process - called immediately after $$added/$$updated/$$moved/$$removed + * (assuming that these methods do not abort by returning false or null) + * to splice/manipulate the array and invoke $$notify + * + * Additionally, these methods may be of interest to devs extending this class: + * $$notify - triggers notifications to any $watch listeners, called by $$process + * $$getKey - determines how to look up a record's key (returns $id by default) + * + * Instead of directly modifying this class, one should generally use the $extend + * method to add or change how methods behave. $extend modifies the prototype of + * the array class by returning a clone of $firebaseArray. + * + *
    
    +   * var ExtendedArray = $firebaseArray.$extend({
    +   *    // add a new method to the prototype
    +   *    foo: function() { return 'bar'; },
    +   *
    +   *    // change how records are created
    +   *    $$added: function(snap, prevChild) {
    +   *       return new Widget(snap, prevChild);
    +   *    },
    +   *
    +   *    // change how records are updated
    +   *    $$updated: function(snap) {
    +   *      return this.$getRecord(snap.key()).update(snap);
    +   *    }
    +   * });
    +   *
    +   * var list = new ExtendedArray(ref);
    +   * 
    + */ + angular.module('firebase.database').factory('$firebaseArray', ["$log", "$firebaseUtils", "$q", + function($log, $firebaseUtils, $q) { + /** + * This constructor should probably never be called manually. It is used internally by + * $firebase.$asArray(). + * + * @param {Firebase} ref + * @returns {Array} + * @constructor + */ + function FirebaseArray(ref) { + if( !(this instanceof FirebaseArray) ) { + return new FirebaseArray(ref); + } + var self = this; + this._observers = []; + this.$list = []; + this._ref = ref; + this._sync = new ArraySyncManager(this); + + $firebaseUtils.assertValidRef(ref, 'Must pass a valid Firebase reference ' + + 'to $firebaseArray (not a string or URL)'); + + // indexCache is a weak hashmap (a lazy list) of keys to array indices, + // items are not guaranteed to stay up to date in this list (since the data + // array can be manually edited without calling the $ methods) and it should + // always be used with skepticism regarding whether it is accurate + // (see $indexFor() below for proper usage) + this._indexCache = {}; + + // Array.isArray will not work on objects which extend the Array class. + // So instead of extending the Array class, we just return an actual array. + // However, it's still possible to extend FirebaseArray and have the public methods + // appear on the array object. We do this by iterating the prototype and binding + // any method that is not prefixed with an underscore onto the final array. + $firebaseUtils.getPublicMethods(self, function(fn, key) { + self.$list[key] = fn.bind(self); + }); + + this._sync.init(this.$list); + + // $resolved provides quick access to the current state of the $loaded() promise. + // This is useful in data-binding when needing to delay the rendering or visibilty + // of the data until is has been loaded from firebase. + this.$list.$resolved = false; + this.$loaded().finally(function() { + self.$list.$resolved = true; + }); + + return this.$list; + } + + FirebaseArray.prototype = { + /** + * Create a new record with a unique ID and add it to the end of the array. + * This should be used instead of Array.prototype.push, since those changes will not be + * synchronized with the server. + * + * Any value, including a primitive, can be added in this way. Note that when the record + * is created, the primitive value would be stored in $value (records are always objects + * by default). + * + * Returns a future which is resolved when the data has successfully saved to the server. + * The resolve callback will be passed a Firebase ref representing the new data element. + * + * @param data + * @returns a promise resolved after data is added + */ + $add: function(data) { + this._assertNotDestroyed('$add'); + var self = this; + var def = $q.defer(); + var ref = this.$ref().ref.push(); + var dataJSON; + + try { + dataJSON = $firebaseUtils.toJSON(data); + } catch (err) { + def.reject(err); + } + + if (typeof dataJSON !== 'undefined') { + $firebaseUtils.doSet(ref, dataJSON).then(function() { + self.$$notify('child_added', ref.key); + def.resolve(ref); + }).catch(def.reject); + } + + return def.promise; + }, + + /** + * Pass either an item in the array or the index of an item and it will be saved back + * to Firebase. While the array is read-only and its structure should not be changed, + * it is okay to modify properties on the objects it contains and then save those back + * individually. + * + * Returns a future which is resolved when the data has successfully saved to the server. + * The resolve callback will be passed a Firebase ref representing the saved element. + * If passed an invalid index or an object which is not a record in this array, + * the promise will be rejected. + * + * @param {int|object} indexOrItem + * @returns a promise resolved after data is saved + */ + $save: function(indexOrItem) { + this._assertNotDestroyed('$save'); + var self = this; + var item = self._resolveItem(indexOrItem); + var key = self.$keyAt(item); + var def = $q.defer(); + + if( key !== null ) { + var ref = self.$ref().ref.child(key); + var dataJSON; + + try { + dataJSON = $firebaseUtils.toJSON(item); + } catch (err) { + def.reject(err); + } + + if (typeof dataJSON !== 'undefined') { + $firebaseUtils.doSet(ref, dataJSON).then(function() { + self.$$notify('child_changed', key); + def.resolve(ref); + }).catch(def.reject); + } + } + else { + def.reject('Invalid record; could not determine key for '+indexOrItem); + } + + return def.promise; + }, + + /** + * Pass either an existing item in this array or the index of that item and it will + * be removed both locally and in Firebase. This should be used in place of + * Array.prototype.splice for removing items out of the array, as calling splice + * will not update the value on the server. + * + * Returns a future which is resolved when the data has successfully removed from the + * server. The resolve callback will be passed a Firebase ref representing the deleted + * element. If passed an invalid index or an object which is not a record in this array, + * the promise will be rejected. + * + * @param {int|object} indexOrItem + * @returns a promise which resolves after data is removed + */ + $remove: function(indexOrItem) { + this._assertNotDestroyed('$remove'); + var key = this.$keyAt(indexOrItem); + if( key !== null ) { + var ref = this.$ref().ref.child(key); + return $firebaseUtils.doRemove(ref).then(function() { + return ref; + }); + } + else { + return $q.reject('Invalid record; could not determine key for '+indexOrItem); + } + }, + + /** + * Given an item in this array or the index of an item in the array, this returns the + * Firebase key (record.$id) for that record. If passed an invalid key or an item which + * does not exist in this array, it will return null. + * + * @param {int|object} indexOrItem + * @returns {null|string} + */ + $keyAt: function(indexOrItem) { + var item = this._resolveItem(indexOrItem); + return this.$$getKey(item); + }, + + /** + * The inverse of $keyAt, this method takes a Firebase key (record.$id) and returns the + * index in the array where that record is stored. If the record is not in the array, + * this method returns -1. + * + * @param {String} key + * @returns {int} -1 if not found + */ + $indexFor: function(key) { + var self = this; + var cache = self._indexCache; + // evaluate whether our key is cached and, if so, whether it is up to date + if( !cache.hasOwnProperty(key) || self.$keyAt(cache[key]) !== key ) { + // update the hashmap + var pos = self.$list.findIndex(function(rec) { return self.$$getKey(rec) === key; }); + if( pos !== -1 ) { + cache[key] = pos; + } + } + return cache.hasOwnProperty(key)? cache[key] : -1; + }, + + /** + * The loaded method is invoked after the initial batch of data arrives from the server. + * When this resolves, all data which existed prior to calling $asArray() is now cached + * locally in the array. + * + * As a shortcut is also possible to pass resolve/reject methods directly into this + * method just as they would be passed to .then() + * + * @param {Function} [resolve] + * @param {Function} [reject] + * @returns a promise + */ + $loaded: function(resolve, reject) { + var promise = this._sync.ready(); + if( arguments.length ) { + // allow this method to be called just like .then + // by passing any arguments on to .then + promise = promise.then.call(promise, resolve, reject); + } + return promise; + }, + + /** + * @returns {Firebase} the original Firebase ref used to create this object. + */ + $ref: function() { return this._ref; }, + + /** + * Listeners passed into this method are notified whenever a new change (add, updated, + * move, remove) is received from the server. Each invocation is sent an object + * containing { type: 'child_added|child_updated|child_moved|child_removed', + * key: 'key_of_item_affected'} + * + * Additionally, added and moved events receive a prevChild parameter, containing the + * key of the item before this one in the array. + * + * This method returns a function which can be invoked to stop observing events. + * + * @param {Function} cb + * @param {Object} [context] + * @returns {Function} used to stop observing + */ + $watch: function(cb, context) { + var list = this._observers; + list.push([cb, context]); + // an off function for cancelling the listener + return function() { + var i = list.findIndex(function(parts) { + return parts[0] === cb && parts[1] === context; + }); + if( i > -1 ) { + list.splice(i, 1); + } + }; + }, + + /** + * Informs $firebase to stop sending events and clears memory being used + * by this array (delete's its local content). + */ + $destroy: function(err) { + if( !this._isDestroyed ) { + this._isDestroyed = true; + this._sync.destroy(err); + this.$list.length = 0; + } + }, + + /** + * Returns the record for a given Firebase key (record.$id). If the record is not found + * then returns null. + * + * @param {string} key + * @returns {Object|null} a record in this array + */ + $getRecord: function(key) { + var i = this.$indexFor(key); + return i > -1? this.$list[i] : null; + }, + + /** + * Called to inform the array when a new item has been added at the server. + * This method should return the record (an object) that will be passed into $$process + * along with the add event. Alternately, the record will be skipped if this method returns + * a falsey value. + * + * @param {object} snap a Firebase snapshot + * @param {string} prevChild + * @return {object} the record to be inserted into the array + * @protected + */ + $$added: function(snap/*, prevChild*/) { + // check to make sure record does not exist + var i = this.$indexFor(snap.key); + if( i === -1 ) { + // parse data and create record + var rec = snap.val(); + if( !angular.isObject(rec) ) { + rec = { $value: rec }; + } + rec.$id = snap.key; + rec.$priority = snap.getPriority(); + $firebaseUtils.applyDefaults(rec, this.$$defaults); + + return rec; + } + return false; + }, + + /** + * Called whenever an item is removed at the server. + * This method does not physically remove the objects, but instead + * returns a boolean indicating whether it should be removed (and + * taking any other desired actions before the remove completes). + * + * @param {object} snap a Firebase snapshot + * @return {boolean} true if item should be removed + * @protected + */ + $$removed: function(snap) { + return this.$indexFor(snap.key) > -1; + }, + + /** + * Called whenever an item is changed at the server. + * This method should apply the changes, including changes to data + * and to $priority, and then return true if any changes were made. + * + * If this method returns false, then $$process will not be invoked, + * which means that $$notify will not take place and no $watch events + * will be triggered. + * + * @param {object} snap a Firebase snapshot + * @return {boolean} true if any data changed + * @protected + */ + $$updated: function(snap) { + var changed = false; + var rec = this.$getRecord(snap.key); + if( angular.isObject(rec) ) { + // apply changes to the record + changed = $firebaseUtils.updateRec(rec, snap); + $firebaseUtils.applyDefaults(rec, this.$$defaults); + } + return changed; + }, + + /** + * Called whenever an item changes order (moves) on the server. + * This method should set $priority to the updated value and return true if + * the record should actually be moved. It should not actually apply the move + * operation. + * + * If this method returns false, then the record will not be moved in the array + * and no $watch listeners will be notified. (When true, $$process is invoked + * which invokes $$notify) + * + * @param {object} snap a Firebase snapshot + * @param {string} prevChild + * @protected + */ + $$moved: function(snap/*, prevChild*/) { + var rec = this.$getRecord(snap.key); + if( angular.isObject(rec) ) { + rec.$priority = snap.getPriority(); + return true; + } + return false; + }, + + /** + * Called whenever a security error or other problem causes the listeners to become + * invalid. This is generally an unrecoverable error. + * + * @param {Object} err which will have a `code` property and possibly a `message` + * @protected + */ + $$error: function(err) { + $log.error(err); + this.$destroy(err); + }, + + /** + * Returns ID for a given record + * @param {object} rec + * @returns {string||null} + * @protected + */ + $$getKey: function(rec) { + return angular.isObject(rec)? rec.$id : null; + }, + + /** + * Handles placement of recs in the array, sending notifications, + * and other internals. Called by the synchronization process + * after $$added, $$updated, $$moved, and $$removed return a truthy value. + * + * @param {string} event one of child_added, child_removed, child_moved, or child_changed + * @param {object} rec + * @param {string} [prevChild] + * @protected + */ + $$process: function(event, rec, prevChild) { + var key = this.$$getKey(rec); + var changed = false; + var curPos; + switch(event) { + case 'child_added': + curPos = this.$indexFor(key); + break; + case 'child_moved': + curPos = this.$indexFor(key); + this._spliceOut(key); + break; + case 'child_removed': + // remove record from the array + changed = this._spliceOut(key) !== null; + break; + case 'child_changed': + changed = true; + break; + default: + throw new Error('Invalid event type: ' + event); + } + if( angular.isDefined(curPos) ) { + // add it to the array + changed = this._addAfter(rec, prevChild) !== curPos; + } + if( changed ) { + // send notifications to anybody monitoring $watch + this.$$notify(event, key, prevChild); + } + return changed; + }, + + /** + * Used to trigger notifications for listeners registered using $watch. This method is + * typically invoked internally by the $$process method. + * + * @param {string} event + * @param {string} key + * @param {string} [prevChild] + * @protected + */ + $$notify: function(event, key, prevChild) { + var eventData = {event: event, key: key}; + if( angular.isDefined(prevChild) ) { + eventData.prevChild = prevChild; + } + angular.forEach(this._observers, function(parts) { + parts[0].call(parts[1], eventData); + }); + }, + + /** + * Used to insert a new record into the array at a specific position. If prevChild is + * null, is inserted first, if prevChild is not found, it is inserted last, otherwise, + * it goes immediately after prevChild. + * + * @param {object} rec + * @param {string|null} prevChild + * @private + */ + _addAfter: function(rec, prevChild) { + var i; + if( prevChild === null ) { + i = 0; + } + else { + i = this.$indexFor(prevChild)+1; + if( i === 0 ) { i = this.$list.length; } + } + this.$list.splice(i, 0, rec); + this._indexCache[this.$$getKey(rec)] = i; + return i; + }, + + /** + * Removes a record from the array by calling splice. If the item is found + * this method returns it. Otherwise, this method returns null. + * + * @param {string} key + * @returns {object|null} + * @private + */ + _spliceOut: function(key) { + var i = this.$indexFor(key); + if( i > -1 ) { + delete this._indexCache[key]; + return this.$list.splice(i, 1)[0]; + } + return null; + }, + + /** + * Resolves a variable which may contain an integer or an item that exists in this array. + * Returns the item or null if it does not exist. + * + * @param indexOrItem + * @returns {*} + * @private + */ + _resolveItem: function(indexOrItem) { + var list = this.$list; + if( angular.isNumber(indexOrItem) && indexOrItem >= 0 && list.length >= indexOrItem ) { + return list[indexOrItem]; + } + else if( angular.isObject(indexOrItem) ) { + // it must be an item in this array; it's not sufficient for it just to have + // a $id or even a $id that is in the array, it must be an actual record + // the fastest way to determine this is to use $getRecord (to avoid iterating all recs) + // and compare the two + var key = this.$$getKey(indexOrItem); + var rec = this.$getRecord(key); + return rec === indexOrItem? rec : null; + } + return null; + }, + + /** + * Throws an error if $destroy has been called. Should be used for any function + * which tries to write data back to $firebase. + * @param {string} method + * @private + */ + _assertNotDestroyed: function(method) { + if( this._isDestroyed ) { + throw new Error('Cannot call ' + method + ' method on a destroyed $firebaseArray object'); + } + } + }; + + /** + * This method allows FirebaseArray to be inherited by child classes. Methods passed into this + * function will be added onto the array's prototype. They can override existing methods as + * well. + * + * In addition to passing additional methods, it is also possible to pass in a class function. + * The prototype on that class function will be preserved, and it will inherit from + * FirebaseArray. It's also possible to do both, passing a class to inherit and additional + * methods to add onto the prototype. + * + *
    
    +       * var ExtendedArray = $firebaseArray.$extend({
    +       *    // add a method onto the prototype that sums all items in the array
    +       *    getSum: function() {
    +       *       var ct = 0;
    +       *       angular.forEach(this.$list, function(rec) { ct += rec.x; });
    +        *      return ct;
    +       *    }
    +       * });
    +       *
    +       * // use our new factory in place of $firebaseArray
    +       * var list = new ExtendedArray(ref);
    +       * 
    + * + * @param {Function} [ChildClass] a child class which should inherit FirebaseArray + * @param {Object} [methods] a list of functions to add onto the prototype + * @returns {Function} a child class suitable for use with $firebase (this will be ChildClass if provided) + * @static + */ + FirebaseArray.$extend = function(ChildClass, methods) { + if( arguments.length === 1 && angular.isObject(ChildClass) ) { + methods = ChildClass; + ChildClass = function(ref) { + if( !(this instanceof ChildClass) ) { + return new ChildClass(ref); + } + FirebaseArray.apply(this, arguments); + return this.$list; + }; + } + return $firebaseUtils.inherit(ChildClass, FirebaseArray, methods); + }; + + function ArraySyncManager(firebaseArray) { + function destroy(err) { + if( !sync.isDestroyed ) { + sync.isDestroyed = true; + var ref = firebaseArray.$ref(); + ref.off('child_added', created); + ref.off('child_moved', moved); + ref.off('child_changed', updated); + ref.off('child_removed', removed); + firebaseArray = null; + initComplete(err||'destroyed'); + } + } + + function init($list) { + var ref = firebaseArray.$ref(); + + // listen for changes at the Firebase instance + ref.on('child_added', created, error); + ref.on('child_moved', moved, error); + ref.on('child_changed', updated, error); + ref.on('child_removed', removed, error); + + // determine when initial load is completed + ref.once('value', function(snap) { + if (angular.isArray(snap.val())) { + $log.warn('Storing data using array indices in Firebase can result in unexpected behavior. See https://firebase.google.com/docs/database/web/structure-data for more information.'); + } + + initComplete(null, $list); + }, initComplete); + } + + // call initComplete(), do not call this directly + function _initComplete(err, result) { + if( !isResolved ) { + isResolved = true; + if( err ) { def.reject(err); } + else { def.resolve(result); } + } + } + + var def = $q.defer(); + var created = function(snap, prevChild) { + if (!firebaseArray) { + return; + } + waitForResolution(firebaseArray.$$added(snap, prevChild), function(rec) { + firebaseArray.$$process('child_added', rec, prevChild); + }); + }; + var updated = function(snap) { + if (!firebaseArray) { + return; + } + var rec = firebaseArray.$getRecord(snap.key); + if( rec ) { + waitForResolution(firebaseArray.$$updated(snap), function() { + firebaseArray.$$process('child_changed', rec); + }); + } + }; + var moved = function(snap, prevChild) { + if (!firebaseArray) { + return; + } + var rec = firebaseArray.$getRecord(snap.key); + if( rec ) { + waitForResolution(firebaseArray.$$moved(snap, prevChild), function() { + firebaseArray.$$process('child_moved', rec, prevChild); + }); + } + }; + var removed = function(snap) { + if (!firebaseArray) { + return; + } + var rec = firebaseArray.$getRecord(snap.key); + if( rec ) { + waitForResolution(firebaseArray.$$removed(snap), function() { + firebaseArray.$$process('child_removed', rec); + }); + } + }; + + function waitForResolution(maybePromise, callback) { + var promise = $q.when(maybePromise); + promise.then(function(result){ + if (result) { + callback(result); + } + }); + if (!isResolved) { + resolutionPromises.push(promise); + } + } + + var resolutionPromises = []; + var isResolved = false; + var error = $firebaseUtils.batch(function(err) { + _initComplete(err); + if( firebaseArray ) { + firebaseArray.$$error(err); + } + }); + var initComplete = $firebaseUtils.batch(_initComplete); + + var sync = { + destroy: destroy, + isDestroyed: false, + init: init, + ready: function() { return def.promise.then(function(result){ + return $q.all(resolutionPromises).then(function(){ + return result; + }); + }); } + }; + + return sync; + } + + return FirebaseArray; + } + ]); + + /** @deprecated */ + angular.module('firebase').factory('$FirebaseArray', ['$log', '$firebaseArray', + function($log, $firebaseArray) { + return function() { + $log.warn('$FirebaseArray has been renamed. Use $firebaseArray instead.'); + return $firebaseArray.apply(null, arguments); + }; + } + ]); +})(); diff --git a/src/database/FirebaseObject.js b/src/database/FirebaseObject.js new file mode 100644 index 00000000..1ba690c1 --- /dev/null +++ b/src/database/FirebaseObject.js @@ -0,0 +1,503 @@ +(function() { + 'use strict'; + /** + * Creates and maintains a synchronized object, with 2-way bindings between Angular and Firebase. + * + * Implementations of this class are contracted to provide the following internal methods, + * which are used by the synchronization process and 3-way bindings: + * $$updated - called whenever a change occurs (a value event from Firebase) + * $$error - called when listeners are canceled due to a security error + * $$notify - called to update $watch listeners and trigger updates to 3-way bindings + * $ref - called to obtain the underlying Firebase reference + * + * Instead of directly modifying this class, one should generally use the $extend + * method to add or change how methods behave: + * + *
    
    +   * var ExtendedObject = $firebaseObject.$extend({
    +   *    // add a new method to the prototype
    +   *    foo: function() { return 'bar'; },
    +   * });
    +   *
    +   * var obj = new ExtendedObject(ref);
    +   * 
    + */ + angular.module('firebase.database').factory('$firebaseObject', [ + '$parse', '$firebaseUtils', '$log', '$q', + function($parse, $firebaseUtils, $log, $q) { + /** + * Creates a synchronized object with 2-way bindings between Angular and Firebase. + * + * @param {Firebase} ref + * @returns {FirebaseObject} + * @constructor + */ + function FirebaseObject(ref) { + if( !(this instanceof FirebaseObject) ) { + return new FirebaseObject(ref); + } + var self = this; + // These are private config props and functions used internally + // they are collected here to reduce clutter in console.log and forEach + this.$$conf = { + // synchronizes data to Firebase + sync: new ObjectSyncManager(this, ref), + // stores the Firebase ref + ref: ref, + // synchronizes $scope variables with this object + binding: new ThreeWayBinding(this), + // stores observers registered with $watch + listeners: [] + }; + + // this bit of magic makes $$conf non-enumerable and non-configurable + // and non-writable (its properties are still writable but the ref cannot be replaced) + // we redundantly assign it above so the IDE can relax + Object.defineProperty(this, '$$conf', { + value: this.$$conf + }); + + this.$id = ref.ref.key; + this.$priority = null; + + $firebaseUtils.applyDefaults(this, this.$$defaults); + + // start synchronizing data with Firebase + this.$$conf.sync.init(); + + // $resolved provides quick access to the current state of the $loaded() promise. + // This is useful in data-binding when needing to delay the rendering or visibilty + // of the data until is has been loaded from firebase. + this.$resolved = false; + this.$loaded().finally(function() { + self.$resolved = true; + }); + } + + FirebaseObject.prototype = { + /** + * Saves all data on the FirebaseObject back to Firebase. + * @returns a promise which will resolve after the save is completed. + */ + $save: function () { + var self = this; + var ref = self.$ref(); + var def = $q.defer(); + var dataJSON; + + try { + dataJSON = $firebaseUtils.toJSON(self); + } catch (e) { + def.reject(e); + } + + if (typeof dataJSON !== 'undefined') { + $firebaseUtils.doSet(ref, dataJSON).then(function() { + self.$$notify(); + def.resolve(self.$ref()); + }).catch(def.reject); + } + + return def.promise; + }, + + /** + * Removes all keys from the FirebaseObject and also removes + * the remote data from the server. + * + * @returns a promise which will resolve after the op completes + */ + $remove: function() { + var self = this; + $firebaseUtils.trimKeys(self, {}); + self.$value = null; + return $firebaseUtils.doRemove(self.$ref()).then(function() { + self.$$notify(); + return self.$ref(); + }); + }, + + /** + * The loaded method is invoked after the initial batch of data arrives from the server. + * When this resolves, all data which existed prior to calling $asObject() is now cached + * locally in the object. + * + * As a shortcut is also possible to pass resolve/reject methods directly into this + * method just as they would be passed to .then() + * + * @param {Function} resolve + * @param {Function} reject + * @returns a promise which resolves after initial data is downloaded from Firebase + */ + $loaded: function(resolve, reject) { + var promise = this.$$conf.sync.ready(); + if (arguments.length) { + // allow this method to be called just like .then + // by passing any arguments on to .then + promise = promise.then.call(promise, resolve, reject); + } + return promise; + }, + + /** + * @returns {Firebase} the original Firebase instance used to create this object. + */ + $ref: function () { + return this.$$conf.ref; + }, + + /** + * Creates a 3-way data sync between this object, the Firebase server, and a + * scope variable. This means that any changes made to the scope variable are + * pushed to Firebase, and vice versa. + * + * If scope emits a $destroy event, the binding is automatically severed. Otherwise, + * it is possible to unbind the scope variable by using the `unbind` function + * passed into the resolve method. + * + * Can only be bound to one scope variable at a time. If a second is attempted, + * the promise will be rejected with an error. + * + * @param {object} scope + * @param {string} varName + * @returns a promise which resolves to an unbind method after data is set in scope + */ + $bindTo: function (scope, varName) { + var self = this; + return self.$loaded().then(function () { + return self.$$conf.binding.bindTo(scope, varName); + }); + }, + + /** + * Listeners passed into this method are notified whenever a new change is received + * from the server. Each invocation is sent an object containing + * { type: 'value', key: 'my_firebase_id' } + * + * This method returns an unbind function that can be used to detach the listener. + * + * @param {Function} cb + * @param {Object} [context] + * @returns {Function} invoke to stop observing events + */ + $watch: function (cb, context) { + var list = this.$$conf.listeners; + list.push([cb, context]); + // an off function for cancelling the listener + return function () { + var i = list.findIndex(function (parts) { + return parts[0] === cb && parts[1] === context; + }); + if (i > -1) { + list.splice(i, 1); + } + }; + }, + + /** + * Informs $firebase to stop sending events and clears memory being used + * by this object (delete's its local content). + */ + $destroy: function(err) { + var self = this; + if (!self.$isDestroyed) { + self.$isDestroyed = true; + self.$$conf.sync.destroy(err); + self.$$conf.binding.destroy(); + $firebaseUtils.each(self, function (v, k) { + delete self[k]; + }); + } + }, + + /** + * Called by $firebase whenever an item is changed at the server. + * This method must exist on any objectFactory passed into $firebase. + * + * It should return true if any changes were made, otherwise `$$notify` will + * not be invoked. + * + * @param {object} snap a Firebase snapshot + * @return {boolean} true if any changes were made. + */ + $$updated: function (snap) { + // applies new data to this object + var changed = $firebaseUtils.updateRec(this, snap); + // applies any defaults set using $$defaults + $firebaseUtils.applyDefaults(this, this.$$defaults); + // returning true here causes $$notify to be triggered + return changed; + }, + + /** + * Called whenever a security error or other problem causes the listeners to become + * invalid. This is generally an unrecoverable error. + * @param {Object} err which will have a `code` property and possibly a `message` + */ + $$error: function (err) { + // prints an error to the console (via Angular's logger) + $log.error(err); + // frees memory and cancels any remaining listeners + this.$destroy(err); + }, + + /** + * Called internally by $bindTo when data is changed in $scope. + * Should apply updates to this record but should not call + * notify(). + */ + $$scopeUpdated: function(newData) { + // we use a one-directional loop to avoid feedback with 3-way bindings + // since set() is applied locally anyway, this is still performant + var def = $q.defer(); + this.$ref().set($firebaseUtils.toJSON(newData), $firebaseUtils.makeNodeResolver(def)); + return def.promise; + }, + + /** + * Updates any bound scope variables and + * notifies listeners registered with $watch + */ + $$notify: function() { + var self = this, list = this.$$conf.listeners.slice(); + // be sure to do this after setting up data and init state + angular.forEach(list, function (parts) { + parts[0].call(parts[1], {event: 'value', key: self.$id}); + }); + }, + + /** + * Overrides how Angular.forEach iterates records on this object so that only + * fields stored in Firebase are part of the iteration. To include meta fields like + * $id and $priority in the iteration, utilize for(key in obj) instead. + */ + forEach: function(iterator, context) { + return $firebaseUtils.each(this, iterator, context); + } + }; + + /** + * This method allows FirebaseObject to be copied into a new factory. Methods passed into this + * function will be added onto the object's prototype. They can override existing methods as + * well. + * + * In addition to passing additional methods, it is also possible to pass in a class function. + * The prototype on that class function will be preserved, and it will inherit from + * FirebaseObject. It's also possible to do both, passing a class to inherit and additional + * methods to add onto the prototype. + * + * Once a factory is obtained by this method, it can be passed into $firebase as the + * `objectFactory` parameter: + * + *
    
    +       * var MyFactory = $firebaseObject.$extend({
    +       *    // add a method onto the prototype that prints a greeting
    +       *    getGreeting: function() {
    +       *       return 'Hello ' + this.first_name + ' ' + this.last_name + '!';
    +       *    }
    +       * });
    +       *
    +       * // use our new factory in place of $firebaseObject
    +       * var obj = $firebase(ref, {objectFactory: MyFactory}).$asObject();
    +       * 
    + * + * @param {Function} [ChildClass] a child class which should inherit FirebaseObject + * @param {Object} [methods] a list of functions to add onto the prototype + * @returns {Function} a new factory suitable for use with $firebase + */ + FirebaseObject.$extend = function(ChildClass, methods) { + if( arguments.length === 1 && angular.isObject(ChildClass) ) { + methods = ChildClass; + ChildClass = function(ref) { + if( !(this instanceof ChildClass) ) { + return new ChildClass(ref); + } + FirebaseObject.apply(this, arguments); + }; + } + return $firebaseUtils.inherit(ChildClass, FirebaseObject, methods); + }; + + /** + * Creates a three-way data binding on a scope variable. + * + * @param {FirebaseObject} rec + * @returns {*} + * @constructor + */ + function ThreeWayBinding(rec) { + this.subs = []; + this.scope = null; + this.key = null; + this.rec = rec; + } + + ThreeWayBinding.prototype = { + assertNotBound: function(varName) { + if( this.scope ) { + var msg = 'Cannot bind to ' + varName + ' because this instance is already bound to ' + + this.key + '; one binding per instance ' + + '(call unbind method or create another FirebaseObject instance)'; + $log.error(msg); + return $q.reject(msg); + } + }, + + bindTo: function(scope, varName) { + function _bind(self) { + var sending = false; + var parsed = $parse(varName); + var rec = self.rec; + self.scope = scope; + self.varName = varName; + + function equals(scopeValue) { + return angular.equals(scopeValue, rec) && + scopeValue.$priority === rec.$priority && + scopeValue.$value === rec.$value; + } + + function setScope(rec) { + parsed.assign(scope, $firebaseUtils.scopeData(rec)); + } + + var send = $firebaseUtils.debounce(function(val) { + var scopeData = $firebaseUtils.scopeData(val); + rec.$$scopeUpdated(scopeData) + ['finally'](function() { + sending = false; + if(!scopeData.hasOwnProperty('$value')){ + delete rec.$value; + delete parsed(scope).$value; + } + setScope(rec); + } + ); + }, 50, 500); + + var scopeUpdated = function(newVal) { + newVal = newVal[0]; + if( !equals(newVal) ) { + sending = true; + send(newVal); + } + }; + + var recUpdated = function() { + if( !sending && !equals(parsed(scope)) ) { + setScope(rec); + } + }; + + // $watch will not check any vars prefixed with $, so we + // manually check $priority and $value using this method + function watchExp(){ + var obj = parsed(scope); + return [obj, obj.$priority, obj.$value]; + } + + setScope(rec); + self.subs.push(scope.$on('$destroy', self.unbind.bind(self))); + + // monitor scope for any changes + self.subs.push(scope.$watch(watchExp, scopeUpdated, true)); + + // monitor the object for changes + self.subs.push(rec.$watch(recUpdated)); + + return self.unbind.bind(self); + } + + return this.assertNotBound(varName) || _bind(this); + }, + + unbind: function() { + if( this.scope ) { + angular.forEach(this.subs, function(unbind) { + unbind(); + }); + this.subs = []; + this.scope = null; + this.key = null; + } + }, + + destroy: function() { + this.unbind(); + this.rec = null; + } + }; + + function ObjectSyncManager(firebaseObject, ref) { + function destroy(err) { + if( !sync.isDestroyed ) { + sync.isDestroyed = true; + ref.off('value', applyUpdate); + firebaseObject = null; + initComplete(err||'destroyed'); + } + } + + function init() { + ref.on('value', applyUpdate, error); + ref.once('value', function(snap) { + if (angular.isArray(snap.val())) { + $log.warn('Storing data using array indices in Firebase can result in unexpected behavior. See https://firebase.google.com/docs/database/web/structure-data for more information. Also note that you probably wanted $firebaseArray and not $firebaseObject.'); + } + + initComplete(null); + }, initComplete); + } + + // call initComplete(); do not call this directly + function _initComplete(err) { + if( !isResolved ) { + isResolved = true; + if( err ) { def.reject(err); } + else { def.resolve(firebaseObject); } + } + } + + var isResolved = false; + var def = $q.defer(); + var applyUpdate = $firebaseUtils.batch(function(snap) { + if (firebaseObject) { + var changed = firebaseObject.$$updated(snap); + if( changed ) { + // notifies $watch listeners and + // updates $scope if bound to a variable + firebaseObject.$$notify(); + } + } + }); + var error = $firebaseUtils.batch(function(err) { + _initComplete(err); + if( firebaseObject ) { + firebaseObject.$$error(err); + } + }); + var initComplete = $firebaseUtils.batch(_initComplete); + + var sync = { + isDestroyed: false, + destroy: destroy, + init: init, + ready: function() { return def.promise; } + }; + return sync; + } + + return FirebaseObject; + } + ]); + + /** @deprecated */ + angular.module('firebase').factory('$FirebaseObject', ['$log', '$firebaseObject', + function($log, $firebaseObject) { + return function() { + $log.warn('$FirebaseObject has been renamed. Use $firebaseObject instead.'); + return $firebaseObject.apply(null, arguments); + }; + } + ]); +})(); diff --git a/src/database/firebaseRef.js b/src/database/firebaseRef.js new file mode 100644 index 00000000..28e4ef75 --- /dev/null +++ b/src/database/firebaseRef.js @@ -0,0 +1,46 @@ +(function() { + "use strict"; + + function FirebaseRef() { + this.urls = null; + this.registerUrl = function registerUrl(urlOrConfig) { + + if (typeof urlOrConfig === 'string') { + this.urls = {}; + this.urls.default = urlOrConfig; + } + + if (angular.isObject(urlOrConfig)) { + this.urls = urlOrConfig; + } + + }; + + this.$$checkUrls = function $$checkUrls(urlConfig) { + if (!urlConfig) { + return new Error('No Firebase URL registered. Use firebaseRefProvider.registerUrl() in the config phase. This is required if you are using $firebaseAuthService.'); + } + if (!urlConfig.default) { + return new Error('No default Firebase URL registered. Use firebaseRefProvider.registerUrl({ default: "https://.firebaseio.com/"}).'); + } + }; + + this.$$createRefsFromUrlConfig = function $$createMultipleRefs(urlConfig) { + var refs = {}; + var error = this.$$checkUrls(urlConfig); + if (error) { throw error; } + angular.forEach(urlConfig, function(value, key) { + refs[key] = firebase.database().refFromURL(value); + }); + return refs; + }; + + this.$get = function FirebaseRef_$get() { + return this.$$createRefsFromUrlConfig(this.urls); + }; + } + + angular.module('firebase.database') + .provider('$firebaseRef', FirebaseRef); + +})(); diff --git a/src/firebase.js b/src/firebase.js new file mode 100644 index 00000000..04f7d7f5 --- /dev/null +++ b/src/firebase.js @@ -0,0 +1,16 @@ +(function() { + 'use strict'; + + angular.module("firebase") + + /** @deprecated */ + .factory("$firebase", function() { + return function() { + //TODO: Update this error to speak about new module stuff + throw new Error('$firebase has been removed. You may instantiate $firebaseArray and $firebaseObject ' + + 'directly now. For simple write operations, just use the Firebase ref directly. ' + + 'See the AngularFire 1.0.0 changelog for details: https://github.com/firebase/angularfire/releases/tag/v1.0.0'); + }; + }); + +})(); diff --git a/src/lib/polyfills.js b/src/lib/polyfills.js new file mode 100644 index 00000000..d75f11b6 --- /dev/null +++ b/src/lib/polyfills.js @@ -0,0 +1,167 @@ +'use strict'; + +// Shim Array.indexOf for IE compatibility. +if (!Array.prototype.indexOf) { + Array.prototype.indexOf = function (searchElement, fromIndex) { + if (this === undefined || this === null) { + throw new TypeError("'this' is null or not defined"); + } + // Hack to convert object.length to a UInt32 + // jshint -W016 + var length = this.length >>> 0; + fromIndex = +fromIndex || 0; + // jshint +W016 + + if (Math.abs(fromIndex) === Infinity) { + fromIndex = 0; + } + + if (fromIndex < 0) { + fromIndex += length; + if (fromIndex < 0) { + fromIndex = 0; + } + } + + for (;fromIndex < length; fromIndex++) { + if (this[fromIndex] === searchElement) { + return fromIndex; + } + } + + return -1; + }; +} + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind +if (!Function.prototype.bind) { + Function.prototype.bind = function (oThis) { + if (typeof this !== "function") { + // closest thing possible to the ECMAScript 5 + // internal IsCallable function + throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); + } + + var aArgs = Array.prototype.slice.call(arguments, 1), + fToBind = this, + fNOP = function () {}, + fBound = function () { + return fToBind.apply(this instanceof fNOP && oThis + ? this + : oThis, + aArgs.concat(Array.prototype.slice.call(arguments))); + }; + + fNOP.prototype = this.prototype; + fBound.prototype = new fNOP(); + + return fBound; + }; +} + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex +if (!Array.prototype.findIndex) { + Object.defineProperty(Array.prototype, 'findIndex', { + enumerable: false, + configurable: true, + writable: true, + value: function(predicate) { + if (this == null) { + throw new TypeError('Array.prototype.find called on null or undefined'); + } + if (typeof predicate !== 'function') { + throw new TypeError('predicate must be a function'); + } + var list = Object(this); + var length = list.length >>> 0; + var thisArg = arguments[1]; + var value; + + for (var i = 0; i < length; i++) { + if (i in list) { + value = list[i]; + if (predicate.call(thisArg, value, i, list)) { + return i; + } + } + } + return -1; + } + }); +} + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create +if (typeof Object.create != 'function') { + (function () { + var F = function () {}; + Object.create = function (o) { + if (arguments.length > 1) { + throw new Error('Second argument not supported'); + } + if (o === null) { + throw new Error('Cannot set a null [[Prototype]]'); + } + if (typeof o != 'object') { + throw new TypeError('Argument must be an object'); + } + F.prototype = o; + return new F(); + }; + })(); +} + +// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys +if (!Object.keys) { + Object.keys = (function () { + 'use strict'; + var hasOwnProperty = Object.prototype.hasOwnProperty, + hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'), + dontEnums = [ + 'toString', + 'toLocaleString', + 'valueOf', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'constructor' + ], + dontEnumsLength = dontEnums.length; + + return function (obj) { + if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) { + throw new TypeError('Object.keys called on non-object'); + } + + var result = [], prop, i; + + for (prop in obj) { + if (hasOwnProperty.call(obj, prop)) { + result.push(prop); + } + } + + if (hasDontEnumBug) { + for (i = 0; i < dontEnumsLength; i++) { + if (hasOwnProperty.call(obj, dontEnums[i])) { + result.push(dontEnums[i]); + } + } + } + return result; + }; + }()); +} + +// http://ejohn.org/blog/objectgetprototypeof/ +if ( typeof Object.getPrototypeOf !== "function" ) { + if ( typeof "test".__proto__ === "object" ) { + Object.getPrototypeOf = function(object){ + return object.__proto__; + }; + } else { + Object.getPrototypeOf = function(object){ + // May break if the constructor has been tampered with + return object.constructor.prototype; + }; + } +} diff --git a/src/module.js b/src/module.js new file mode 100644 index 00000000..48ed1ec2 --- /dev/null +++ b/src/module.js @@ -0,0 +1,22 @@ +(function(exports) { + "use strict"; + + angular.module("firebase.utils", []); + angular.module("firebase.config", []); + angular.module("firebase.auth", ["firebase.utils"]); + angular.module("firebase.database", ["firebase.utils"]); + angular.module("firebase.storage", ["firebase.utils"]); + + // Define the `firebase` module under which all AngularFire + // services will live. + angular.module("firebase", [ + "firebase.utils", + "firebase.config", + "firebase.auth", + "firebase.database", + "firebase.storage" + ]) + //TODO: use $window + .value("Firebase", exports.firebase) + .value("firebase", exports.firebase); +})(window); diff --git a/src/storage/FirebaseStorage.js b/src/storage/FirebaseStorage.js new file mode 100644 index 00000000..766c2706 --- /dev/null +++ b/src/storage/FirebaseStorage.js @@ -0,0 +1,149 @@ +(function() { + "use strict"; + + /** + * Take an UploadTask and create an interface for the user to monitor the + * file's upload. The $progress, $error, and $complete methods are provided + * to work with the $digest cycle. + * + * @param task + * @param $firebaseUtils + * @returns A converted task, which contains methods for monitoring the + * upload progress. + */ + function _convertTask(task, $firebaseUtils) { + return { + $progress: function $progress(callback) { + task.on('state_changed', function () { + $firebaseUtils.compile(function () { + callback(_unwrapStorageSnapshot(task.snapshot)); + }); + }); + }, + $error: function $error(callback) { + task.on('state_changed', null, function (err) { + $firebaseUtils.compile(function () { + callback(err); + }); + }); + }, + $complete: function $complete(callback) { + task.on('state_changed', null, null, function () { + $firebaseUtils.compile(function () { + callback(_unwrapStorageSnapshot(task.snapshot)); + }); + }); + }, + $cancel: task.cancel.bind(task), + $resume: task.resume.bind(task), + $pause: task.pause.bind(task), + then: task.then.bind(task), + catch: task.catch.bind(task), + $snapshot: task.snapshot + }; + } + + /** + * Take a Storage snapshot and unwrap only the needed properties. + * + * @param snapshot + * @returns An object containing the unwrapped values. + */ + function _unwrapStorageSnapshot(storageSnapshot) { + return { + bytesTransferred: storageSnapshot.bytesTransferred, + downloadURL: storageSnapshot.downloadURL, + metadata: storageSnapshot.metadata, + ref: storageSnapshot.ref, + state: storageSnapshot.state, + task: storageSnapshot.task, + totalBytes: storageSnapshot.totalBytes + }; + } + + /** + * Determines if the value passed in is a Storage Reference. The + * put method is used for the check. + * + * @param value + * @returns A boolean that indicates if the value is a Storage Reference. + */ + function _isStorageRef(value) { + value = value || {}; + return typeof value.put === 'function'; + } + + /** + * Checks if the parameter is a Storage Reference, and throws an + * error if it is not. + * + * @param storageRef + */ + function _assertStorageRef(storageRef) { + if (!_isStorageRef(storageRef)) { + throw new Error('$firebaseStorage expects a Storage reference'); + } + } + + /** + * This constructor should probably never be called manually. It is setup + * for dependecy injection of the $firebaseUtils and $q service. + * + * @param {Object} $firebaseUtils + * @param {Object} $q + * @returns {Object} + * @constructor + */ + function FirebaseStorage($firebaseUtils, $q) { + + /** + * This inner constructor `Storage` allows for exporting of private methods + * like _assertStorageRef, _isStorageRef, _convertTask, and _unwrapStorageSnapshot. + */ + var Storage = function Storage(storageRef) { + _assertStorageRef(storageRef); + return { + $put: function $put(file, metadata) { + var task = storageRef.put(file, metadata); + return _convertTask(task, $firebaseUtils); + }, + $putString: function $putString(data, format, metadata) { + var task = storageRef.putString(data, format, metadata); + return _convertTask(task, $firebaseUtils); + }, + $getDownloadURL: function $getDownloadURL() { + return $q.when(storageRef.getDownloadURL()); + }, + $delete: function $delete() { + return $q.when(storageRef.delete()); + }, + $getMetadata: function $getMetadata() { + return $q.when(storageRef.getMetadata()); + }, + $updateMetadata: function $updateMetadata(object) { + return $q.when(storageRef.updateMetadata(object)); + }, + $toString: function $toString() { + return storageRef.toString(); + } + }; + }; + + Storage.utils = { + _unwrapStorageSnapshot: _unwrapStorageSnapshot, + _isStorageRef: _isStorageRef, + _assertStorageRef: _assertStorageRef + }; + + return Storage; + } + + /** + * Creates a wrapper for the firebase.storage() object. This factory allows + * you to upload files and monitor their progress and the callbacks are + * wrapped in the $digest cycle. + */ + angular.module('firebase.storage') + .factory('$firebaseStorage', ["$firebaseUtils", "$q", FirebaseStorage]); + +})(); diff --git a/src/storage/FirebaseStorageDirective.js b/src/storage/FirebaseStorageDirective.js new file mode 100644 index 00000000..58fb2a9d --- /dev/null +++ b/src/storage/FirebaseStorageDirective.js @@ -0,0 +1,31 @@ +/* istanbul ignore next */ +(function () { + "use strict"; + + function FirebaseStorageDirective($firebaseStorage, firebase) { + return { + restrict: 'A', + priority: 99, // run after the attributes are interpolated + scope: {}, + link: function (scope, element, attrs) { + // $observe is like $watch but it waits for interpolation + // any value passed as an attribute is converted to a string + // if null or undefined is passed, it is converted to an empty string + // Ex: + attrs.$observe('firebaseSrc', function (newFirebaseSrcVal) { + if (newFirebaseSrcVal !== '') { + var storageRef = firebase.storage().ref(newFirebaseSrcVal); + var storage = $firebaseStorage(storageRef); + storage.$getDownloadURL().then(function getDownloadURL(url) { + element[0].src = url; + }); + } + }); + } + }; + } + FirebaseStorageDirective.$inject = ['$firebaseStorage', 'firebase']; + + angular.module('firebase.storage') + .directive('firebaseSrc', FirebaseStorageDirective); +})(); diff --git a/src/utils/utils.js b/src/utils/utils.js new file mode 100644 index 00000000..d240c582 --- /dev/null +++ b/src/utils/utils.js @@ -0,0 +1,407 @@ +(function() { + 'use strict'; + + angular.module('firebase.utils') + .factory('$firebaseConfig', ["$firebaseArray", "$firebaseObject", "$injector", + function($firebaseArray, $firebaseObject, $injector) { + return function(configOpts) { + // make a copy we can modify + var opts = angular.extend({}, configOpts); + // look up factories if passed as string names + if( typeof opts.objectFactory === 'string' ) { + opts.objectFactory = $injector.get(opts.objectFactory); + } + if( typeof opts.arrayFactory === 'string' ) { + opts.arrayFactory = $injector.get(opts.arrayFactory); + } + // extend defaults and return + return angular.extend({ + arrayFactory: $firebaseArray, + objectFactory: $firebaseObject + }, opts); + }; + } + ]) + + .factory('$firebaseUtils', ["$q", "$timeout", "$rootScope", + function($q, $timeout, $rootScope) { + var utils = { + /** + * Returns a function which, each time it is invoked, will gather up the values until + * the next "tick" in the Angular compiler process. Then they are all run at the same + * time to avoid multiple cycles of the digest loop. Internally, this is done using $evalAsync() + * + * @param {Function} action + * @param {Object} [context] + * @returns {Function} + */ + batch: function(action, context) { + return function() { + var args = Array.prototype.slice.call(arguments, 0); + utils.compile(function() { + action.apply(context, args); + }); + }; + }, + + /** + * A rudimentary debounce method + * @param {function} fn the function to debounce + * @param {object} [ctx] the `this` context to set in fn + * @param {int} wait number of milliseconds to pause before sending out after each invocation + * @param {int} [maxWait] max milliseconds to wait before sending out, defaults to wait * 10 or 100 + */ + debounce: function(fn, ctx, wait, maxWait) { + var start, cancelTimer, args, runScheduledForNextTick; + if( typeof(ctx) === 'number' ) { + maxWait = wait; + wait = ctx; + ctx = null; + } + + if( typeof wait !== 'number' ) { + throw new Error('Must provide a valid integer for wait. Try 0 for a default'); + } + if( typeof(fn) !== 'function' ) { + throw new Error('Must provide a valid function to debounce'); + } + if( !maxWait ) { maxWait = wait*10 || 100; } + + // clears the current wait timer and creates a new one + // however, if maxWait is exceeded, calls runNow() on the next tick. + function resetTimer() { + if( cancelTimer ) { + cancelTimer(); + cancelTimer = null; + } + if( start && Date.now() - start > maxWait ) { + if(!runScheduledForNextTick){ + runScheduledForNextTick = true; + utils.compile(runNow); + } + } + else { + if( !start ) { start = Date.now(); } + cancelTimer = utils.wait(runNow, wait); + } + } + + // Clears the queue and invokes the debounced function with the most recent arguments + function runNow() { + cancelTimer = null; + start = null; + runScheduledForNextTick = false; + fn.apply(ctx, args); + } + + function debounced() { + args = Array.prototype.slice.call(arguments, 0); + resetTimer(); + } + debounced.running = function() { + return start > 0; + }; + + return debounced; + }, + + assertValidRef: function(ref, msg) { + if( !angular.isObject(ref) || + typeof(ref.ref) !== 'object' || + typeof(ref.ref.transaction) !== 'function' ) { + throw new Error(msg || 'Invalid Firebase reference'); + } + }, + + // http://stackoverflow.com/questions/7509831/alternative-for-the-deprecated-proto + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create + inherit: function(ChildClass, ParentClass, methods) { + var childMethods = ChildClass.prototype; + ChildClass.prototype = Object.create(ParentClass.prototype); + ChildClass.prototype.constructor = ChildClass; // restoring proper constructor for child class + angular.forEach(Object.keys(childMethods), function(k) { + ChildClass.prototype[k] = childMethods[k]; + }); + if( angular.isObject(methods) ) { + angular.extend(ChildClass.prototype, methods); + } + return ChildClass; + }, + + getPrototypeMethods: function(inst, iterator, context) { + var methods = {}; + var objProto = Object.getPrototypeOf({}); + var proto = angular.isFunction(inst) && angular.isObject(inst.prototype)? + inst.prototype : Object.getPrototypeOf(inst); + while(proto && proto !== objProto) { + for (var key in proto) { + // we only invoke each key once; if a super is overridden it's skipped here + if (proto.hasOwnProperty(key) && !methods.hasOwnProperty(key)) { + methods[key] = true; + iterator.call(context, proto[key], key, proto); + } + } + proto = Object.getPrototypeOf(proto); + } + }, + + getPublicMethods: function(inst, iterator, context) { + utils.getPrototypeMethods(inst, function(m, k) { + if( typeof(m) === 'function' && k.charAt(0) !== '_' ) { + iterator.call(context, m, k); + } + }); + }, + + makeNodeResolver:function(deferred){ + return function(err,result){ + if(err === null){ + if(arguments.length > 2){ + result = Array.prototype.slice.call(arguments,1); + } + + deferred.resolve(result); + } + else { + deferred.reject(err); + } + }; + }, + + wait: function(fn, wait) { + var to = $timeout(fn, wait||0); + return function() { + if( to ) { + $timeout.cancel(to); + to = null; + } + }; + }, + + compile: function(fn) { + return $rootScope.$evalAsync(fn||function() {}); + }, + + deepCopy: function(obj) { + if( !angular.isObject(obj) || angular.isDate(obj) ) { return obj; } + var newCopy = angular.isArray(obj) ? obj.slice() : angular.extend({}, obj); + for (var key in newCopy) { + if (newCopy.hasOwnProperty(key)) { + if (angular.isObject(newCopy[key])) { + newCopy[key] = utils.deepCopy(newCopy[key]); + } + } + } + return newCopy; + }, + + trimKeys: function(dest, source) { + utils.each(dest, function(v,k) { + if( !source.hasOwnProperty(k) ) { + delete dest[k]; + } + }); + }, + + scopeData: function(dataOrRec) { + var data = { + $id: dataOrRec.$id, + $priority: dataOrRec.$priority + }; + var hasPublicProp = false; + utils.each(dataOrRec, function(v,k) { + hasPublicProp = true; + data[k] = utils.deepCopy(v); + }); + if(!hasPublicProp && dataOrRec.hasOwnProperty('$value')){ + data.$value = dataOrRec.$value; + } + return data; + }, + + updateRec: function(rec, snap) { + var data = snap.val(); + var oldData = angular.extend({}, rec); + + // deal with primitives + if( !angular.isObject(data) ) { + rec.$value = data; + data = {}; + } + else { + delete rec.$value; + } + + // apply changes: remove old keys, insert new data, set priority + utils.trimKeys(rec, data); + angular.extend(rec, data); + rec.$priority = snap.getPriority(); + + return !angular.equals(oldData, rec) || + oldData.$value !== rec.$value || + oldData.$priority !== rec.$priority; + }, + + applyDefaults: function(rec, defaults) { + if( angular.isObject(defaults) ) { + angular.forEach(defaults, function(v,k) { + if( !rec.hasOwnProperty(k) ) { + rec[k] = v; + } + }); + } + return rec; + }, + + dataKeys: function(obj) { + var out = []; + utils.each(obj, function(v,k) { + out.push(k); + }); + return out; + }, + + each: function(obj, iterator, context) { + if(angular.isObject(obj)) { + for (var k in obj) { + if (obj.hasOwnProperty(k)) { + var c = k.charAt(0); + if( c !== '_' && c !== '$' && c !== '.' ) { + iterator.call(context, obj[k], k, obj); + } + } + } + } + else if(angular.isArray(obj)) { + for(var i = 0, len = obj.length; i < len; i++) { + iterator.call(context, obj[i], i, obj); + } + } + return obj; + }, + + /** + * A utility for converting records to JSON objects + * which we can save into Firebase. It asserts valid + * keys and strips off any items prefixed with $. + * + * If the rec passed into this method has a toJSON() + * method, that will be used in place of the custom + * functionality here. + * + * @param rec + * @returns {*} + */ + toJSON: function(rec) { + var dat; + if( !angular.isObject(rec) ) { + rec = {$value: rec}; + } + if (angular.isFunction(rec.toJSON)) { + dat = rec.toJSON(); + } + else { + dat = {}; + utils.each(rec, function (v, k) { + dat[k] = stripDollarPrefixedKeys(v); + }); + } + if( angular.isDefined(rec.$value) && Object.keys(dat).length === 0 && rec.$value !== null ) { + dat['.value'] = rec.$value; + } + if( angular.isDefined(rec.$priority) && Object.keys(dat).length > 0 && rec.$priority !== null ) { + dat['.priority'] = rec.$priority; + } + angular.forEach(dat, function(v,k) { + if (k.match(/[.$\[\]#\/]/) && k !== '.value' && k !== '.priority' ) { + throw new Error('Invalid key ' + k + ' (cannot contain .$[]#/)'); + } + else if( angular.isUndefined(v) ) { + throw new Error('Key '+k+' was undefined. Cannot pass undefined in JSON. Use null instead.'); + } + }); + return dat; + }, + + doSet: function(ref, data) { + var def = $q.defer(); + if( angular.isFunction(ref.set) || !angular.isObject(data) ) { + // this is not a query, just do a flat set + // Use try / catch to handle being passed data which is undefined or has invalid keys + try { + ref.set(data, utils.makeNodeResolver(def)); + } catch (err) { + def.reject(err); + } + } + else { + var dataCopy = angular.extend({}, data); + // this is a query, so we will replace all the elements + // of this query with the value provided, but not blow away + // the entire Firebase path + ref.once('value', function(snap) { + snap.forEach(function(ss) { + if( !dataCopy.hasOwnProperty(ss.key) ) { + dataCopy[ss.key] = null; + } + }); + ref.ref.update(dataCopy, utils.makeNodeResolver(def)); + }, function(err) { + def.reject(err); + }); + } + return def.promise; + }, + + doRemove: function(ref) { + var def = $q.defer(); + if( angular.isFunction(ref.remove) ) { + // ref is not a query, just do a flat remove + ref.remove(utils.makeNodeResolver(def)); + } + else { + // ref is a query so let's only remove the + // items in the query and not the entire path + ref.once('value', function(snap) { + var promises = []; + snap.forEach(function(ss) { + promises.push(ss.ref.remove()); + }); + utils.allPromises(promises) + .then(function() { + def.resolve(ref); + }, + function(err){ + def.reject(err); + } + ); + }, function(err) { + def.reject(err); + }); + } + return def.promise; + }, + + /** + * AngularFire version number. + */ + VERSION: '0.0.0', + + allPromises: $q.all.bind($q) + }; + + return utils; + } + ]); + + function stripDollarPrefixedKeys(data) { + if( !angular.isObject(data) || angular.isDate(data)) { return data; } + var out = angular.isArray(data)? [] : {}; + angular.forEach(data, function(v,k) { + if(typeof k !== 'string' || k.charAt(0) !== '$') { + out[k] = stripDollarPrefixedKeys(v); + } + }); + return out; + } +})(); diff --git a/tests/automatic_karma.conf.js b/tests/automatic_karma.conf.js index 7a6c47be..063ab357 100644 --- a/tests/automatic_karma.conf.js +++ b/tests/automatic_karma.conf.js @@ -4,18 +4,56 @@ module.exports = function(config) { config.set({ frameworks: ['jasmine'], + browsers: ['Chrome'], + reporters: ['spec', 'failed', 'coverage'], + autowatch: false, + singleRun: true, + + preprocessors: { + "../src/!(lib)/**/*.js": "coverage", + "./fixtures/**/*.json": "html2js" + }, + + coverageReporter: { + reporters: [ + { + // Nice HTML reports on developer machines, but not on Travis + type: process.env.TRAVIS ? "lcovonly" : "lcov", + dir: "coverage", + subdir: "." + }, + { + type: "text-summary" + } + ] + }, + files: [ - '../bower_components/angular/angular.js', - '../bower_components/angular-mocks/angular-mocks.js', - '../lib/omnibinder-protocol.js', - 'lib/lodash.js', - 'lib/MockFirebase.js', - '../angularfire.js', + '../node_modules/angular/angular.js', + '../node_modules/angular-mocks/angular-mocks.js', + '../node_modules/firebase/firebase.js', + 'lib/**/*.js', + '../src/module.js', + '../src/**/*.js', + 'mocks/**/*.js', + "fixtures/**/*.json", + 'initialize.js', 'unit/**/*.spec.js' - ], - - autoWatch: true, - //Recommend starting Chrome manually with experimental javascript flag enabled, and open localhost:9876. - browsers: ['Chrome'] + ] }); + + var configuration = { + customLaunchers: { + Chrome_travis_ci: { + base: 'Chrome', + flags: ['--no-sandbox'] + } + }, + }; + + if (process.env.TRAVIS) { + configuration.browsers = ['Chrome_travis_ci']; + } + + config.set(configuration); }; diff --git a/tests/browsers.json b/tests/browsers.json new file mode 100644 index 00000000..97c9fc56 --- /dev/null +++ b/tests/browsers.json @@ -0,0 +1,38 @@ +[ + { + "name": "chrome", + "version": "35", + "platform": "OS X 10.9" + }, + { + "name": "firefox", + "version": "30" + }, + { + "name": "safari", + "platform": "OS X 10.9", + "version": "7" + }, + { + "device": "iPhone", + "name": "iphone", + "platform": "OS X 10.9", + "version": "7.1" + }, + { + "device": "Android", + "name": "android", + "platform": "linux", + "version": "4.3" + }, + { + "name": "internet explorer", + "platform": "Windows 8.1", + "version": "11" + }, + { + "name": "internet explorer", + "platform": "Windows 7", + "version": "9" + } +] diff --git a/tests/e2e/test_chat.html b/tests/e2e/test_chat.html deleted file mode 100644 index 96d8235e..00000000 --- a/tests/e2e/test_chat.html +++ /dev/null @@ -1,69 +0,0 @@ - - - - - AngularFire Chat Test - - - - - -
    - -
    -

    -
    -
    - - {{message.from}}: - {{message.content}} - -
    - # Messages: {{messageCount.$value}} -
    -
    - - -
    - - - diff --git a/tests/e2e/test_chat.js b/tests/e2e/test_chat.js deleted file mode 100644 index aaf9e378..00000000 --- a/tests/e2e/test_chat.js +++ /dev/null @@ -1,174 +0,0 @@ - -casper.test.comment("Testing Chat example with $firebase"); - -casper.start("tests/e2e/test_chat.html", function() { - // Sanity test for the environment. - this.test.assertTitle("AngularFire Chat Test"); - this.test.assertEval(function() { - if (!Firebase) return false; - if (!_scope) return false; - return true; - }, "Firebase exists"); -}); - -casper.thenEvaluate(function() { - // Clean up Firebase to start fresh test. - var fbRef = new Firebase(_url); - var fbTnRef = new Firebase(_tnUrl); - fbRef.set(null, function(err) { - window.__flag = true; - }); - fbTnRef.set(null, function(err) {}); -}); - -casper.waitFor(function() { - return this.getGlobal("__flag") === true; -}); - -casper.then(function() { - this.test.assertEval(function() { - if (_scope.messages.$id != "chat") return false; - if (_scope.messageCount.$id != "chatMsgs") return false; - return true; - }, "Testing $id for object"); -}); - -casper.then(function() { - var _testName = "TestGuest"; - var _testMessage = "This is a test message"; - var _testCountMessage = 1; - - this.test.assertEval(function(params) { - _scope.username = params[0]; - _scope.message = params[1]; - _scope.addMessage(); - return _scope.message == ""; - }, "Adding a new message", [_testName, _testMessage]); - - this.waitForSelector(".messageBlock", function() { - this.test.assertEval(function(params) { - return testIfInDOM( - params[0], params[1], document.querySelector(".messageBlock") - ); - }, "Testing if message is in the DOM", [_testName, _testMessage]); - }); - - this.waitForSelector(".messageCountBlock", function() { - this.test.assertEval(function(params) { - return testMessageCount( - params[0], document.querySelector(".messageCountBlock") - ); - }, "Testing if message count is in the DOM", [_testCountMessage]); - }); - -}); - -casper.then(function() { - var _testName = "GuestTest"; - var _testMessage = "This is another test message"; - - this.evaluate(function(params) { - window.__flag = false; - var ref = new Firebase(_url); - ref.push({from: params[0], content: params[1]}, function(err) { - window.__flag = true; - }); - }, [_testName, _testMessage]); - - this.waitFor(function() { - return this.getGlobal("__flag") === true; - }, function() { - this.test.assertEval(function(params) { - var msgs = document.querySelectorAll(".messageBlock"); - if (msgs.length != 2) { - return false; - } - return testIfInDOM(params[0], params[1], msgs[1]); - }, "Testing if remote message is in the DOM", [_testName, _testMessage]); - }); -}); - -casper.then(function() { - var _testName = "GuestTest"; - var _testMessage = "Modified test message"; - - this.evaluate(function(params) { - window.__flag = false; - var idx = _scope.messages.$getIndex(); - var key = idx[idx.length-1]; - var obj = {}; obj[key] = {from: params[0], content: params[1]}; - _scope.messages.$update(obj).then(function() { - window.__flag = true; - }); - }, [_testName, _testMessage]); - - this.waitFor(function() { - return this.getGlobal("__flag") === true; - }, function() { - this.test.assertEval(function(params) { - var msgs = document.querySelectorAll(".messageBlock"); - if (msgs.length != 2) { - return false; - } - return testIfInDOM(params[0], params[1], msgs[1]); - }, "Testing if $update works", [_testName, _testMessage]); - }); -}); - -casper.then(function() { - this.test.assertEval(function() { - _scope.message = "Limit Test"; - _scope.addMessage(); - - var ref = new Firebase(_url); - ref.once("value", function(snapshot) { - window.__flag = snapshot.val(); - }); - - return _scope.message == ""; - }, "Adding limit message"); - - this.waitFor(function() { - return this.getGlobal("__flag") !== true; - }, function() { - this.test.assertEval(function() { - var msgs = document.querySelectorAll(".messageBlock"); - return msgs.length === 2; - }, "Testing if limits and queries work"); - }); -}); - -casper.then(function() { - this.test.assertEval(function() { - _scope.message = "Testing add promise"; - var promise = _scope.addMessage(); - if (typeof promise.then != "function") return false; - promise = _scope.messages.$set({foo: "bar"}); - if (typeof promise.then != "function") return false; - promise = _scope.messages.$save(); - if (typeof promise.then != "function") return false; - promise = _scope.messages.$remove(); - if (typeof promise.then != "function") return false; - return true; - }, "Testing if $add, $set, $save and $remove return a promise"); -}); - -casper.then(function() { - this.test.assertEval(function() { - if (_scope.messages.$getRef().toString() != _url) return false; - if (!(_scope.messages.$getRef() instanceof Firebase)) return false; - return true; - }, "Testing if $getRef returns valid Firebase reference"); -}); - -casper.then(function() { - this.test.assertEval(function() { - var empty = function() {}; - var obj = _scope.messages.$on('loaded', empty).$on('change', empty); - return JSON.stringify(_scope.messages) == JSON.stringify(obj); - }, "Testing chaining of $on method"); -}); - -casper.run(function() { - this.test.done(); -}); diff --git a/tests/e2e/test_priority.html b/tests/e2e/test_priority.html deleted file mode 100644 index e6aea29b..00000000 --- a/tests/e2e/test_priority.html +++ /dev/null @@ -1,57 +0,0 @@ - - - - - AngularFire Priority Test - - - - - -
    - -
    -

    -
    -
    - - {{message.from}}: - {{message.content}} - -
    -
    -
    - - -
    - - - diff --git a/tests/e2e/test_priority.js b/tests/e2e/test_priority.js deleted file mode 100644 index 7c816a9d..00000000 --- a/tests/e2e/test_priority.js +++ /dev/null @@ -1,162 +0,0 @@ -casper.test.comment("Testing priority changes with $firebase"); - -casper.start("tests/e2e/test_priority.html", function() { - // Sanity test for the environment. - this.test.assertTitle("AngularFire Priority Test"); - this.test.assertEval(function() { - if (!Firebase) return false; - if (!_scope) return false; - return true; - }, "Firebase exists"); -}); - -casper.thenEvaluate(function() { - // Clean up Firebase to start fresh test. - var fbRef = new Firebase(_url); - fbRef.set(null, function(err) { - window.__flag = true; - }); -}); - -casper.waitFor(function() { - return this.getGlobal("__flag") === true; -}); - -casper.then(function() { - - var _testName = "TestGuest"; - var _testMessage = "First message"; - - this.test.assertEval(function(params) { - _scope.username = params[0]; - _scope.message = params[1]; - _scope.addMessage(); - return _scope.message == ""; - }, "Adding a first message", [_testName, _testMessage]); - - this.waitForSelector(".messageBlock", function() { - this.test.assertEval(function(params) { - return testIfInDOMAtPos( - params[0], params[1], document.querySelectorAll(".messageBlock"), 0 - ); - }, "Testing if first message is in the DOM", [_testName, _testMessage]); - this.test.assertEvalEquals(function(params) { - return getMessagePriority(0); - }, 0, "Testing first message is at priority 0"); - }); -}); - -casper.then(function() { - - var _testName = "TestGuest"; - var _testMessage = "Second message"; - - this.test.assertEval(function(params) { - _scope.username = params[0]; - _scope.message = params[1]; - _scope.addMessage(); - return _scope.message == ""; - }, "Adding a second message", [_testName, _testMessage]); - - this.waitForSelector(".messageBlock", function() { - this.test.assertEval(function(params) { - return testIfInDOMAtPos( - params[0], params[1], document.querySelectorAll(".messageBlock"), 1 - ); - }, "Testing if second message is in the DOM", [_testName, _testMessage]); - this.test.assertEvalEquals(function(params) { - return getMessagePriority(1); - }, 1, "Testing if second message is at priority 1"); - }); -}); - -casper.then(function() { - - var _testName = "TestGuest"; - var _testMessage = "Third message"; - - this.test.assertEval(function(params) { - _scope.username = params[0]; - _scope.message = params[1]; - _scope.addMessage(); - return _scope.message == ""; - }, "Adding a third message", [_testName, _testMessage]); - - this.waitForSelector(".messageBlock", function() { - this.test.assertEval(function(params) { - return testIfInDOMAtPos( - params[0], params[1], document.querySelectorAll(".messageBlock"), 2 - ); - }, "Testing if third message is in the DOM", [_testName, _testMessage]); - this.test.assertEvalEquals(function(params) { - return getMessagePriority(2); - }, 2, "Testing if third message is at priority 2"); - }); -}); - -casper.then(function() { - this.evaluate(function() { - _scope.messages[_scope.messages.$getIndex()[1]].$priority = 0; - _scope.messages[_scope.messages.$getIndex()[0]].$priority = 1; - _scope.messages.$save(); - - window.__flag = null; - var ref = new Firebase(_url); - ref.once("value", function(snapshot) { - window.__flag = snapshot.val(); - }); - }, "Moving second to first"); - - this.waitFor(function() { - return this.getGlobal("__flag") != null; - }, function() { - this.test.assertEval(function(params) { - var nodes = document.querySelectorAll(".messageBlock"); - return testIfInDOMAtPos(params[0], params[1], nodes, 1); - }, "Testing if first message moved to second position", ["TestGuest", "First message"]); - - this.test.assertEval(function(params) { - var nodes = document.querySelectorAll(".messageBlock"); - return testIfInDOMAtPos(params[0], params[1], nodes, 0); - }, "Testing if second message moved to first position", ["TestGuest", "Second message"]); - }); -}); - -casper.then(function() { - var checkPrio = function (idx) { return getMessagePriority(idx); } - this.test.assertEvalEquals(checkPrio, 0, "Testing array's first element has priority 0", 0); - this.test.assertEvalEquals(checkPrio, 1, "Testing array's second element has priority 1", 1); - this.test.assertEvalEquals(checkPrio, 2, "Testing array's third element has priority 2", 2); -}); - -casper.then(function() { - this.evaluate(function() { - _scope.messages[_scope.messages.$getIndex()[1]].$priority = 0; - _scope.messages[_scope.messages.$getIndex()[0]].$priority = 1; - _scope.messages.$save(); - - window.__flag = null; - var ref = new Firebase(_url); - ref.once("value", function(snapshot) { - window.__flag = snapshot.val(); - }); - }, "Moving first message back to first position"); - - this.waitFor(function() { - return this.getGlobal("__flag") != null; - }, function() { - this.test.assertEval(function(params) { - var nodes = document.querySelectorAll(".messageBlock"); - return testIfInDOMAtPos(params[0], params[1], nodes, 0); - }, "Testing if first message moved to first position", ["TestGuest", "First message"]); - - this.test.assertEval(function(params) { - var nodes = document.querySelectorAll(".messageBlock"); - return testIfInDOMAtPos(params[0], params[1], nodes, 1); - }, "Testing if second message moved to second position", ["TestGuest", "Second message"]); - }); -}); - -casper.run(function() { - this.test.done(); -}); diff --git a/tests/e2e/test_todo.html b/tests/e2e/test_todo.html deleted file mode 100644 index 9ea3d5ef..00000000 --- a/tests/e2e/test_todo.html +++ /dev/null @@ -1,76 +0,0 @@ - - - - - AngularFire TODO Test - - - - - - -

    -
    - -
    -
    -
    -
    - - - -
    -
    -
    - - - diff --git a/tests/e2e/test_todo.js b/tests/e2e/test_todo.js deleted file mode 100644 index 6aeb0399..00000000 --- a/tests/e2e/test_todo.js +++ /dev/null @@ -1,96 +0,0 @@ - -casper.test.comment("Testing TODO example with $firebase.$bind"); - -casper.start("tests/e2e/test_todo.html", function() { - // Sanity test for environment. - this.test.assertTitle("AngularFire TODO Test"); - this.test.assertEval(function() { - if (!Firebase) return false; - return true; - }, "Firebase exists"); -}); - -casper.waitFor(function() { - return this.evaluate(function() { - // Wait for initial data to load to check if data was merged. - return _scope != null; - }); -}); - -casper.then(function() { - var _testTodo = "Eat some Chocolate"; - - this.test.assertEval(function(title) { - _scope.newTodo = title; - _scope.addTodo(); - _scope.$digest(); - return _scope.newTodo == ""; - }, "Adding a new TODO", _testTodo); - - // By adding this new TODO, we now should have two in the list. - this.waitForSelector(".todoView", function() { - this.test.assertEval(function(todo) { - return testIfInDOM(todo, document.querySelectorAll(".todoView")[1]); - }, "Testing if TODO is in the DOM", {title: _testTodo, completed: false}); - }); -}); - -casper.then(function() { - this.evaluate(function() { - _scope.todos[Object.keys(_scope.todos)[0]].completed = true; - _scope.$digest(); - }); - this.waitFor(function() { - return this.evaluate(function() { - return document.querySelector(".todoView").childNodes[1].checked === true; - }); - }); -}); - -casper.then(function() { - var _testTodo = "Run for 10 miles"; - - this.test.assertEval(function(title) { - _scope.newTodo = title; - _scope.addTodo(); - _scope.$digest(); - return _scope.newTodo == ""; - }, "Adding another TODO", _testTodo); - - this.waitFor(function() { - return this.evaluate(function() { - return document.querySelectorAll(".todoView").length == 3; - }); - }); -}); - -casper.then(function() { - var _testTodo = "This TODO should never show up"; - - this.test.assertEval(function(title) { - _scope.$destroy(); - _scope.newTodo = title; - _scope.addTodo(); - _scope.$digest(); - return Object.keys(_scope.todos).length == 4; - }, "Testing if destroying $scope causes disassociate", _testTodo); - - this.evaluate(function() { - window.__flag = false; - var ref = new Firebase(_url); - ref.once("value", function(snapshot) { - if (Object.keys(snapshot.val()).length == 3) { - window.__flag = true; - } - }); - }); - - this.waitFor(function() { - return this.getGlobal("__flag") === true; - }); -}); - -casper.run(function() { - this.test.done(); -}); - diff --git a/tests/firebase-mock.js b/tests/firebase-mock.js deleted file mode 100644 index 3cd84df8..00000000 --- a/tests/firebase-mock.js +++ /dev/null @@ -1,20 +0,0 @@ -var Firebase = function (url) { - this._url = url; - this.on = function (event, callback) { - this._on = this._on || []; - this._on.push(event); - - this._events = this._events || {}; - this._events[event] = callback; - }; - - this.startAt = function (num) { - this._startAt = num; - return this; - }; - - this.limit = function (num) { - this._limit = num; - return this; - } -} \ No newline at end of file diff --git a/tests/fixtures/data.json b/tests/fixtures/data.json new file mode 100644 index 00000000..b9eb02f7 --- /dev/null +++ b/tests/fixtures/data.json @@ -0,0 +1,82 @@ +{ + "data": { + "a": { + "aString": "alpha", + "aNumber": 1, + "aBoolean": false + }, + "b": { + "aString": "bravo", + "aNumber": 2, + "aBoolean": true + }, + "c": { + "aString": "charlie", + "aNumber": 3, + "aBoolean": true + }, + "d": { + "aString": "delta", + "aNumber": 4, + "aBoolean": true + }, + "e": { + "aString": "echo", + "aNumber": 5 + } + }, + "index": { + "b": true, + "c": 1, + "e": false, + "z": true + }, + "ordered": { + "null_a": { + "aNumber": 0, + "aLetter": "a" + }, + "null_b": { + "aNumber": 0, + "aLetter": "b" + }, + "null_c": { + "aNumber": 0, + "aLetter": "c" + }, + "num_1_a": { + ".priority": 1, + "aNumber": 1 + }, + "num_1_b": { + ".priority": 1, + "aNumber": 1 + }, + "num_2": { + ".priority": 2, + "aNumber": 2 + }, + "num_3": { + ".priority": 3, + "aNumber": 3 + }, + "char_a_1": { + ".priority": "a", + "aNumber": 1, + "aLetter": "a" + }, + "char_a_2": { + ".priority": "a", + "aNumber": 2, + "aLetter": "a" + }, + "char_b": { + ".priority": "b", + "aLetter": "b" + }, + "char_c": { + ".priority": "c", + "aLetter": "c" + } + } +} diff --git a/tests/initialize-node.js b/tests/initialize-node.js new file mode 100644 index 00000000..9cb859a8 --- /dev/null +++ b/tests/initialize-node.js @@ -0,0 +1,6 @@ +var path = require('path'); +var firebase = require('firebase'); + +firebase.initializeApp({ + databaseURL: 'https://oss-test.firebaseio.com' +}); diff --git a/tests/initialize.js b/tests/initialize.js new file mode 100644 index 00000000..5dd9c4b7 --- /dev/null +++ b/tests/initialize.js @@ -0,0 +1,16 @@ +if (window.jamsine) { + jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000; +} + +try { + // TODO: stop hard-coding this + var config = { + apiKey: "AIzaSyCcB9Ozrh1M-WzrwrSMB6t5y1flL8yXYmY", + authDomain: "oss-test.firebaseapp.com", + databaseURL: "https://oss-test.firebaseio.com", + storageBucket: "oss-test.appspot.com" + }; + firebase.initializeApp(config); +} catch (err) { + console.log('Failed to initialize the Firebase SDK [web]:', err); +} diff --git a/tests/lib/MockFirebase.js b/tests/lib/MockFirebase.js deleted file mode 100644 index 0553a842..00000000 --- a/tests/lib/MockFirebase.js +++ /dev/null @@ -1,488 +0,0 @@ -(function() { - - // some hoop jumping for node require() vs browser usage - var exports = typeof exports != 'undefined' ? exports : this; - var _, sinon; - exports.Firebase = MockFirebase; //todo use MockFirebase.stub() instead of forcing overwrite - if( typeof module !== "undefined" && module.exports && typeof(require) === 'function' ) { - _ = require('lodash'); - sinon = require('sinon'); - } - else { - _ = exports._; - sinon = exports.sinon; - } - - /** - * A mock that simulates Firebase operations for use in unit tests. - * - * ## Setup - * - * // in windows - * MockFirebase.stub(window, 'Firebase'); // replace window.Firebase - * - * // in node.js - * var Firebase = require('../lib/MockFirebase'); - * - * ## Usage Examples - * - * var fb = new Firebase('Mock://foo/bar'); - * fb.on('value', function(snap) { - * console.log(snap.val()); - * }); - * - * // do something async or synchronously... - * - * // trigger callbacks and event listeners - * fb.flush(); - * - * // spy on methods - * expect(fb.on.called).toBe(true); - * - * ## Trigger events automagically instead of calling flush() - * - * var fb = new MockFirebase('Mock://hello/world'); - * fb.autoFlush(1000); // triggers events after 1 second - * fb.autoFlush(); // triggers events immediately - * - * ## Simulating Errors - * - * var fb = new MockFirebase('Mock://fails/a/lot'); - * fb.failNext('set', new Error('PERMISSION_DENIED'); // create an error to be invoked on the next set() op - * fb.set({foo: bar}, function(err) { - * // err.message === 'PERMISSION_DENIED' - * }); - * fb.flush(); - * - * @param {string} [currentPath] use a relative path here or a url, all .child() calls will append to this - * @param {Object} [data] specify the data in this Firebase instance (defaults to MockFirebase.DEFAULT_DATA) - * @param {MockFirebase} [parent] for internal use - * @param {string} [name] for internal use - * @constructor - */ - function MockFirebase(currentPath, data, parent, name) { - // these are set whenever startAt(), limit() or endAt() get invoked - this._queryProps = { limit: undefined, startAt: undefined, endAt: undefined }; - - // represents the fake url - this.currentPath = currentPath || 'Mock://'; - - // do not modify this directly, use set() and flush(true) - this.data = _.cloneDeep(arguments.length > 1? data||null : MockFirebase.DEFAULT_DATA); - - // see failNext() - this.errs = {}; - - // null for the root path - this.myName = parent? name : extractName(currentPath); - - // see autoFlush() - this.flushDelay = false; - - // stores the listeners for various event types - this._events = { value: [], child_added: [], child_removed: [], child_changed: [], child_moved: [] }; - - // allows changes to be propagated between child/parent instances - this.parent = parent||null; - this.children = []; - parent && parent.children.push(this); - - // stores the operations that have been queued until a flush() event is triggered - this.ops = []; - - // turn all our public methods into spies so they can be monitored for calls and return values - // see jasmine spies: https://github.com/pivotal/jasmine/wiki/Spies - // the Firebase constructor can be spied on using spyOn(window, 'Firebase') from within the test unit - if( typeof spyOn === 'function' ) { - for(var key in this) { - if( !key.match(/^_/) && typeof(this[key]) === 'function' ) { - spyOn(this, key).andCallThrough(); - } - } - } - } - - MockFirebase.prototype = { - /***************************************************** - * Test Unit tools (not part of Firebase API) - *****************************************************/ - - /** - * Invoke all the operations that have been queued thus far. If a numeric delay is specified, this - * occurs asynchronously. Otherwise, it is a synchronous event. - * - * This allows Firebase to be used in synchronous tests without waiting for async callbacks. It also - * provides a rudimentary mechanism for simulating locally cached data (events are triggered - * synchronously when you do on('value') or on('child_added') against locally cached data) - * - * If you call this multiple times with different delay values, you could invoke the events out - * of order; make sure that is your intention. - * - * @param {boolean|int} [delay] - * @returns {MockFirebase} - */ - flush: function(delay) { - var self = this, list = self.ops; - self.ops = []; - if( _.isNumber(delay) ) { - setTimeout(process, delay); - } - else { - process(); - } - function process() { - list.forEach(function(parts) { - parts[0].apply(self, parts.slice(1)); - }); - self.children.forEach(function(c) { - c.flush(); - }); - } - return self; - }, - - /** - * Automatically trigger a flush event after each operation. If a numeric delay is specified, this is an - * asynchronous event. If value is set to true, it is synchronous (flush is triggered immediately). Setting - * this to false disabled autoFlush - * - * @param {int|boolean} [delay] - */ - autoFlush: function(delay){ - this.flushDelay = _.isUndefined(delay)? true : delay; - this.children.forEach(function(c) { - c.autoFlush(delay); - }); - delay !== false && this.flush(delay); - return this; - }, - - /** - * Simulate a failure by specifying that the next invocation of methodName should - * fail with the provided error. - * - * @param {String} methodName currently only supports `set` and `transaction` - * @param {String|Error} error - */ - failNext: function(methodName, error) { - this.errs[methodName] = error; - }, - - /** - * Returns a copy of the current data - * @returns {*} - */ - getData: function() { - return _.cloneDeep(this.data); - }, - - - /***************************************************** - * Firebase API methods - *****************************************************/ - - toString: function() { - return this.currentPath; - }, - - child: function(childPath) { - var ref = this, parts = childPath.split('/'); - parts.forEach(function(p) { - var v = _.isObject(ref.data) && _.has(ref.data, p)? ref.data[p] : null; - ref = new MockFirebase(mergePaths(ref.currentPath, p), v, ref, p); - }); - ref.flushDelay = this.flushDelay; - return ref; - }, - - set: function(data, callback) { - var self = this; - var err = this._nextErr('set'); - data = _.cloneDeep(data); - this._defer(function() { - if( err === null ) { - self._dataChanged(data); - } - callback && callback(err); - }); - return this; - }, - - name: function() { - return this.myName; - }, - - ref: function() { - return this; - }, - - once: function(event, callback) { - function fn(snap) { - this.off(event, fn); - callback(snap); - } - this.on(event, fn); - }, - - on: function(event, callback) { //todo cancelCallback? - this._events[event].push(callback); - var data = this.getData(), self = this; - if( event === 'value' ) { - this._defer(function() { - callback(makeSnap(self, data)); - }); - } - else if( event === 'child_added' ) { - this._defer(function() { - var prev = null; - _.each(data, function(v, k) { - callback(makeSnap(self.child(k), v), prev); - prev = k; - }); - }); - } - }, - - off: function(event, callback) { - if( !event ) { - for (var key in this._events) - if( this._events.hasOwnProperty(key) ) - this.off(key); - } - else if( callback ) { - this._events[event] = _.without(this._events[event], callback); - } - else { - this._events[event] = []; - } - }, - - transaction: function(valueFn, finishedFn, applyLocally) { - var valueSpy = sinon.spy(valueFn); - var finishedSpy = sinon.spy(finishedFn); - this._defer(function() { - var err = this._nextErr('transaction'); - // unlike most defer methods, this will use the value as it exists at the time - // the transaction is actually invoked, which is the eventual consistent value - // it would have in reality - var res = valueSpy(this.getData()); - var newData = _.isUndefined(res) || err? this.getData() : res; - finishedSpy(err, err === null && !_.isUndefined(res), makeSnap(this, newData)); - this._dataChanged(newData); - }); - return [valueSpy, finishedSpy, applyLocally]; - }, - - /** - * If token is valid and parses, returns the contents of token as exected. If not, the error is returned. - * Does not change behavior in any way (since we don't really auth anywhere) - * - * @param {String} token - * @param {Function} [callback] - */ - auth: function(token, callback) { - callback && this._defer(callback); - }, - - /** - * Just a stub at this point. - * @param {int} limit - */ - limit: function(limit) { - this._queryProps.limit = limit; - //todo - }, - - startAt: function(priority, recordId) { - this._queryProps.startAt = [priority, recordId]; - //todo - }, - - endAt: function(priority, recordId) { - this._queryProps.endAt = [priority, recordId]; - //todo - }, - - /***************************************************** - * Private/internal methods - *****************************************************/ - - _childChanged: function(ref, data) { - if( !_.isObject(this.data) ) { this.data = {}; } - this.data[ref.name()] = _.cloneDeep(data); - this._trigger('child_changed', data, ref.name()); - this._trigger('value', this.data); - this.parent && this.parent._childChanged(this, this.data); - }, - - _dataChanged: function(data) { - if( !_.isEqual(data, this.data) ) { - this.data = _.cloneDeep(data); - this._trigger('value', this.data); - if( this.parent && _.isObject(this.parent.data) ) { - this.parent._childChanged(this, this.data); - } - if(this.children) { - _.each(this.children, function(child) { - child._dataChanged(extractChildData(child.name(), data)); - }); - } - } - }, - - _defer: function(fn) { - this.ops.push(Array.prototype.slice.call(arguments, 0)); - if( this.flushDelay !== false ) { this.flush(this.flushDelay); } - }, - - _trigger: function(event, data, key) { - var snap = makeSnap(this, data), self = this; - _.each(this._events[event], function(fn) { - if(_.contains(['child_added', 'child_moved'], event)) { - fn(snap, getPrevChild(self.data, key)); - } - else { - //todo allow scope by changing fn to an array? for use with on() and once() which accept scope? - fn(snap); - } - }); - }, - - _nextErr: function(type) { - var err = this.errs[type]; - delete this.errs[type]; - return err||null; - } - }; - - function ref(path, autoSyncDelay) { - var ref = new MockFirebase(); - ref.flushDelay = _.isUndefined(autoSyncDelay)? true : autoSyncDelay; - if( path ) { ref = ref.child(path); } - return ref; - } - - function mergePaths(base, add) { - return base.replace(/\/$/, '')+'/'+add.replace(/^\//, ''); - } - - function makeSnap(ref, data) { - data = _.cloneDeep(data); - return { - val: function() { return data; }, - ref: function() { return ref; }, - name: function() { return ref.name() }, - getPriority: function() { return null; }, //todo - forEach: function(cb, scope) { - _.each(data, function(v, k, list) { - var res = cb.call(scope, v, k, list); - return !(res === true); - }); - } - } - } - - function extractChildData(childName, data) { - if( !_.isObject(data) || !_.hasKey(data, childName) ) { - return null; - } - else { - return data[childName]; - } - } - - function extractName(path) { - return ((path || '').match(/\/([^.$\[\]#\/]+)$/)||[null, null])[1]; - } - - function getPrevChild(data, key) { - var keys = _.keys(data), i = _.indexOf(keys, key); - if( keys.length < 2 || i < 1 ) { return null; } - else { - return keys[i]; - } - } - - // a polyfill for window.atob to allow JWT token parsing - // credits: https://github.com/davidchambers/Base64.js - ;(function (object) { - var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; - - function InvalidCharacterError(message) { - this.message = message; - } - InvalidCharacterError.prototype = new Error; - InvalidCharacterError.prototype.name = 'InvalidCharacterError'; - - // encoder - // [https://gist.github.com/999166] by [https://github.com/nignag] - object.btoa || ( - object.btoa = function (input) { - for ( - // initialize result and counter - var block, charCode, idx = 0, map = chars, output = ''; - // if the next input index does not exist: - // change the mapping table to "=" - // check if d has no fractional digits - input.charAt(idx | 0) || (map = '=', idx % 1); - // "8 - idx % 1 * 8" generates the sequence 2, 4, 6, 8 - output += map.charAt(63 & block >> 8 - idx % 1 * 8) - ) { - charCode = input.charCodeAt(idx += 3/4); - if (charCode > 0xFF) { - throw new InvalidCharacterError("'btoa' failed: The string to be encoded contains characters outside of the Latin1 range."); - } - block = block << 8 | charCode; - } - return output; - }); - - // decoder - // [https://gist.github.com/1020396] by [https://github.com/atk] - object.atob || ( - object.atob = function (input) { - input = input.replace(/=+$/, '') - if (input.length % 4 == 1) { - throw new InvalidCharacterError("'atob' failed: The string to be decoded is not correctly encoded."); - } - for ( - // initialize result and counters - var bc = 0, bs, buffer, idx = 0, output = ''; - // get next character - buffer = input.charAt(idx++); - // character found in table? initialize bit storage and add its ascii value; - ~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer, - // and if not first of each 4 characters, - // convert the first 8 bits to one ascii character - bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0 - ) { - // try to find character in table (0-63, not found => -1) - buffer = chars.indexOf(buffer); - } - return output; - }); - - }(exports)); - - MockFirebase._ = _; // expose for tests - - MockFirebase.stub = function(obj, key) { - obj[key] = MockFirebase; - }; - - MockFirebase.ref = ref; - MockFirebase.DEFAULT_DATA = { - 'data': { - 'a': { - hello: 'world', - aNumber: 1, - aBoolean: false - }, - 'b': { - foo: 'bar', - aNumber: 2, - aBoolean: true - } - } - }; -})(); \ No newline at end of file diff --git a/tests/lib/jasmineMatchers.js b/tests/lib/jasmineMatchers.js new file mode 100644 index 00000000..c8c896bd --- /dev/null +++ b/tests/lib/jasmineMatchers.js @@ -0,0 +1,215 @@ +/** + * Adds matchers to Jasmine so they can be called from test units + * These are handy for debugging because they produce better error + * messages than "Expected false to be true" + */ +beforeEach(function() { + 'use strict'; + + function extendedTypeOf(x) { + var actual; + if( isArray(x) ) { + actual = 'array'; + } + else if( x === null ) { + actual = 'null'; + } + else { + actual = typeof x; + } + return actual.toLowerCase(); + } + + jasmine.addMatchers({ + toBeAFirebaseRef: function() { + return { + compare: function(actual) { + var type = extendedTypeOf(actual); + var pass = isFirebaseRef(actual); + var notText = pass? ' not' : ''; + var msg = 'Expected ' + type + notText + ' to be a Firebase ref'; + return {pass: pass, message: msg}; + } + } + }, + + toBeASnapshot: function() { + return { + compare: function(actual) { + var type = extendedTypeOf(actual); + var pass = + type === 'object' && + typeof actual.val === 'function' && + typeof actual.ref === 'function' && + typeof actual.name === 'function'; + var notText = pass? ' not' : ''; + var msg = 'Expected ' + type + notText + ' to be a Firebase snapshot'; + return {pass: pass, message: msg}; + } + } + }, + + toBeAPromise: function() { + return { + compare: function(obj) { + var objType = extendedTypeOf(obj); + var pass = + objType === 'object' && + typeof obj.then === 'function'; + var notText = pass? ' not' : ''; + var msg = 'Expected ' + objType + notText + ' to be a promise'; + return {pass: pass, message: msg}; + } + } + }, + + // inspired by: https://gist.github.com/prantlf/8631877 + toBeInstanceOf: function() { + return { + compare: function (actual, expected, name) { + var result = { + pass: actual instanceof expected + }; + var notText = result.pass? ' not' : ''; + result.message = 'Expected ' + actual + notText + ' to be an instance of ' + (name||expected.constructor.name); + return result; + } + }; + }, + + /** + * Checks type of a value. This method will also accept null and array + * as valid types. It will not treat null or arrays as objects. Multiple + * types can be passed into this method and it will be true if any matches + * are found. + */ + toBeA: function() { + return { + compare: function() { + var args = Array.prototype.slice.apply(arguments); + return compare.apply(null, ['a'].concat(args)); + } + }; + }, + + toBeAn: function() { + return { + compare: function(actual) { + var args = Array.prototype.slice.apply(arguments); + return compare.apply(null, ['an'].concat(args)); + } + } + }, + + toHaveKey: function() { + return { + compare: function(actual, key) { + var pass = + actual && + typeof(actual) === 'object' && + actual.hasOwnProperty(key); + var notText = pass? ' not' : ''; + return { + pass: pass, + message: 'Expected key ' + key + notText + ' to exist in ' + extendedTypeOf(actual) + } + } + } + }, + + toHaveLength: function() { + return { + compare: function(actual, len) { + var actLen = isArray(actual)? actual.length : 'not an array'; + var pass = actLen === len; + var notText = pass? ' not' : ''; + return { + pass: pass, + message: 'Expected array ' + notText + ' to have length ' + len + ', but it was ' + actLen + } + } + } + }, + + toBeEmpty: function() { + return { + compare: function(actual) { + var pass, contents; + if( isObject(actual) ) { + actual = Object.keys(actual); + } + if( isArray(actual) ) { + pass = actual.length === 0; + contents = 'had ' + actual.length + ' items'; + } + else { + pass = false; + contents = 'was not an array or object'; + } + var notText = pass? ' not' : ''; + return { + pass: pass, + message: 'Expected collection ' + notText + ' to be empty, but it ' + contents + } + } + } + }, + + toHaveCallCount: function() { + return { + compare: function(spy, expCount) { + var pass, not, count; + count = spy.calls.count(); + pass = count === expCount; + not = pass? '" not' : '"'; + return { + pass: pass, + message: 'Expected spy "' + spy.and.identity() + not + ' to have been called ' + expCount + ' times' + + (pass? '' : ', but it was called ' + count) + } + } + } + } + }); + + function isObject(x) { + return x && typeof(x) === 'object' && !isArray(x); + } + + function isArray(x) { + if (typeof Array.isArray !== 'function') { + return x && typeof x === 'object' && Object.prototype.toString.call(x) === '[object Array]'; + } + return Array.isArray(x); + } + + function isFirebaseRef(obj) { + return extendedTypeOf(obj) === 'object' && + typeof obj.ref === 'object' && + typeof obj.set === 'function' && + typeof obj.on === 'function' && + typeof obj.once === 'function' && + typeof obj.transaction === 'function'; + } + + // inspired by: https://gist.github.com/prantlf/8631877 + function compare(article, actual) { + var validTypes = Array.prototype.slice.call(arguments, 2); + if( !validTypes.length ) { + throw new Error('Must pass at least one valid type into toBeA() and toBeAn() functions'); + } + var verbiage = validTypes.length === 1 ? 'to be ' + article : 'to be one of'; + var actualType = extendedTypeOf(actual); + + var found = false; + for (var i = 0, len = validTypes.length; i < len; i++) { + found = validTypes[i].toLowerCase() === actualType; + if( found ) { break; } + } + + var notText = found? ' not' : ''; + var message = 'Expected ' + actualType + notText + ' ' + verbiage + ' ' + validTypes; + + return { pass: found, message: message }; + } +}); diff --git a/tests/lib/lodash.js b/tests/lib/lodash.js deleted file mode 100644 index 5b379036..00000000 --- a/tests/lib/lodash.js +++ /dev/null @@ -1,7179 +0,0 @@ -/** - * @license - * Lo-Dash 2.4.1 - * Copyright 2012-2013 The Dojo Foundation - * Based on Underscore.js 1.5.2 - * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors - * Available under MIT license - */ -;(function() { - - /** Used as a safe reference for `undefined` in pre ES5 environments */ - var undefined; - - /** Used to pool arrays and objects used internally */ - var arrayPool = [], - objectPool = []; - - /** Used to generate unique IDs */ - var idCounter = 0; - - /** Used internally to indicate various things */ - var indicatorObject = {}; - - /** Used to prefix keys to avoid issues with `__proto__` and properties on `Object.prototype` */ - var keyPrefix = +new Date + ''; - - /** Used as the size when optimizations are enabled for large arrays */ - var largeArraySize = 75; - - /** Used as the max size of the `arrayPool` and `objectPool` */ - var maxPoolSize = 40; - - /** Used to detect and test whitespace */ - var whitespace = ( - // whitespace - ' \t\x0B\f\xA0\ufeff' + - - // line terminators - '\n\r\u2028\u2029' + - - // unicode category "Zs" space separators - '\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000' - ); - - /** Used to match empty string literals in compiled template source */ - var reEmptyStringLeading = /\b__p \+= '';/g, - reEmptyStringMiddle = /\b(__p \+=) '' \+/g, - reEmptyStringTrailing = /(__e\(.*?\)|\b__t\)) \+\n'';/g; - - /** - * Used to match ES6 template delimiters - * http://people.mozilla.org/~jorendorff/es6-draft.html#sec-literals-string-literals - */ - var reEsTemplate = /\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g; - - /** Used to match regexp flags from their coerced string values */ - var reFlags = /\w*$/; - - /** Used to detected named functions */ - var reFuncName = /^\s*function[ \n\r\t]+\w/; - - /** Used to match "interpolate" template delimiters */ - var reInterpolate = /<%=([\s\S]+?)%>/g; - - /** Used to match leading whitespace and zeros to be removed */ - var reLeadingSpacesAndZeros = RegExp('^[' + whitespace + ']*0+(?=.$)'); - - /** Used to ensure capturing order of template delimiters */ - var reNoMatch = /($^)/; - - /** Used to detect functions containing a `this` reference */ - var reThis = /\bthis\b/; - - /** Used to match unescaped characters in compiled string literals */ - var reUnescapedString = /['\n\r\t\u2028\u2029\\]/g; - - /** Used to assign default `context` object properties */ - var contextProps = [ - 'Array', 'Boolean', 'Date', 'Error', 'Function', 'Math', 'Number', 'Object', - 'RegExp', 'String', '_', 'attachEvent', 'clearTimeout', 'isFinite', 'isNaN', - 'parseInt', 'setTimeout' - ]; - - /** Used to fix the JScript [[DontEnum]] bug */ - var shadowedProps = [ - 'constructor', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', - 'toLocaleString', 'toString', 'valueOf' - ]; - - /** Used to make template sourceURLs easier to identify */ - var templateCounter = 0; - - /** `Object#toString` result shortcuts */ - var argsClass = '[object Arguments]', - arrayClass = '[object Array]', - boolClass = '[object Boolean]', - dateClass = '[object Date]', - errorClass = '[object Error]', - funcClass = '[object Function]', - numberClass = '[object Number]', - objectClass = '[object Object]', - regexpClass = '[object RegExp]', - stringClass = '[object String]'; - - /** Used to identify object classifications that `_.clone` supports */ - var cloneableClasses = {}; - cloneableClasses[funcClass] = false; - cloneableClasses[argsClass] = cloneableClasses[arrayClass] = - cloneableClasses[boolClass] = cloneableClasses[dateClass] = - cloneableClasses[numberClass] = cloneableClasses[objectClass] = - cloneableClasses[regexpClass] = cloneableClasses[stringClass] = true; - - /** Used as an internal `_.debounce` options object */ - var debounceOptions = { - 'leading': false, - 'maxWait': 0, - 'trailing': false - }; - - /** Used as the property descriptor for `__bindData__` */ - var descriptor = { - 'configurable': false, - 'enumerable': false, - 'value': null, - 'writable': false - }; - - /** Used as the data object for `iteratorTemplate` */ - var iteratorData = { - 'args': '', - 'array': null, - 'bottom': '', - 'firstArg': '', - 'init': '', - 'keys': null, - 'loop': '', - 'shadowedProps': null, - 'support': null, - 'top': '', - 'useHas': false - }; - - /** Used to determine if values are of the language type Object */ - var objectTypes = { - 'boolean': false, - 'function': true, - 'object': true, - 'number': false, - 'string': false, - 'undefined': false - }; - - /** Used to escape characters for inclusion in compiled string literals */ - var stringEscapes = { - '\\': '\\', - "'": "'", - '\n': 'n', - '\r': 'r', - '\t': 't', - '\u2028': 'u2028', - '\u2029': 'u2029' - }; - - /** Used as a reference to the global object */ - var root = (objectTypes[typeof window] && window) || this; - - /** Detect free variable `exports` */ - var freeExports = objectTypes[typeof exports] && exports && !exports.nodeType && exports; - - /** Detect free variable `module` */ - var freeModule = objectTypes[typeof module] && module && !module.nodeType && module; - - /** Detect the popular CommonJS extension `module.exports` */ - var moduleExports = freeModule && freeModule.exports === freeExports && freeExports; - - /** Detect free variable `global` from Node.js or Browserified code and use it as `root` */ - var freeGlobal = objectTypes[typeof global] && global; - if (freeGlobal && (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal)) { - root = freeGlobal; - } - - /*--------------------------------------------------------------------------*/ - - /** - * The base implementation of `_.indexOf` without support for binary searches - * or `fromIndex` constraints. - * - * @private - * @param {Array} array The array to search. - * @param {*} value The value to search for. - * @param {number} [fromIndex=0] The index to search from. - * @returns {number} Returns the index of the matched value or `-1`. - */ - function baseIndexOf(array, value, fromIndex) { - var index = (fromIndex || 0) - 1, - length = array ? array.length : 0; - - while (++index < length) { - if (array[index] === value) { - return index; - } - } - return -1; - } - - /** - * An implementation of `_.contains` for cache objects that mimics the return - * signature of `_.indexOf` by returning `0` if the value is found, else `-1`. - * - * @private - * @param {Object} cache The cache object to inspect. - * @param {*} value The value to search for. - * @returns {number} Returns `0` if `value` is found, else `-1`. - */ - function cacheIndexOf(cache, value) { - var type = typeof value; - cache = cache.cache; - - if (type == 'boolean' || value == null) { - return cache[value] ? 0 : -1; - } - if (type != 'number' && type != 'string') { - type = 'object'; - } - var key = type == 'number' ? value : keyPrefix + value; - cache = (cache = cache[type]) && cache[key]; - - return type == 'object' - ? (cache && baseIndexOf(cache, value) > -1 ? 0 : -1) - : (cache ? 0 : -1); - } - - /** - * Adds a given value to the corresponding cache object. - * - * @private - * @param {*} value The value to add to the cache. - */ - function cachePush(value) { - var cache = this.cache, - type = typeof value; - - if (type == 'boolean' || value == null) { - cache[value] = true; - } else { - if (type != 'number' && type != 'string') { - type = 'object'; - } - var key = type == 'number' ? value : keyPrefix + value, - typeCache = cache[type] || (cache[type] = {}); - - if (type == 'object') { - (typeCache[key] || (typeCache[key] = [])).push(value); - } else { - typeCache[key] = true; - } - } - } - - /** - * Used by `_.max` and `_.min` as the default callback when a given - * collection is a string value. - * - * @private - * @param {string} value The character to inspect. - * @returns {number} Returns the code unit of given character. - */ - function charAtCallback(value) { - return value.charCodeAt(0); - } - - /** - * Used by `sortBy` to compare transformed `collection` elements, stable sorting - * them in ascending order. - * - * @private - * @param {Object} a The object to compare to `b`. - * @param {Object} b The object to compare to `a`. - * @returns {number} Returns the sort order indicator of `1` or `-1`. - */ - function compareAscending(a, b) { - var ac = a.criteria, - bc = b.criteria, - index = -1, - length = ac.length; - - while (++index < length) { - var value = ac[index], - other = bc[index]; - - if (value !== other) { - if (value > other || typeof value == 'undefined') { - return 1; - } - if (value < other || typeof other == 'undefined') { - return -1; - } - } - } - // Fixes an `Array#sort` bug in the JS engine embedded in Adobe applications - // that causes it, under certain circumstances, to return the same value for - // `a` and `b`. See https://github.com/jashkenas/underscore/pull/1247 - // - // This also ensures a stable sort in V8 and other engines. - // See http://code.google.com/p/v8/issues/detail?id=90 - return a.index - b.index; - } - - /** - * Creates a cache object to optimize linear searches of large arrays. - * - * @private - * @param {Array} [array=[]] The array to search. - * @returns {null|Object} Returns the cache object or `null` if caching should not be used. - */ - function createCache(array) { - var index = -1, - length = array.length, - first = array[0], - mid = array[(length / 2) | 0], - last = array[length - 1]; - - if (first && typeof first == 'object' && - mid && typeof mid == 'object' && last && typeof last == 'object') { - return false; - } - var cache = getObject(); - cache['false'] = cache['null'] = cache['true'] = cache['undefined'] = false; - - var result = getObject(); - result.array = array; - result.cache = cache; - result.push = cachePush; - - while (++index < length) { - result.push(array[index]); - } - return result; - } - - /** - * Used by `template` to escape characters for inclusion in compiled - * string literals. - * - * @private - * @param {string} match The matched character to escape. - * @returns {string} Returns the escaped character. - */ - function escapeStringChar(match) { - return '\\' + stringEscapes[match]; - } - - /** - * Gets an array from the array pool or creates a new one if the pool is empty. - * - * @private - * @returns {Array} The array from the pool. - */ - function getArray() { - return arrayPool.pop() || []; - } - - /** - * Gets an object from the object pool or creates a new one if the pool is empty. - * - * @private - * @returns {Object} The object from the pool. - */ - function getObject() { - return objectPool.pop() || { - 'array': null, - 'cache': null, - 'criteria': null, - 'false': false, - 'index': 0, - 'null': false, - 'number': null, - 'object': null, - 'push': null, - 'string': null, - 'true': false, - 'undefined': false, - 'value': null - }; - } - - /** - * Checks if `value` is a DOM node in IE < 9. - * - * @private - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if the `value` is a DOM node, else `false`. - */ - function isNode(value) { - // IE < 9 presents DOM nodes as `Object` objects except they have `toString` - // methods that are `typeof` "string" and still can coerce nodes to strings - return typeof value.toString != 'function' && typeof (value + '') == 'string'; - } - - /** - * Releases the given array back to the array pool. - * - * @private - * @param {Array} [array] The array to release. - */ - function releaseArray(array) { - array.length = 0; - if (arrayPool.length < maxPoolSize) { - arrayPool.push(array); - } - } - - /** - * Releases the given object back to the object pool. - * - * @private - * @param {Object} [object] The object to release. - */ - function releaseObject(object) { - var cache = object.cache; - if (cache) { - releaseObject(cache); - } - object.array = object.cache = object.criteria = object.object = object.number = object.string = object.value = null; - if (objectPool.length < maxPoolSize) { - objectPool.push(object); - } - } - - /** - * Slices the `collection` from the `start` index up to, but not including, - * the `end` index. - * - * Note: This function is used instead of `Array#slice` to support node lists - * in IE < 9 and to ensure dense arrays are returned. - * - * @private - * @param {Array|Object|string} collection The collection to slice. - * @param {number} start The start index. - * @param {number} end The end index. - * @returns {Array} Returns the new array. - */ - function slice(array, start, end) { - start || (start = 0); - if (typeof end == 'undefined') { - end = array ? array.length : 0; - } - var index = -1, - length = end - start || 0, - result = Array(length < 0 ? 0 : length); - - while (++index < length) { - result[index] = array[start + index]; - } - return result; - } - - /*--------------------------------------------------------------------------*/ - - /** - * Create a new `lodash` function using the given context object. - * - * @static - * @memberOf _ - * @category Utilities - * @param {Object} [context=root] The context object. - * @returns {Function} Returns the `lodash` function. - */ - function runInContext(context) { - // Avoid issues with some ES3 environments that attempt to use values, named - // after built-in constructors like `Object`, for the creation of literals. - // ES5 clears this up by stating that literals must use built-in constructors. - // See http://es5.github.io/#x11.1.5. - context = context ? _.defaults(root.Object(), context, _.pick(root, contextProps)) : root; - - /** Native constructor references */ - var Array = context.Array, - Boolean = context.Boolean, - Date = context.Date, - Error = context.Error, - Function = context.Function, - Math = context.Math, - Number = context.Number, - Object = context.Object, - RegExp = context.RegExp, - String = context.String, - TypeError = context.TypeError; - - /** - * Used for `Array` method references. - * - * Normally `Array.prototype` would suffice, however, using an array literal - * avoids issues in Narwhal. - */ - var arrayRef = []; - - /** Used for native method references */ - var errorProto = Error.prototype, - objectProto = Object.prototype, - stringProto = String.prototype; - - /** Used to restore the original `_` reference in `noConflict` */ - var oldDash = context._; - - /** Used to resolve the internal [[Class]] of values */ - var toString = objectProto.toString; - - /** Used to detect if a method is native */ - var reNative = RegExp('^' + - String(toString) - .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - .replace(/toString| for [^\]]+/g, '.*?') + '$' - ); - - /** Native method shortcuts */ - var ceil = Math.ceil, - clearTimeout = context.clearTimeout, - floor = Math.floor, - fnToString = Function.prototype.toString, - getPrototypeOf = isNative(getPrototypeOf = Object.getPrototypeOf) && getPrototypeOf, - hasOwnProperty = objectProto.hasOwnProperty, - push = arrayRef.push, - propertyIsEnumerable = objectProto.propertyIsEnumerable, - setTimeout = context.setTimeout, - splice = arrayRef.splice, - unshift = arrayRef.unshift; - - /** Used to set meta data on functions */ - var defineProperty = (function() { - // IE 8 only accepts DOM elements - try { - var o = {}, - func = isNative(func = Object.defineProperty) && func, - result = func(o, o, o) && func; - } catch(e) { } - return result; - }()); - - /* Native method shortcuts for methods with the same name as other `lodash` methods */ - var nativeCreate = isNative(nativeCreate = Object.create) && nativeCreate, - nativeIsArray = isNative(nativeIsArray = Array.isArray) && nativeIsArray, - nativeIsFinite = context.isFinite, - nativeIsNaN = context.isNaN, - nativeKeys = isNative(nativeKeys = Object.keys) && nativeKeys, - nativeMax = Math.max, - nativeMin = Math.min, - nativeParseInt = context.parseInt, - nativeRandom = Math.random; - - /** Used to lookup a built-in constructor by [[Class]] */ - var ctorByClass = {}; - ctorByClass[arrayClass] = Array; - ctorByClass[boolClass] = Boolean; - ctorByClass[dateClass] = Date; - ctorByClass[funcClass] = Function; - ctorByClass[objectClass] = Object; - ctorByClass[numberClass] = Number; - ctorByClass[regexpClass] = RegExp; - ctorByClass[stringClass] = String; - - /** Used to avoid iterating non-enumerable properties in IE < 9 */ - var nonEnumProps = {}; - nonEnumProps[arrayClass] = nonEnumProps[dateClass] = nonEnumProps[numberClass] = { 'constructor': true, 'toLocaleString': true, 'toString': true, 'valueOf': true }; - nonEnumProps[boolClass] = nonEnumProps[stringClass] = { 'constructor': true, 'toString': true, 'valueOf': true }; - nonEnumProps[errorClass] = nonEnumProps[funcClass] = nonEnumProps[regexpClass] = { 'constructor': true, 'toString': true }; - nonEnumProps[objectClass] = { 'constructor': true }; - - (function() { - var length = shadowedProps.length; - while (length--) { - var key = shadowedProps[length]; - for (var className in nonEnumProps) { - if (hasOwnProperty.call(nonEnumProps, className) && !hasOwnProperty.call(nonEnumProps[className], key)) { - nonEnumProps[className][key] = false; - } - } - } - }()); - - /*--------------------------------------------------------------------------*/ - - /** - * Creates a `lodash` object which wraps the given value to enable intuitive - * method chaining. - * - * In addition to Lo-Dash methods, wrappers also have the following `Array` methods: - * `concat`, `join`, `pop`, `push`, `reverse`, `shift`, `slice`, `sort`, `splice`, - * and `unshift` - * - * Chaining is supported in custom builds as long as the `value` method is - * implicitly or explicitly included in the build. - * - * The chainable wrapper functions are: - * `after`, `assign`, `bind`, `bindAll`, `bindKey`, `chain`, `compact`, - * `compose`, `concat`, `countBy`, `create`, `createCallback`, `curry`, - * `debounce`, `defaults`, `defer`, `delay`, `difference`, `filter`, `flatten`, - * `forEach`, `forEachRight`, `forIn`, `forInRight`, `forOwn`, `forOwnRight`, - * `functions`, `groupBy`, `indexBy`, `initial`, `intersection`, `invert`, - * `invoke`, `keys`, `map`, `max`, `memoize`, `merge`, `min`, `object`, `omit`, - * `once`, `pairs`, `partial`, `partialRight`, `pick`, `pluck`, `pull`, `push`, - * `range`, `reject`, `remove`, `rest`, `reverse`, `shuffle`, `slice`, `sort`, - * `sortBy`, `splice`, `tap`, `throttle`, `times`, `toArray`, `transform`, - * `union`, `uniq`, `unshift`, `unzip`, `values`, `where`, `without`, `wrap`, - * and `zip` - * - * The non-chainable wrapper functions are: - * `clone`, `cloneDeep`, `contains`, `escape`, `every`, `find`, `findIndex`, - * `findKey`, `findLast`, `findLastIndex`, `findLastKey`, `has`, `identity`, - * `indexOf`, `isArguments`, `isArray`, `isBoolean`, `isDate`, `isElement`, - * `isEmpty`, `isEqual`, `isFinite`, `isFunction`, `isNaN`, `isNull`, `isNumber`, - * `isObject`, `isPlainObject`, `isRegExp`, `isString`, `isUndefined`, `join`, - * `lastIndexOf`, `mixin`, `noConflict`, `parseInt`, `pop`, `random`, `reduce`, - * `reduceRight`, `result`, `shift`, `size`, `some`, `sortedIndex`, `runInContext`, - * `template`, `unescape`, `uniqueId`, and `value` - * - * The wrapper functions `first` and `last` return wrapped values when `n` is - * provided, otherwise they return unwrapped values. - * - * Explicit chaining can be enabled by using the `_.chain` method. - * - * @name _ - * @constructor - * @category Chaining - * @param {*} value The value to wrap in a `lodash` instance. - * @returns {Object} Returns a `lodash` instance. - * @example - * - * var wrapped = _([1, 2, 3]); - * - * // returns an unwrapped value - * wrapped.reduce(function(sum, num) { - * return sum + num; - * }); - * // => 6 - * - * // returns a wrapped value - * var squares = wrapped.map(function(num) { - * return num * num; - * }); - * - * _.isArray(squares); - * // => false - * - * _.isArray(squares.value()); - * // => true - */ - function lodash(value) { - // don't wrap if already wrapped, even if wrapped by a different `lodash` constructor - return (value && typeof value == 'object' && !isArray(value) && hasOwnProperty.call(value, '__wrapped__')) - ? value - : new lodashWrapper(value); - } - - /** - * A fast path for creating `lodash` wrapper objects. - * - * @private - * @param {*} value The value to wrap in a `lodash` instance. - * @param {boolean} chainAll A flag to enable chaining for all methods - * @returns {Object} Returns a `lodash` instance. - */ - function lodashWrapper(value, chainAll) { - this.__chain__ = !!chainAll; - this.__wrapped__ = value; - } - // ensure `new lodashWrapper` is an instance of `lodash` - lodashWrapper.prototype = lodash.prototype; - - /** - * An object used to flag environments features. - * - * @static - * @memberOf _ - * @type Object - */ - var support = lodash.support = {}; - - (function() { - var ctor = function() { this.x = 1; }, - object = { '0': 1, 'length': 1 }, - props = []; - - ctor.prototype = { 'valueOf': 1, 'y': 1 }; - for (var key in new ctor) { props.push(key); } - for (key in arguments) { } - - /** - * Detect if an `arguments` object's [[Class]] is resolvable (all but Firefox < 4, IE < 9). - * - * @memberOf _.support - * @type boolean - */ - support.argsClass = toString.call(arguments) == argsClass; - - /** - * Detect if `arguments` objects are `Object` objects (all but Narwhal and Opera < 10.5). - * - * @memberOf _.support - * @type boolean - */ - support.argsObject = arguments.constructor == Object && !(arguments instanceof Array); - - /** - * Detect if `name` or `message` properties of `Error.prototype` are - * enumerable by default. (IE < 9, Safari < 5.1) - * - * @memberOf _.support - * @type boolean - */ - support.enumErrorProps = propertyIsEnumerable.call(errorProto, 'message') || propertyIsEnumerable.call(errorProto, 'name'); - - /** - * Detect if `prototype` properties are enumerable by default. - * - * Firefox < 3.6, Opera > 9.50 - Opera < 11.60, and Safari < 5.1 - * (if the prototype or a property on the prototype has been set) - * incorrectly sets a function's `prototype` property [[Enumerable]] - * value to `true`. - * - * @memberOf _.support - * @type boolean - */ - support.enumPrototypes = propertyIsEnumerable.call(ctor, 'prototype'); - - /** - * Detect if functions can be decompiled by `Function#toString` - * (all but PS3 and older Opera mobile browsers & avoided in Windows 8 apps). - * - * @memberOf _.support - * @type boolean - */ - support.funcDecomp = !isNative(context.WinRTError) && reThis.test(runInContext); - - /** - * Detect if `Function#name` is supported (all but IE). - * - * @memberOf _.support - * @type boolean - */ - support.funcNames = typeof Function.name == 'string'; - - /** - * Detect if `arguments` object indexes are non-enumerable - * (Firefox < 4, IE < 9, PhantomJS, Safari < 5.1). - * - * @memberOf _.support - * @type boolean - */ - support.nonEnumArgs = key != 0; - - /** - * Detect if properties shadowing those on `Object.prototype` are non-enumerable. - * - * In IE < 9 an objects own properties, shadowing non-enumerable ones, are - * made non-enumerable as well (a.k.a the JScript [[DontEnum]] bug). - * - * @memberOf _.support - * @type boolean - */ - support.nonEnumShadows = !/valueOf/.test(props); - - /** - * Detect if own properties are iterated after inherited properties (all but IE < 9). - * - * @memberOf _.support - * @type boolean - */ - support.ownLast = props[0] != 'x'; - - /** - * Detect if `Array#shift` and `Array#splice` augment array-like objects correctly. - * - * Firefox < 10, IE compatibility mode, and IE < 9 have buggy Array `shift()` - * and `splice()` functions that fail to remove the last element, `value[0]`, - * of array-like objects even though the `length` property is set to `0`. - * The `shift()` method is buggy in IE 8 compatibility mode, while `splice()` - * is buggy regardless of mode in IE < 9 and buggy in compatibility mode in IE 9. - * - * @memberOf _.support - * @type boolean - */ - support.spliceObjects = (arrayRef.splice.call(object, 0, 1), !object[0]); - - /** - * Detect lack of support for accessing string characters by index. - * - * IE < 8 can't access characters by index and IE 8 can only access - * characters by index on string literals. - * - * @memberOf _.support - * @type boolean - */ - support.unindexedChars = ('x'[0] + Object('x')[0]) != 'xx'; - - /** - * Detect if a DOM node's [[Class]] is resolvable (all but IE < 9) - * and that the JS engine errors when attempting to coerce an object to - * a string without a `toString` function. - * - * @memberOf _.support - * @type boolean - */ - try { - support.nodeClass = !(toString.call(document) == objectClass && !({ 'toString': 0 } + '')); - } catch(e) { - support.nodeClass = true; - } - }(1)); - - /** - * By default, the template delimiters used by Lo-Dash are similar to those in - * embedded Ruby (ERB). Change the following template settings to use alternative - * delimiters. - * - * @static - * @memberOf _ - * @type Object - */ - lodash.templateSettings = { - - /** - * Used to detect `data` property values to be HTML-escaped. - * - * @memberOf _.templateSettings - * @type RegExp - */ - 'escape': /<%-([\s\S]+?)%>/g, - - /** - * Used to detect code to be evaluated. - * - * @memberOf _.templateSettings - * @type RegExp - */ - 'evaluate': /<%([\s\S]+?)%>/g, - - /** - * Used to detect `data` property values to inject. - * - * @memberOf _.templateSettings - * @type RegExp - */ - 'interpolate': reInterpolate, - - /** - * Used to reference the data object in the template text. - * - * @memberOf _.templateSettings - * @type string - */ - 'variable': '', - - /** - * Used to import variables into the compiled template. - * - * @memberOf _.templateSettings - * @type Object - */ - 'imports': { - - /** - * A reference to the `lodash` function. - * - * @memberOf _.templateSettings.imports - * @type Function - */ - '_': lodash - } - }; - - /*--------------------------------------------------------------------------*/ - - /** - * The template used to create iterator functions. - * - * @private - * @param {Object} data The data object used to populate the text. - * @returns {string} Returns the interpolated text. - */ - var iteratorTemplate = template( - // the `iterable` may be reassigned by the `top` snippet - 'var index, iterable = <%= firstArg %>, ' + - // assign the `result` variable an initial value - 'result = <%= init %>;\n' + - // exit early if the first argument is falsey - 'if (!iterable) return result;\n' + - // add code before the iteration branches - '<%= top %>;' + - - // array-like iteration: - '<% if (array) { %>\n' + - 'var length = iterable.length; index = -1;\n' + - 'if (<%= array %>) {' + - - // add support for accessing string characters by index if needed - ' <% if (support.unindexedChars) { %>\n' + - ' if (isString(iterable)) {\n' + - " iterable = iterable.split('')\n" + - ' }' + - ' <% } %>\n' + - - // iterate over the array-like value - ' while (++index < length) {\n' + - ' <%= loop %>;\n' + - ' }\n' + - '}\n' + - 'else {' + - - // object iteration: - // add support for iterating over `arguments` objects if needed - ' <% } else if (support.nonEnumArgs) { %>\n' + - ' var length = iterable.length; index = -1;\n' + - ' if (length && isArguments(iterable)) {\n' + - ' while (++index < length) {\n' + - " index += '';\n" + - ' <%= loop %>;\n' + - ' }\n' + - ' } else {' + - ' <% } %>' + - - // avoid iterating over `prototype` properties in older Firefox, Opera, and Safari - ' <% if (support.enumPrototypes) { %>\n' + - " var skipProto = typeof iterable == 'function';\n" + - ' <% } %>' + - - // avoid iterating over `Error.prototype` properties in older IE and Safari - ' <% if (support.enumErrorProps) { %>\n' + - ' var skipErrorProps = iterable === errorProto || iterable instanceof Error;\n' + - ' <% } %>' + - - // define conditions used in the loop - ' <%' + - ' var conditions = [];' + - ' if (support.enumPrototypes) { conditions.push(\'!(skipProto && index == "prototype")\'); }' + - ' if (support.enumErrorProps) { conditions.push(\'!(skipErrorProps && (index == "message" || index == "name"))\'); }' + - ' %>' + - - // iterate own properties using `Object.keys` - ' <% if (useHas && keys) { %>\n' + - ' var ownIndex = -1,\n' + - ' ownProps = objectTypes[typeof iterable] && keys(iterable),\n' + - ' length = ownProps ? ownProps.length : 0;\n\n' + - ' while (++ownIndex < length) {\n' + - ' index = ownProps[ownIndex];\n<%' + - " if (conditions.length) { %> if (<%= conditions.join(' && ') %>) {\n <% } %>" + - ' <%= loop %>;' + - ' <% if (conditions.length) { %>\n }<% } %>\n' + - ' }' + - - // else using a for-in loop - ' <% } else { %>\n' + - ' for (index in iterable) {\n<%' + - ' if (useHas) { conditions.push("hasOwnProperty.call(iterable, index)"); }' + - " if (conditions.length) { %> if (<%= conditions.join(' && ') %>) {\n <% } %>" + - ' <%= loop %>;' + - ' <% if (conditions.length) { %>\n }<% } %>\n' + - ' }' + - - // Because IE < 9 can't set the `[[Enumerable]]` attribute of an - // existing property and the `constructor` property of a prototype - // defaults to non-enumerable, Lo-Dash skips the `constructor` - // property when it infers it's iterating over a `prototype` object. - ' <% if (support.nonEnumShadows) { %>\n\n' + - ' if (iterable !== objectProto) {\n' + - " var ctor = iterable.constructor,\n" + - ' isProto = iterable === (ctor && ctor.prototype),\n' + - ' className = iterable === stringProto ? stringClass : iterable === errorProto ? errorClass : toString.call(iterable),\n' + - ' nonEnum = nonEnumProps[className];\n' + - ' <% for (k = 0; k < 7; k++) { %>\n' + - " index = '<%= shadowedProps[k] %>';\n" + - ' if ((!(isProto && nonEnum[index]) && hasOwnProperty.call(iterable, index))<%' + - ' if (!useHas) { %> || (!nonEnum[index] && iterable[index] !== objectProto[index])<% }' + - ' %>) {\n' + - ' <%= loop %>;\n' + - ' }' + - ' <% } %>\n' + - ' }' + - ' <% } %>' + - ' <% } %>' + - ' <% if (array || support.nonEnumArgs) { %>\n}<% } %>\n' + - - // add code to the bottom of the iteration function - '<%= bottom %>;\n' + - // finally, return the `result` - 'return result' - ); - - /*--------------------------------------------------------------------------*/ - - /** - * The base implementation of `_.bind` that creates the bound function and - * sets its meta data. - * - * @private - * @param {Array} bindData The bind data array. - * @returns {Function} Returns the new bound function. - */ - function baseBind(bindData) { - var func = bindData[0], - partialArgs = bindData[2], - thisArg = bindData[4]; - - function bound() { - // `Function#bind` spec - // http://es5.github.io/#x15.3.4.5 - if (partialArgs) { - // avoid `arguments` object deoptimizations by using `slice` instead - // of `Array.prototype.slice.call` and not assigning `arguments` to a - // variable as a ternary expression - var args = slice(partialArgs); - push.apply(args, arguments); - } - // mimic the constructor's `return` behavior - // http://es5.github.io/#x13.2.2 - if (this instanceof bound) { - // ensure `new bound` is an instance of `func` - var thisBinding = baseCreate(func.prototype), - result = func.apply(thisBinding, args || arguments); - return isObject(result) ? result : thisBinding; - } - return func.apply(thisArg, args || arguments); - } - setBindData(bound, bindData); - return bound; - } - - /** - * The base implementation of `_.clone` without argument juggling or support - * for `thisArg` binding. - * - * @private - * @param {*} value The value to clone. - * @param {boolean} [isDeep=false] Specify a deep clone. - * @param {Function} [callback] The function to customize cloning values. - * @param {Array} [stackA=[]] Tracks traversed source objects. - * @param {Array} [stackB=[]] Associates clones with source counterparts. - * @returns {*} Returns the cloned value. - */ - function baseClone(value, isDeep, callback, stackA, stackB) { - if (callback) { - var result = callback(value); - if (typeof result != 'undefined') { - return result; - } - } - // inspect [[Class]] - var isObj = isObject(value); - if (isObj) { - var className = toString.call(value); - if (!cloneableClasses[className] || (!support.nodeClass && isNode(value))) { - return value; - } - var ctor = ctorByClass[className]; - switch (className) { - case boolClass: - case dateClass: - return new ctor(+value); - - case numberClass: - case stringClass: - return new ctor(value); - - case regexpClass: - result = ctor(value.source, reFlags.exec(value)); - result.lastIndex = value.lastIndex; - return result; - } - } else { - return value; - } - var isArr = isArray(value); - if (isDeep) { - // check for circular references and return corresponding clone - var initedStack = !stackA; - stackA || (stackA = getArray()); - stackB || (stackB = getArray()); - - var length = stackA.length; - while (length--) { - if (stackA[length] == value) { - return stackB[length]; - } - } - result = isArr ? ctor(value.length) : {}; - } - else { - result = isArr ? slice(value) : assign({}, value); - } - // add array properties assigned by `RegExp#exec` - if (isArr) { - if (hasOwnProperty.call(value, 'index')) { - result.index = value.index; - } - if (hasOwnProperty.call(value, 'input')) { - result.input = value.input; - } - } - // exit for shallow clone - if (!isDeep) { - return result; - } - // add the source value to the stack of traversed objects - // and associate it with its clone - stackA.push(value); - stackB.push(result); - - // recursively populate clone (susceptible to call stack limits) - (isArr ? baseEach : forOwn)(value, function(objValue, key) { - result[key] = baseClone(objValue, isDeep, callback, stackA, stackB); - }); - - if (initedStack) { - releaseArray(stackA); - releaseArray(stackB); - } - return result; - } - - /** - * The base implementation of `_.create` without support for assigning - * properties to the created object. - * - * @private - * @param {Object} prototype The object to inherit from. - * @returns {Object} Returns the new object. - */ - function baseCreate(prototype, properties) { - return isObject(prototype) ? nativeCreate(prototype) : {}; - } - // fallback for browsers without `Object.create` - if (!nativeCreate) { - baseCreate = (function() { - function Object() {} - return function(prototype) { - if (isObject(prototype)) { - Object.prototype = prototype; - var result = new Object; - Object.prototype = null; - } - return result || context.Object(); - }; - }()); - } - - /** - * The base implementation of `_.createCallback` without support for creating - * "_.pluck" or "_.where" style callbacks. - * - * @private - * @param {*} [func=identity] The value to convert to a callback. - * @param {*} [thisArg] The `this` binding of the created callback. - * @param {number} [argCount] The number of arguments the callback accepts. - * @returns {Function} Returns a callback function. - */ - function baseCreateCallback(func, thisArg, argCount) { - if (typeof func != 'function') { - return identity; - } - // exit early for no `thisArg` or already bound by `Function#bind` - if (typeof thisArg == 'undefined' || !('prototype' in func)) { - return func; - } - var bindData = func.__bindData__; - if (typeof bindData == 'undefined') { - if (support.funcNames) { - bindData = !func.name; - } - bindData = bindData || !support.funcDecomp; - if (!bindData) { - var source = fnToString.call(func); - if (!support.funcNames) { - bindData = !reFuncName.test(source); - } - if (!bindData) { - // checks if `func` references the `this` keyword and stores the result - bindData = reThis.test(source); - setBindData(func, bindData); - } - } - } - // exit early if there are no `this` references or `func` is bound - if (bindData === false || (bindData !== true && bindData[1] & 1)) { - return func; - } - switch (argCount) { - case 1: return function(value) { - return func.call(thisArg, value); - }; - case 2: return function(a, b) { - return func.call(thisArg, a, b); - }; - case 3: return function(value, index, collection) { - return func.call(thisArg, value, index, collection); - }; - case 4: return function(accumulator, value, index, collection) { - return func.call(thisArg, accumulator, value, index, collection); - }; - } - return bind(func, thisArg); - } - - /** - * The base implementation of `createWrapper` that creates the wrapper and - * sets its meta data. - * - * @private - * @param {Array} bindData The bind data array. - * @returns {Function} Returns the new function. - */ - function baseCreateWrapper(bindData) { - var func = bindData[0], - bitmask = bindData[1], - partialArgs = bindData[2], - partialRightArgs = bindData[3], - thisArg = bindData[4], - arity = bindData[5]; - - var isBind = bitmask & 1, - isBindKey = bitmask & 2, - isCurry = bitmask & 4, - isCurryBound = bitmask & 8, - key = func; - - function bound() { - var thisBinding = isBind ? thisArg : this; - if (partialArgs) { - var args = slice(partialArgs); - push.apply(args, arguments); - } - if (partialRightArgs || isCurry) { - args || (args = slice(arguments)); - if (partialRightArgs) { - push.apply(args, partialRightArgs); - } - if (isCurry && args.length < arity) { - bitmask |= 16 & ~32; - return baseCreateWrapper([func, (isCurryBound ? bitmask : bitmask & ~3), args, null, thisArg, arity]); - } - } - args || (args = arguments); - if (isBindKey) { - func = thisBinding[key]; - } - if (this instanceof bound) { - thisBinding = baseCreate(func.prototype); - var result = func.apply(thisBinding, args); - return isObject(result) ? result : thisBinding; - } - return func.apply(thisBinding, args); - } - setBindData(bound, bindData); - return bound; - } - - /** - * The base implementation of `_.difference` that accepts a single array - * of values to exclude. - * - * @private - * @param {Array} array The array to process. - * @param {Array} [values] The array of values to exclude. - * @returns {Array} Returns a new array of filtered values. - */ - function baseDifference(array, values) { - var index = -1, - indexOf = getIndexOf(), - length = array ? array.length : 0, - isLarge = length >= largeArraySize && indexOf === baseIndexOf, - result = []; - - if (isLarge) { - var cache = createCache(values); - if (cache) { - indexOf = cacheIndexOf; - values = cache; - } else { - isLarge = false; - } - } - while (++index < length) { - var value = array[index]; - if (indexOf(values, value) < 0) { - result.push(value); - } - } - if (isLarge) { - releaseObject(values); - } - return result; - } - - /** - * The base implementation of `_.flatten` without support for callback - * shorthands or `thisArg` binding. - * - * @private - * @param {Array} array The array to flatten. - * @param {boolean} [isShallow=false] A flag to restrict flattening to a single level. - * @param {boolean} [isStrict=false] A flag to restrict flattening to arrays and `arguments` objects. - * @param {number} [fromIndex=0] The index to start from. - * @returns {Array} Returns a new flattened array. - */ - function baseFlatten(array, isShallow, isStrict, fromIndex) { - var index = (fromIndex || 0) - 1, - length = array ? array.length : 0, - result = []; - - while (++index < length) { - var value = array[index]; - - if (value && typeof value == 'object' && typeof value.length == 'number' - && (isArray(value) || isArguments(value))) { - // recursively flatten arrays (susceptible to call stack limits) - if (!isShallow) { - value = baseFlatten(value, isShallow, isStrict); - } - var valIndex = -1, - valLength = value.length, - resIndex = result.length; - - result.length += valLength; - while (++valIndex < valLength) { - result[resIndex++] = value[valIndex]; - } - } else if (!isStrict) { - result.push(value); - } - } - return result; - } - - /** - * The base implementation of `_.isEqual`, without support for `thisArg` binding, - * that allows partial "_.where" style comparisons. - * - * @private - * @param {*} a The value to compare. - * @param {*} b The other value to compare. - * @param {Function} [callback] The function to customize comparing values. - * @param {Function} [isWhere=false] A flag to indicate performing partial comparisons. - * @param {Array} [stackA=[]] Tracks traversed `a` objects. - * @param {Array} [stackB=[]] Tracks traversed `b` objects. - * @returns {boolean} Returns `true` if the values are equivalent, else `false`. - */ - function baseIsEqual(a, b, callback, isWhere, stackA, stackB) { - // used to indicate that when comparing objects, `a` has at least the properties of `b` - if (callback) { - var result = callback(a, b); - if (typeof result != 'undefined') { - return !!result; - } - } - // exit early for identical values - if (a === b) { - // treat `+0` vs. `-0` as not equal - return a !== 0 || (1 / a == 1 / b); - } - var type = typeof a, - otherType = typeof b; - - // exit early for unlike primitive values - if (a === a && - !(a && objectTypes[type]) && - !(b && objectTypes[otherType])) { - return false; - } - // exit early for `null` and `undefined` avoiding ES3's Function#call behavior - // http://es5.github.io/#x15.3.4.4 - if (a == null || b == null) { - return a === b; - } - // compare [[Class]] names - var className = toString.call(a), - otherClass = toString.call(b); - - if (className == argsClass) { - className = objectClass; - } - if (otherClass == argsClass) { - otherClass = objectClass; - } - if (className != otherClass) { - return false; - } - switch (className) { - case boolClass: - case dateClass: - // coerce dates and booleans to numbers, dates to milliseconds and booleans - // to `1` or `0` treating invalid dates coerced to `NaN` as not equal - return +a == +b; - - case numberClass: - // treat `NaN` vs. `NaN` as equal - return (a != +a) - ? b != +b - // but treat `+0` vs. `-0` as not equal - : (a == 0 ? (1 / a == 1 / b) : a == +b); - - case regexpClass: - case stringClass: - // coerce regexes to strings (http://es5.github.io/#x15.10.6.4) - // treat string primitives and their corresponding object instances as equal - return a == String(b); - } - var isArr = className == arrayClass; - if (!isArr) { - // unwrap any `lodash` wrapped values - var aWrapped = hasOwnProperty.call(a, '__wrapped__'), - bWrapped = hasOwnProperty.call(b, '__wrapped__'); - - if (aWrapped || bWrapped) { - return baseIsEqual(aWrapped ? a.__wrapped__ : a, bWrapped ? b.__wrapped__ : b, callback, isWhere, stackA, stackB); - } - // exit for functions and DOM nodes - if (className != objectClass || (!support.nodeClass && (isNode(a) || isNode(b)))) { - return false; - } - // in older versions of Opera, `arguments` objects have `Array` constructors - var ctorA = !support.argsObject && isArguments(a) ? Object : a.constructor, - ctorB = !support.argsObject && isArguments(b) ? Object : b.constructor; - - // non `Object` object instances with different constructors are not equal - if (ctorA != ctorB && - !(isFunction(ctorA) && ctorA instanceof ctorA && isFunction(ctorB) && ctorB instanceof ctorB) && - ('constructor' in a && 'constructor' in b) - ) { - return false; - } - } - // assume cyclic structures are equal - // the algorithm for detecting cyclic structures is adapted from ES 5.1 - // section 15.12.3, abstract operation `JO` (http://es5.github.io/#x15.12.3) - var initedStack = !stackA; - stackA || (stackA = getArray()); - stackB || (stackB = getArray()); - - var length = stackA.length; - while (length--) { - if (stackA[length] == a) { - return stackB[length] == b; - } - } - var size = 0; - result = true; - - // add `a` and `b` to the stack of traversed objects - stackA.push(a); - stackB.push(b); - - // recursively compare objects and arrays (susceptible to call stack limits) - if (isArr) { - // compare lengths to determine if a deep comparison is necessary - length = a.length; - size = b.length; - result = size == length; - - if (result || isWhere) { - // deep compare the contents, ignoring non-numeric properties - while (size--) { - var index = length, - value = b[size]; - - if (isWhere) { - while (index--) { - if ((result = baseIsEqual(a[index], value, callback, isWhere, stackA, stackB))) { - break; - } - } - } else if (!(result = baseIsEqual(a[size], value, callback, isWhere, stackA, stackB))) { - break; - } - } - } - } - else { - // deep compare objects using `forIn`, instead of `forOwn`, to avoid `Object.keys` - // which, in this case, is more costly - forIn(b, function(value, key, b) { - if (hasOwnProperty.call(b, key)) { - // count the number of properties. - size++; - // deep compare each property value. - return (result = hasOwnProperty.call(a, key) && baseIsEqual(a[key], value, callback, isWhere, stackA, stackB)); - } - }); - - if (result && !isWhere) { - // ensure both objects have the same number of properties - forIn(a, function(value, key, a) { - if (hasOwnProperty.call(a, key)) { - // `size` will be `-1` if `a` has more properties than `b` - return (result = --size > -1); - } - }); - } - } - stackA.pop(); - stackB.pop(); - - if (initedStack) { - releaseArray(stackA); - releaseArray(stackB); - } - return result; - } - - /** - * The base implementation of `_.merge` without argument juggling or support - * for `thisArg` binding. - * - * @private - * @param {Object} object The destination object. - * @param {Object} source The source object. - * @param {Function} [callback] The function to customize merging properties. - * @param {Array} [stackA=[]] Tracks traversed source objects. - * @param {Array} [stackB=[]] Associates values with source counterparts. - */ - function baseMerge(object, source, callback, stackA, stackB) { - (isArray(source) ? forEach : forOwn)(source, function(source, key) { - var found, - isArr, - result = source, - value = object[key]; - - if (source && ((isArr = isArray(source)) || isPlainObject(source))) { - // avoid merging previously merged cyclic sources - var stackLength = stackA.length; - while (stackLength--) { - if ((found = stackA[stackLength] == source)) { - value = stackB[stackLength]; - break; - } - } - if (!found) { - var isShallow; - if (callback) { - result = callback(value, source); - if ((isShallow = typeof result != 'undefined')) { - value = result; - } - } - if (!isShallow) { - value = isArr - ? (isArray(value) ? value : []) - : (isPlainObject(value) ? value : {}); - } - // add `source` and associated `value` to the stack of traversed objects - stackA.push(source); - stackB.push(value); - - // recursively merge objects and arrays (susceptible to call stack limits) - if (!isShallow) { - baseMerge(value, source, callback, stackA, stackB); - } - } - } - else { - if (callback) { - result = callback(value, source); - if (typeof result == 'undefined') { - result = source; - } - } - if (typeof result != 'undefined') { - value = result; - } - } - object[key] = value; - }); - } - - /** - * The base implementation of `_.random` without argument juggling or support - * for returning floating-point numbers. - * - * @private - * @param {number} min The minimum possible value. - * @param {number} max The maximum possible value. - * @returns {number} Returns a random number. - */ - function baseRandom(min, max) { - return min + floor(nativeRandom() * (max - min + 1)); - } - - /** - * The base implementation of `_.uniq` without support for callback shorthands - * or `thisArg` binding. - * - * @private - * @param {Array} array The array to process. - * @param {boolean} [isSorted=false] A flag to indicate that `array` is sorted. - * @param {Function} [callback] The function called per iteration. - * @returns {Array} Returns a duplicate-value-free array. - */ - function baseUniq(array, isSorted, callback) { - var index = -1, - indexOf = getIndexOf(), - length = array ? array.length : 0, - result = []; - - var isLarge = !isSorted && length >= largeArraySize && indexOf === baseIndexOf, - seen = (callback || isLarge) ? getArray() : result; - - if (isLarge) { - var cache = createCache(seen); - indexOf = cacheIndexOf; - seen = cache; - } - while (++index < length) { - var value = array[index], - computed = callback ? callback(value, index, array) : value; - - if (isSorted - ? !index || seen[seen.length - 1] !== computed - : indexOf(seen, computed) < 0 - ) { - if (callback || isLarge) { - seen.push(computed); - } - result.push(value); - } - } - if (isLarge) { - releaseArray(seen.array); - releaseObject(seen); - } else if (callback) { - releaseArray(seen); - } - return result; - } - - /** - * Creates a function that aggregates a collection, creating an object composed - * of keys generated from the results of running each element of the collection - * through a callback. The given `setter` function sets the keys and values - * of the composed object. - * - * @private - * @param {Function} setter The setter function. - * @returns {Function} Returns the new aggregator function. - */ - function createAggregator(setter) { - return function(collection, callback, thisArg) { - var result = {}; - callback = lodash.createCallback(callback, thisArg, 3); - - if (isArray(collection)) { - var index = -1, - length = collection.length; - - while (++index < length) { - var value = collection[index]; - setter(result, value, callback(value, index, collection), collection); - } - } else { - baseEach(collection, function(value, key, collection) { - setter(result, value, callback(value, key, collection), collection); - }); - } - return result; - }; - } - - /** - * Creates a function that, when called, either curries or invokes `func` - * with an optional `this` binding and partially applied arguments. - * - * @private - * @param {Function|string} func The function or method name to reference. - * @param {number} bitmask The bitmask of method flags to compose. - * The bitmask may be composed of the following flags: - * 1 - `_.bind` - * 2 - `_.bindKey` - * 4 - `_.curry` - * 8 - `_.curry` (bound) - * 16 - `_.partial` - * 32 - `_.partialRight` - * @param {Array} [partialArgs] An array of arguments to prepend to those - * provided to the new function. - * @param {Array} [partialRightArgs] An array of arguments to append to those - * provided to the new function. - * @param {*} [thisArg] The `this` binding of `func`. - * @param {number} [arity] The arity of `func`. - * @returns {Function} Returns the new function. - */ - function createWrapper(func, bitmask, partialArgs, partialRightArgs, thisArg, arity) { - var isBind = bitmask & 1, - isBindKey = bitmask & 2, - isCurry = bitmask & 4, - isCurryBound = bitmask & 8, - isPartial = bitmask & 16, - isPartialRight = bitmask & 32; - - if (!isBindKey && !isFunction(func)) { - throw new TypeError; - } - if (isPartial && !partialArgs.length) { - bitmask &= ~16; - isPartial = partialArgs = false; - } - if (isPartialRight && !partialRightArgs.length) { - bitmask &= ~32; - isPartialRight = partialRightArgs = false; - } - var bindData = func && func.__bindData__; - if (bindData && bindData !== true) { - // clone `bindData` - bindData = slice(bindData); - if (bindData[2]) { - bindData[2] = slice(bindData[2]); - } - if (bindData[3]) { - bindData[3] = slice(bindData[3]); - } - // set `thisBinding` is not previously bound - if (isBind && !(bindData[1] & 1)) { - bindData[4] = thisArg; - } - // set if previously bound but not currently (subsequent curried functions) - if (!isBind && bindData[1] & 1) { - bitmask |= 8; - } - // set curried arity if not yet set - if (isCurry && !(bindData[1] & 4)) { - bindData[5] = arity; - } - // append partial left arguments - if (isPartial) { - push.apply(bindData[2] || (bindData[2] = []), partialArgs); - } - // append partial right arguments - if (isPartialRight) { - unshift.apply(bindData[3] || (bindData[3] = []), partialRightArgs); - } - // merge flags - bindData[1] |= bitmask; - return createWrapper.apply(null, bindData); - } - // fast path for `_.bind` - var creater = (bitmask == 1 || bitmask === 17) ? baseBind : baseCreateWrapper; - return creater([func, bitmask, partialArgs, partialRightArgs, thisArg, arity]); - } - - /** - * Creates compiled iteration functions. - * - * @private - * @param {...Object} [options] The compile options object(s). - * @param {string} [options.array] Code to determine if the iterable is an array or array-like. - * @param {boolean} [options.useHas] Specify using `hasOwnProperty` checks in the object loop. - * @param {Function} [options.keys] A reference to `_.keys` for use in own property iteration. - * @param {string} [options.args] A comma separated string of iteration function arguments. - * @param {string} [options.top] Code to execute before the iteration branches. - * @param {string} [options.loop] Code to execute in the object loop. - * @param {string} [options.bottom] Code to execute after the iteration branches. - * @returns {Function} Returns the compiled function. - */ - function createIterator() { - // data properties - iteratorData.shadowedProps = shadowedProps; - iteratorData.support = support; - - // iterator options - iteratorData.array = iteratorData.bottom = iteratorData.loop = iteratorData.top = ''; - iteratorData.init = 'iterable'; - iteratorData.useHas = true; - - // merge options into a template data object - for (var object, index = 0; object = arguments[index]; index++) { - for (var key in object) { - iteratorData[key] = object[key]; - } - } - var args = iteratorData.args; - iteratorData.firstArg = /^[^,]+/.exec(args)[0]; - - // create the function factory - var factory = Function( - 'baseCreateCallback, errorClass, errorProto, hasOwnProperty, ' + - 'indicatorObject, isArguments, isArray, isString, keys, objectProto, ' + - 'objectTypes, nonEnumProps, stringClass, stringProto, toString', - 'return function(' + args + ') {\n' + iteratorTemplate(iteratorData) + '\n}' - ); - - // return the compiled function - return factory( - baseCreateCallback, errorClass, errorProto, hasOwnProperty, - indicatorObject, isArguments, isArray, isString, iteratorData.keys, objectProto, - objectTypes, nonEnumProps, stringClass, stringProto, toString - ); - } - - /** - * Used by `escape` to convert characters to HTML entities. - * - * @private - * @param {string} match The matched character to escape. - * @returns {string} Returns the escaped character. - */ - function escapeHtmlChar(match) { - return htmlEscapes[match]; - } - - /** - * Gets the appropriate "indexOf" function. If the `_.indexOf` method is - * customized, this method returns the custom method, otherwise it returns - * the `baseIndexOf` function. - * - * @private - * @returns {Function} Returns the "indexOf" function. - */ - function getIndexOf() { - var result = (result = lodash.indexOf) === indexOf ? baseIndexOf : result; - return result; - } - - /** - * Checks if `value` is a native function. - * - * @private - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if the `value` is a native function, else `false`. - */ - function isNative(value) { - return typeof value == 'function' && reNative.test(value); - } - - /** - * Sets `this` binding data on a given function. - * - * @private - * @param {Function} func The function to set data on. - * @param {Array} value The data array to set. - */ - var setBindData = !defineProperty ? noop : function(func, value) { - descriptor.value = value; - defineProperty(func, '__bindData__', descriptor); - }; - - /** - * A fallback implementation of `isPlainObject` which checks if a given value - * is an object created by the `Object` constructor, assuming objects created - * by the `Object` constructor have no inherited enumerable properties and that - * there are no `Object.prototype` extensions. - * - * @private - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a plain object, else `false`. - */ - function shimIsPlainObject(value) { - var ctor, - result; - - // avoid non Object objects, `arguments` objects, and DOM elements - if (!(value && toString.call(value) == objectClass) || - (ctor = value.constructor, isFunction(ctor) && !(ctor instanceof ctor)) || - (!support.argsClass && isArguments(value)) || - (!support.nodeClass && isNode(value))) { - return false; - } - // IE < 9 iterates inherited properties before own properties. If the first - // iterated property is an object's own property then there are no inherited - // enumerable properties. - if (support.ownLast) { - forIn(value, function(value, key, object) { - result = hasOwnProperty.call(object, key); - return false; - }); - return result !== false; - } - // In most environments an object's own properties are iterated before - // its inherited properties. If the last iterated property is an object's - // own property then there are no inherited enumerable properties. - forIn(value, function(value, key) { - result = key; - }); - return typeof result == 'undefined' || hasOwnProperty.call(value, result); - } - - /** - * Used by `unescape` to convert HTML entities to characters. - * - * @private - * @param {string} match The matched character to unescape. - * @returns {string} Returns the unescaped character. - */ - function unescapeHtmlChar(match) { - return htmlUnescapes[match]; - } - - /*--------------------------------------------------------------------------*/ - - /** - * Checks if `value` is an `arguments` object. - * - * @static - * @memberOf _ - * @category Objects - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if the `value` is an `arguments` object, else `false`. - * @example - * - * (function() { return _.isArguments(arguments); })(1, 2, 3); - * // => true - * - * _.isArguments([1, 2, 3]); - * // => false - */ - function isArguments(value) { - return value && typeof value == 'object' && typeof value.length == 'number' && - toString.call(value) == argsClass || false; - } - // fallback for browsers that can't detect `arguments` objects by [[Class]] - if (!support.argsClass) { - isArguments = function(value) { - return value && typeof value == 'object' && typeof value.length == 'number' && - hasOwnProperty.call(value, 'callee') && !propertyIsEnumerable.call(value, 'callee') || false; - }; - } - - /** - * Checks if `value` is an array. - * - * @static - * @memberOf _ - * @type Function - * @category Objects - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if the `value` is an array, else `false`. - * @example - * - * (function() { return _.isArray(arguments); })(); - * // => false - * - * _.isArray([1, 2, 3]); - * // => true - */ - var isArray = nativeIsArray || function(value) { - return value && typeof value == 'object' && typeof value.length == 'number' && - toString.call(value) == arrayClass || false; - }; - - /** - * A fallback implementation of `Object.keys` which produces an array of the - * given object's own enumerable property names. - * - * @private - * @type Function - * @param {Object} object The object to inspect. - * @returns {Array} Returns an array of property names. - */ - var shimKeys = createIterator({ - 'args': 'object', - 'init': '[]', - 'top': 'if (!(objectTypes[typeof object])) return result', - 'loop': 'result.push(index)' - }); - - /** - * Creates an array composed of the own enumerable property names of an object. - * - * @static - * @memberOf _ - * @category Objects - * @param {Object} object The object to inspect. - * @returns {Array} Returns an array of property names. - * @example - * - * _.keys({ 'one': 1, 'two': 2, 'three': 3 }); - * // => ['one', 'two', 'three'] (property order is not guaranteed across environments) - */ - var keys = !nativeKeys ? shimKeys : function(object) { - if (!isObject(object)) { - return []; - } - if ((support.enumPrototypes && typeof object == 'function') || - (support.nonEnumArgs && object.length && isArguments(object))) { - return shimKeys(object); - } - return nativeKeys(object); - }; - - /** Reusable iterator options shared by `each`, `forIn`, and `forOwn` */ - var eachIteratorOptions = { - 'args': 'collection, callback, thisArg', - 'top': "callback = callback && typeof thisArg == 'undefined' ? callback : baseCreateCallback(callback, thisArg, 3)", - 'array': "typeof length == 'number'", - 'keys': keys, - 'loop': 'if (callback(iterable[index], index, collection) === false) return result' - }; - - /** Reusable iterator options for `assign` and `defaults` */ - var defaultsIteratorOptions = { - 'args': 'object, source, guard', - 'top': - 'var args = arguments,\n' + - ' argsIndex = 0,\n' + - " argsLength = typeof guard == 'number' ? 2 : args.length;\n" + - 'while (++argsIndex < argsLength) {\n' + - ' iterable = args[argsIndex];\n' + - ' if (iterable && objectTypes[typeof iterable]) {', - 'keys': keys, - 'loop': "if (typeof result[index] == 'undefined') result[index] = iterable[index]", - 'bottom': ' }\n}' - }; - - /** Reusable iterator options for `forIn` and `forOwn` */ - var forOwnIteratorOptions = { - 'top': 'if (!objectTypes[typeof iterable]) return result;\n' + eachIteratorOptions.top, - 'array': false - }; - - /** - * Used to convert characters to HTML entities: - * - * Though the `>` character is escaped for symmetry, characters like `>` and `/` - * don't require escaping in HTML and have no special meaning unless they're part - * of a tag or an unquoted attribute value. - * http://mathiasbynens.be/notes/ambiguous-ampersands (under "semi-related fun fact") - */ - var htmlEscapes = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''' - }; - - /** Used to convert HTML entities to characters */ - var htmlUnescapes = invert(htmlEscapes); - - /** Used to match HTML entities and HTML characters */ - var reEscapedHtml = RegExp('(' + keys(htmlUnescapes).join('|') + ')', 'g'), - reUnescapedHtml = RegExp('[' + keys(htmlEscapes).join('') + ']', 'g'); - - /** - * A function compiled to iterate `arguments` objects, arrays, objects, and - * strings consistenly across environments, executing the callback for each - * element in the collection. The callback is bound to `thisArg` and invoked - * with three arguments; (value, index|key, collection). Callbacks may exit - * iteration early by explicitly returning `false`. - * - * @private - * @type Function - * @param {Array|Object|string} collection The collection to iterate over. - * @param {Function} [callback=identity] The function called per iteration. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {Array|Object|string} Returns `collection`. - */ - var baseEach = createIterator(eachIteratorOptions); - - /*--------------------------------------------------------------------------*/ - - /** - * Assigns own enumerable properties of source object(s) to the destination - * object. Subsequent sources will overwrite property assignments of previous - * sources. If a callback is provided it will be executed to produce the - * assigned values. The callback is bound to `thisArg` and invoked with two - * arguments; (objectValue, sourceValue). - * - * @static - * @memberOf _ - * @type Function - * @alias extend - * @category Objects - * @param {Object} object The destination object. - * @param {...Object} [source] The source objects. - * @param {Function} [callback] The function to customize assigning values. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {Object} Returns the destination object. - * @example - * - * _.assign({ 'name': 'fred' }, { 'employer': 'slate' }); - * // => { 'name': 'fred', 'employer': 'slate' } - * - * var defaults = _.partialRight(_.assign, function(a, b) { - * return typeof a == 'undefined' ? b : a; - * }); - * - * var object = { 'name': 'barney' }; - * defaults(object, { 'name': 'fred', 'employer': 'slate' }); - * // => { 'name': 'barney', 'employer': 'slate' } - */ - var assign = createIterator(defaultsIteratorOptions, { - 'top': - defaultsIteratorOptions.top.replace(';', - ';\n' + - "if (argsLength > 3 && typeof args[argsLength - 2] == 'function') {\n" + - ' var callback = baseCreateCallback(args[--argsLength - 1], args[argsLength--], 2);\n' + - "} else if (argsLength > 2 && typeof args[argsLength - 1] == 'function') {\n" + - ' callback = args[--argsLength];\n' + - '}' - ), - 'loop': 'result[index] = callback ? callback(result[index], iterable[index]) : iterable[index]' - }); - - /** - * Creates a clone of `value`. If `isDeep` is `true` nested objects will also - * be cloned, otherwise they will be assigned by reference. If a callback - * is provided it will be executed to produce the cloned values. If the - * callback returns `undefined` cloning will be handled by the method instead. - * The callback is bound to `thisArg` and invoked with one argument; (value). - * - * @static - * @memberOf _ - * @category Objects - * @param {*} value The value to clone. - * @param {boolean} [isDeep=false] Specify a deep clone. - * @param {Function} [callback] The function to customize cloning values. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {*} Returns the cloned value. - * @example - * - * var characters = [ - * { 'name': 'barney', 'age': 36 }, - * { 'name': 'fred', 'age': 40 } - * ]; - * - * var shallow = _.clone(characters); - * shallow[0] === characters[0]; - * // => true - * - * var deep = _.clone(characters, true); - * deep[0] === characters[0]; - * // => false - * - * _.mixin({ - * 'clone': _.partialRight(_.clone, function(value) { - * return _.isElement(value) ? value.cloneNode(false) : undefined; - * }) - * }); - * - * var clone = _.clone(document.body); - * clone.childNodes.length; - * // => 0 - */ - function clone(value, isDeep, callback, thisArg) { - // allows working with "Collections" methods without using their `index` - // and `collection` arguments for `isDeep` and `callback` - if (typeof isDeep != 'boolean' && isDeep != null) { - thisArg = callback; - callback = isDeep; - isDeep = false; - } - return baseClone(value, isDeep, typeof callback == 'function' && baseCreateCallback(callback, thisArg, 1)); - } - - /** - * Creates a deep clone of `value`. If a callback is provided it will be - * executed to produce the cloned values. If the callback returns `undefined` - * cloning will be handled by the method instead. The callback is bound to - * `thisArg` and invoked with one argument; (value). - * - * Note: This method is loosely based on the structured clone algorithm. Functions - * and DOM nodes are **not** cloned. The enumerable properties of `arguments` objects and - * objects created by constructors other than `Object` are cloned to plain `Object` objects. - * See http://www.w3.org/TR/html5/infrastructure.html#internal-structured-cloning-algorithm. - * - * @static - * @memberOf _ - * @category Objects - * @param {*} value The value to deep clone. - * @param {Function} [callback] The function to customize cloning values. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {*} Returns the deep cloned value. - * @example - * - * var characters = [ - * { 'name': 'barney', 'age': 36 }, - * { 'name': 'fred', 'age': 40 } - * ]; - * - * var deep = _.cloneDeep(characters); - * deep[0] === characters[0]; - * // => false - * - * var view = { - * 'label': 'docs', - * 'node': element - * }; - * - * var clone = _.cloneDeep(view, function(value) { - * return _.isElement(value) ? value.cloneNode(true) : undefined; - * }); - * - * clone.node == view.node; - * // => false - */ - function cloneDeep(value, callback, thisArg) { - return baseClone(value, true, typeof callback == 'function' && baseCreateCallback(callback, thisArg, 1)); - } - - /** - * Creates an object that inherits from the given `prototype` object. If a - * `properties` object is provided its own enumerable properties are assigned - * to the created object. - * - * @static - * @memberOf _ - * @category Objects - * @param {Object} prototype The object to inherit from. - * @param {Object} [properties] The properties to assign to the object. - * @returns {Object} Returns the new object. - * @example - * - * function Shape() { - * this.x = 0; - * this.y = 0; - * } - * - * function Circle() { - * Shape.call(this); - * } - * - * Circle.prototype = _.create(Shape.prototype, { 'constructor': Circle }); - * - * var circle = new Circle; - * circle instanceof Circle; - * // => true - * - * circle instanceof Shape; - * // => true - */ - function create(prototype, properties) { - var result = baseCreate(prototype); - return properties ? assign(result, properties) : result; - } - - /** - * Assigns own enumerable properties of source object(s) to the destination - * object for all destination properties that resolve to `undefined`. Once a - * property is set, additional defaults of the same property will be ignored. - * - * @static - * @memberOf _ - * @type Function - * @category Objects - * @param {Object} object The destination object. - * @param {...Object} [source] The source objects. - * @param- {Object} [guard] Allows working with `_.reduce` without using its - * `key` and `object` arguments as sources. - * @returns {Object} Returns the destination object. - * @example - * - * var object = { 'name': 'barney' }; - * _.defaults(object, { 'name': 'fred', 'employer': 'slate' }); - * // => { 'name': 'barney', 'employer': 'slate' } - */ - var defaults = createIterator(defaultsIteratorOptions); - - /** - * This method is like `_.findIndex` except that it returns the key of the - * first element that passes the callback check, instead of the element itself. - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Objects - * @param {Object} object The object to search. - * @param {Function|Object|string} [callback=identity] The function called per - * iteration. If a property name or object is provided it will be used to - * create a "_.pluck" or "_.where" style callback, respectively. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {string|undefined} Returns the key of the found element, else `undefined`. - * @example - * - * var characters = { - * 'barney': { 'age': 36, 'blocked': false }, - * 'fred': { 'age': 40, 'blocked': true }, - * 'pebbles': { 'age': 1, 'blocked': false } - * }; - * - * _.findKey(characters, function(chr) { - * return chr.age < 40; - * }); - * // => 'barney' (property order is not guaranteed across environments) - * - * // using "_.where" callback shorthand - * _.findKey(characters, { 'age': 1 }); - * // => 'pebbles' - * - * // using "_.pluck" callback shorthand - * _.findKey(characters, 'blocked'); - * // => 'fred' - */ - function findKey(object, callback, thisArg) { - var result; - callback = lodash.createCallback(callback, thisArg, 3); - forOwn(object, function(value, key, object) { - if (callback(value, key, object)) { - result = key; - return false; - } - }); - return result; - } - - /** - * This method is like `_.findKey` except that it iterates over elements - * of a `collection` in the opposite order. - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Objects - * @param {Object} object The object to search. - * @param {Function|Object|string} [callback=identity] The function called per - * iteration. If a property name or object is provided it will be used to - * create a "_.pluck" or "_.where" style callback, respectively. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {string|undefined} Returns the key of the found element, else `undefined`. - * @example - * - * var characters = { - * 'barney': { 'age': 36, 'blocked': true }, - * 'fred': { 'age': 40, 'blocked': false }, - * 'pebbles': { 'age': 1, 'blocked': true } - * }; - * - * _.findLastKey(characters, function(chr) { - * return chr.age < 40; - * }); - * // => returns `pebbles`, assuming `_.findKey` returns `barney` - * - * // using "_.where" callback shorthand - * _.findLastKey(characters, { 'age': 40 }); - * // => 'fred' - * - * // using "_.pluck" callback shorthand - * _.findLastKey(characters, 'blocked'); - * // => 'pebbles' - */ - function findLastKey(object, callback, thisArg) { - var result; - callback = lodash.createCallback(callback, thisArg, 3); - forOwnRight(object, function(value, key, object) { - if (callback(value, key, object)) { - result = key; - return false; - } - }); - return result; - } - - /** - * Iterates over own and inherited enumerable properties of an object, - * executing the callback for each property. The callback is bound to `thisArg` - * and invoked with three arguments; (value, key, object). Callbacks may exit - * iteration early by explicitly returning `false`. - * - * @static - * @memberOf _ - * @type Function - * @category Objects - * @param {Object} object The object to iterate over. - * @param {Function} [callback=identity] The function called per iteration. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {Object} Returns `object`. - * @example - * - * function Shape() { - * this.x = 0; - * this.y = 0; - * } - * - * Shape.prototype.move = function(x, y) { - * this.x += x; - * this.y += y; - * }; - * - * _.forIn(new Shape, function(value, key) { - * console.log(key); - * }); - * // => logs 'x', 'y', and 'move' (property order is not guaranteed across environments) - */ - var forIn = createIterator(eachIteratorOptions, forOwnIteratorOptions, { - 'useHas': false - }); - - /** - * This method is like `_.forIn` except that it iterates over elements - * of a `collection` in the opposite order. - * - * @static - * @memberOf _ - * @category Objects - * @param {Object} object The object to iterate over. - * @param {Function} [callback=identity] The function called per iteration. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {Object} Returns `object`. - * @example - * - * function Shape() { - * this.x = 0; - * this.y = 0; - * } - * - * Shape.prototype.move = function(x, y) { - * this.x += x; - * this.y += y; - * }; - * - * _.forInRight(new Shape, function(value, key) { - * console.log(key); - * }); - * // => logs 'move', 'y', and 'x' assuming `_.forIn ` logs 'x', 'y', and 'move' - */ - function forInRight(object, callback, thisArg) { - var pairs = []; - - forIn(object, function(value, key) { - pairs.push(key, value); - }); - - var length = pairs.length; - callback = baseCreateCallback(callback, thisArg, 3); - while (length--) { - if (callback(pairs[length--], pairs[length], object) === false) { - break; - } - } - return object; - } - - /** - * Iterates over own enumerable properties of an object, executing the callback - * for each property. The callback is bound to `thisArg` and invoked with three - * arguments; (value, key, object). Callbacks may exit iteration early by - * explicitly returning `false`. - * - * @static - * @memberOf _ - * @type Function - * @category Objects - * @param {Object} object The object to iterate over. - * @param {Function} [callback=identity] The function called per iteration. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {Object} Returns `object`. - * @example - * - * _.forOwn({ '0': 'zero', '1': 'one', 'length': 2 }, function(num, key) { - * console.log(key); - * }); - * // => logs '0', '1', and 'length' (property order is not guaranteed across environments) - */ - var forOwn = createIterator(eachIteratorOptions, forOwnIteratorOptions); - - /** - * This method is like `_.forOwn` except that it iterates over elements - * of a `collection` in the opposite order. - * - * @static - * @memberOf _ - * @category Objects - * @param {Object} object The object to iterate over. - * @param {Function} [callback=identity] The function called per iteration. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {Object} Returns `object`. - * @example - * - * _.forOwnRight({ '0': 'zero', '1': 'one', 'length': 2 }, function(num, key) { - * console.log(key); - * }); - * // => logs 'length', '1', and '0' assuming `_.forOwn` logs '0', '1', and 'length' - */ - function forOwnRight(object, callback, thisArg) { - var props = keys(object), - length = props.length; - - callback = baseCreateCallback(callback, thisArg, 3); - while (length--) { - var key = props[length]; - if (callback(object[key], key, object) === false) { - break; - } - } - return object; - } - - /** - * Creates a sorted array of property names of all enumerable properties, - * own and inherited, of `object` that have function values. - * - * @static - * @memberOf _ - * @alias methods - * @category Objects - * @param {Object} object The object to inspect. - * @returns {Array} Returns an array of property names that have function values. - * @example - * - * _.functions(_); - * // => ['all', 'any', 'bind', 'bindAll', 'clone', 'compact', 'compose', ...] - */ - function functions(object) { - var result = []; - forIn(object, function(value, key) { - if (isFunction(value)) { - result.push(key); - } - }); - return result.sort(); - } - - /** - * Checks if the specified property name exists as a direct property of `object`, - * instead of an inherited property. - * - * @static - * @memberOf _ - * @category Objects - * @param {Object} object The object to inspect. - * @param {string} key The name of the property to check. - * @returns {boolean} Returns `true` if key is a direct property, else `false`. - * @example - * - * _.has({ 'a': 1, 'b': 2, 'c': 3 }, 'b'); - * // => true - */ - function has(object, key) { - return object ? hasOwnProperty.call(object, key) : false; - } - - /** - * Creates an object composed of the inverted keys and values of the given object. - * - * @static - * @memberOf _ - * @category Objects - * @param {Object} object The object to invert. - * @returns {Object} Returns the created inverted object. - * @example - * - * _.invert({ 'first': 'fred', 'second': 'barney' }); - * // => { 'fred': 'first', 'barney': 'second' } - */ - function invert(object) { - var index = -1, - props = keys(object), - length = props.length, - result = {}; - - while (++index < length) { - var key = props[index]; - result[object[key]] = key; - } - return result; - } - - /** - * Checks if `value` is a boolean value. - * - * @static - * @memberOf _ - * @category Objects - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if the `value` is a boolean value, else `false`. - * @example - * - * _.isBoolean(null); - * // => false - */ - function isBoolean(value) { - return value === true || value === false || - value && typeof value == 'object' && toString.call(value) == boolClass || false; - } - - /** - * Checks if `value` is a date. - * - * @static - * @memberOf _ - * @category Objects - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if the `value` is a date, else `false`. - * @example - * - * _.isDate(new Date); - * // => true - */ - function isDate(value) { - return value && typeof value == 'object' && toString.call(value) == dateClass || false; - } - - /** - * Checks if `value` is a DOM element. - * - * @static - * @memberOf _ - * @category Objects - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if the `value` is a DOM element, else `false`. - * @example - * - * _.isElement(document.body); - * // => true - */ - function isElement(value) { - return value && value.nodeType === 1 || false; - } - - /** - * Checks if `value` is empty. Arrays, strings, or `arguments` objects with a - * length of `0` and objects with no own enumerable properties are considered - * "empty". - * - * @static - * @memberOf _ - * @category Objects - * @param {Array|Object|string} value The value to inspect. - * @returns {boolean} Returns `true` if the `value` is empty, else `false`. - * @example - * - * _.isEmpty([1, 2, 3]); - * // => false - * - * _.isEmpty({}); - * // => true - * - * _.isEmpty(''); - * // => true - */ - function isEmpty(value) { - var result = true; - if (!value) { - return result; - } - var className = toString.call(value), - length = value.length; - - if ((className == arrayClass || className == stringClass || - (support.argsClass ? className == argsClass : isArguments(value))) || - (className == objectClass && typeof length == 'number' && isFunction(value.splice))) { - return !length; - } - forOwn(value, function() { - return (result = false); - }); - return result; - } - - /** - * Performs a deep comparison between two values to determine if they are - * equivalent to each other. If a callback is provided it will be executed - * to compare values. If the callback returns `undefined` comparisons will - * be handled by the method instead. The callback is bound to `thisArg` and - * invoked with two arguments; (a, b). - * - * @static - * @memberOf _ - * @category Objects - * @param {*} a The value to compare. - * @param {*} b The other value to compare. - * @param {Function} [callback] The function to customize comparing values. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {boolean} Returns `true` if the values are equivalent, else `false`. - * @example - * - * var object = { 'name': 'fred' }; - * var copy = { 'name': 'fred' }; - * - * object == copy; - * // => false - * - * _.isEqual(object, copy); - * // => true - * - * var words = ['hello', 'goodbye']; - * var otherWords = ['hi', 'goodbye']; - * - * _.isEqual(words, otherWords, function(a, b) { - * var reGreet = /^(?:hello|hi)$/i, - * aGreet = _.isString(a) && reGreet.test(a), - * bGreet = _.isString(b) && reGreet.test(b); - * - * return (aGreet || bGreet) ? (aGreet == bGreet) : undefined; - * }); - * // => true - */ - function isEqual(a, b, callback, thisArg) { - return baseIsEqual(a, b, typeof callback == 'function' && baseCreateCallback(callback, thisArg, 2)); - } - - /** - * Checks if `value` is, or can be coerced to, a finite number. - * - * Note: This is not the same as native `isFinite` which will return true for - * booleans and empty strings. See http://es5.github.io/#x15.1.2.5. - * - * @static - * @memberOf _ - * @category Objects - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if the `value` is finite, else `false`. - * @example - * - * _.isFinite(-101); - * // => true - * - * _.isFinite('10'); - * // => true - * - * _.isFinite(true); - * // => false - * - * _.isFinite(''); - * // => false - * - * _.isFinite(Infinity); - * // => false - */ - function isFinite(value) { - return nativeIsFinite(value) && !nativeIsNaN(parseFloat(value)); - } - - /** - * Checks if `value` is a function. - * - * @static - * @memberOf _ - * @category Objects - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if the `value` is a function, else `false`. - * @example - * - * _.isFunction(_); - * // => true - */ - function isFunction(value) { - return typeof value == 'function'; - } - // fallback for older versions of Chrome and Safari - if (isFunction(/x/)) { - isFunction = function(value) { - return typeof value == 'function' && toString.call(value) == funcClass; - }; - } - - /** - * Checks if `value` is the language type of Object. - * (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) - * - * @static - * @memberOf _ - * @category Objects - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if the `value` is an object, else `false`. - * @example - * - * _.isObject({}); - * // => true - * - * _.isObject([1, 2, 3]); - * // => true - * - * _.isObject(1); - * // => false - */ - function isObject(value) { - // check if the value is the ECMAScript language type of Object - // http://es5.github.io/#x8 - // and avoid a V8 bug - // http://code.google.com/p/v8/issues/detail?id=2291 - return !!(value && objectTypes[typeof value]); - } - - /** - * Checks if `value` is `NaN`. - * - * Note: This is not the same as native `isNaN` which will return `true` for - * `undefined` and other non-numeric values. See http://es5.github.io/#x15.1.2.4. - * - * @static - * @memberOf _ - * @category Objects - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if the `value` is `NaN`, else `false`. - * @example - * - * _.isNaN(NaN); - * // => true - * - * _.isNaN(new Number(NaN)); - * // => true - * - * isNaN(undefined); - * // => true - * - * _.isNaN(undefined); - * // => false - */ - function isNaN(value) { - // `NaN` as a primitive is the only value that is not equal to itself - // (perform the [[Class]] check first to avoid errors with some host objects in IE) - return isNumber(value) && value != +value; - } - - /** - * Checks if `value` is `null`. - * - * @static - * @memberOf _ - * @category Objects - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if the `value` is `null`, else `false`. - * @example - * - * _.isNull(null); - * // => true - * - * _.isNull(undefined); - * // => false - */ - function isNull(value) { - return value === null; - } - - /** - * Checks if `value` is a number. - * - * Note: `NaN` is considered a number. See http://es5.github.io/#x8.5. - * - * @static - * @memberOf _ - * @category Objects - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if the `value` is a number, else `false`. - * @example - * - * _.isNumber(8.4 * 5); - * // => true - */ - function isNumber(value) { - return typeof value == 'number' || - value && typeof value == 'object' && toString.call(value) == numberClass || false; - } - - /** - * Checks if `value` is an object created by the `Object` constructor. - * - * @static - * @memberOf _ - * @category Objects - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a plain object, else `false`. - * @example - * - * function Shape() { - * this.x = 0; - * this.y = 0; - * } - * - * _.isPlainObject(new Shape); - * // => false - * - * _.isPlainObject([1, 2, 3]); - * // => false - * - * _.isPlainObject({ 'x': 0, 'y': 0 }); - * // => true - */ - var isPlainObject = !getPrototypeOf ? shimIsPlainObject : function(value) { - if (!(value && toString.call(value) == objectClass) || (!support.argsClass && isArguments(value))) { - return false; - } - var valueOf = value.valueOf, - objProto = isNative(valueOf) && (objProto = getPrototypeOf(valueOf)) && getPrototypeOf(objProto); - - return objProto - ? (value == objProto || getPrototypeOf(value) == objProto) - : shimIsPlainObject(value); - }; - - /** - * Checks if `value` is a regular expression. - * - * @static - * @memberOf _ - * @category Objects - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if the `value` is a regular expression, else `false`. - * @example - * - * _.isRegExp(/fred/); - * // => true - */ - function isRegExp(value) { - return value && objectTypes[typeof value] && toString.call(value) == regexpClass || false; - } - - /** - * Checks if `value` is a string. - * - * @static - * @memberOf _ - * @category Objects - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if the `value` is a string, else `false`. - * @example - * - * _.isString('fred'); - * // => true - */ - function isString(value) { - return typeof value == 'string' || - value && typeof value == 'object' && toString.call(value) == stringClass || false; - } - - /** - * Checks if `value` is `undefined`. - * - * @static - * @memberOf _ - * @category Objects - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if the `value` is `undefined`, else `false`. - * @example - * - * _.isUndefined(void 0); - * // => true - */ - function isUndefined(value) { - return typeof value == 'undefined'; - } - - /** - * Creates an object with the same keys as `object` and values generated by - * running each own enumerable property of `object` through the callback. - * The callback is bound to `thisArg` and invoked with three arguments; - * (value, key, object). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Objects - * @param {Object} object The object to iterate over. - * @param {Function|Object|string} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {Array} Returns a new object with values of the results of each `callback` execution. - * @example - * - * _.mapValues({ 'a': 1, 'b': 2, 'c': 3} , function(num) { return num * 3; }); - * // => { 'a': 3, 'b': 6, 'c': 9 } - * - * var characters = { - * 'fred': { 'name': 'fred', 'age': 40 }, - * 'pebbles': { 'name': 'pebbles', 'age': 1 } - * }; - * - * // using "_.pluck" callback shorthand - * _.mapValues(characters, 'age'); - * // => { 'fred': 40, 'pebbles': 1 } - */ - function mapValues(object, callback, thisArg) { - var result = {}; - callback = lodash.createCallback(callback, thisArg, 3); - - forOwn(object, function(value, key, object) { - result[key] = callback(value, key, object); - }); - return result; - } - - /** - * Recursively merges own enumerable properties of the source object(s), that - * don't resolve to `undefined` into the destination object. Subsequent sources - * will overwrite property assignments of previous sources. If a callback is - * provided it will be executed to produce the merged values of the destination - * and source properties. If the callback returns `undefined` merging will - * be handled by the method instead. The callback is bound to `thisArg` and - * invoked with two arguments; (objectValue, sourceValue). - * - * @static - * @memberOf _ - * @category Objects - * @param {Object} object The destination object. - * @param {...Object} [source] The source objects. - * @param {Function} [callback] The function to customize merging properties. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {Object} Returns the destination object. - * @example - * - * var names = { - * 'characters': [ - * { 'name': 'barney' }, - * { 'name': 'fred' } - * ] - * }; - * - * var ages = { - * 'characters': [ - * { 'age': 36 }, - * { 'age': 40 } - * ] - * }; - * - * _.merge(names, ages); - * // => { 'characters': [{ 'name': 'barney', 'age': 36 }, { 'name': 'fred', 'age': 40 }] } - * - * var food = { - * 'fruits': ['apple'], - * 'vegetables': ['beet'] - * }; - * - * var otherFood = { - * 'fruits': ['banana'], - * 'vegetables': ['carrot'] - * }; - * - * _.merge(food, otherFood, function(a, b) { - * return _.isArray(a) ? a.concat(b) : undefined; - * }); - * // => { 'fruits': ['apple', 'banana'], 'vegetables': ['beet', 'carrot] } - */ - function merge(object) { - var args = arguments, - length = 2; - - if (!isObject(object)) { - return object; - } - // allows working with `_.reduce` and `_.reduceRight` without using - // their `index` and `collection` arguments - if (typeof args[2] != 'number') { - length = args.length; - } - if (length > 3 && typeof args[length - 2] == 'function') { - var callback = baseCreateCallback(args[--length - 1], args[length--], 2); - } else if (length > 2 && typeof args[length - 1] == 'function') { - callback = args[--length]; - } - var sources = slice(arguments, 1, length), - index = -1, - stackA = getArray(), - stackB = getArray(); - - while (++index < length) { - baseMerge(object, sources[index], callback, stackA, stackB); - } - releaseArray(stackA); - releaseArray(stackB); - return object; - } - - /** - * Creates a shallow clone of `object` excluding the specified properties. - * Property names may be specified as individual arguments or as arrays of - * property names. If a callback is provided it will be executed for each - * property of `object` omitting the properties the callback returns truey - * for. The callback is bound to `thisArg` and invoked with three arguments; - * (value, key, object). - * - * @static - * @memberOf _ - * @category Objects - * @param {Object} object The source object. - * @param {Function|...string|string[]} [callback] The properties to omit or the - * function called per iteration. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {Object} Returns an object without the omitted properties. - * @example - * - * _.omit({ 'name': 'fred', 'age': 40 }, 'age'); - * // => { 'name': 'fred' } - * - * _.omit({ 'name': 'fred', 'age': 40 }, function(value) { - * return typeof value == 'number'; - * }); - * // => { 'name': 'fred' } - */ - function omit(object, callback, thisArg) { - var result = {}; - if (typeof callback != 'function') { - var props = []; - forIn(object, function(value, key) { - props.push(key); - }); - props = baseDifference(props, baseFlatten(arguments, true, false, 1)); - - var index = -1, - length = props.length; - - while (++index < length) { - var key = props[index]; - result[key] = object[key]; - } - } else { - callback = lodash.createCallback(callback, thisArg, 3); - forIn(object, function(value, key, object) { - if (!callback(value, key, object)) { - result[key] = value; - } - }); - } - return result; - } - - /** - * Creates a two dimensional array of an object's key-value pairs, - * i.e. `[[key1, value1], [key2, value2]]`. - * - * @static - * @memberOf _ - * @category Objects - * @param {Object} object The object to inspect. - * @returns {Array} Returns new array of key-value pairs. - * @example - * - * _.pairs({ 'barney': 36, 'fred': 40 }); - * // => [['barney', 36], ['fred', 40]] (property order is not guaranteed across environments) - */ - function pairs(object) { - var index = -1, - props = keys(object), - length = props.length, - result = Array(length); - - while (++index < length) { - var key = props[index]; - result[index] = [key, object[key]]; - } - return result; - } - - /** - * Creates a shallow clone of `object` composed of the specified properties. - * Property names may be specified as individual arguments or as arrays of - * property names. If a callback is provided it will be executed for each - * property of `object` picking the properties the callback returns truey - * for. The callback is bound to `thisArg` and invoked with three arguments; - * (value, key, object). - * - * @static - * @memberOf _ - * @category Objects - * @param {Object} object The source object. - * @param {Function|...string|string[]} [callback] The function called per - * iteration or property names to pick, specified as individual property - * names or arrays of property names. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {Object} Returns an object composed of the picked properties. - * @example - * - * _.pick({ 'name': 'fred', '_userid': 'fred1' }, 'name'); - * // => { 'name': 'fred' } - * - * _.pick({ 'name': 'fred', '_userid': 'fred1' }, function(value, key) { - * return key.charAt(0) != '_'; - * }); - * // => { 'name': 'fred' } - */ - function pick(object, callback, thisArg) { - var result = {}; - if (typeof callback != 'function') { - var index = -1, - props = baseFlatten(arguments, true, false, 1), - length = isObject(object) ? props.length : 0; - - while (++index < length) { - var key = props[index]; - if (key in object) { - result[key] = object[key]; - } - } - } else { - callback = lodash.createCallback(callback, thisArg, 3); - forIn(object, function(value, key, object) { - if (callback(value, key, object)) { - result[key] = value; - } - }); - } - return result; - } - - /** - * An alternative to `_.reduce` this method transforms `object` to a new - * `accumulator` object which is the result of running each of its own - * enumerable properties through a callback, with each callback execution - * potentially mutating the `accumulator` object. The callback is bound to - * `thisArg` and invoked with four arguments; (accumulator, value, key, object). - * Callbacks may exit iteration early by explicitly returning `false`. - * - * @static - * @memberOf _ - * @category Objects - * @param {Array|Object} object The object to iterate over. - * @param {Function} [callback=identity] The function called per iteration. - * @param {*} [accumulator] The custom accumulator value. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {*} Returns the accumulated value. - * @example - * - * var squares = _.transform([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], function(result, num) { - * num *= num; - * if (num % 2) { - * return result.push(num) < 3; - * } - * }); - * // => [1, 9, 25] - * - * var mapped = _.transform({ 'a': 1, 'b': 2, 'c': 3 }, function(result, num, key) { - * result[key] = num * 3; - * }); - * // => { 'a': 3, 'b': 6, 'c': 9 } - */ - function transform(object, callback, accumulator, thisArg) { - var isArr = isArray(object); - if (accumulator == null) { - if (isArr) { - accumulator = []; - } else { - var ctor = object && object.constructor, - proto = ctor && ctor.prototype; - - accumulator = baseCreate(proto); - } - } - if (callback) { - callback = lodash.createCallback(callback, thisArg, 4); - (isArr ? baseEach : forOwn)(object, function(value, index, object) { - return callback(accumulator, value, index, object); - }); - } - return accumulator; - } - - /** - * Creates an array composed of the own enumerable property values of `object`. - * - * @static - * @memberOf _ - * @category Objects - * @param {Object} object The object to inspect. - * @returns {Array} Returns an array of property values. - * @example - * - * _.values({ 'one': 1, 'two': 2, 'three': 3 }); - * // => [1, 2, 3] (property order is not guaranteed across environments) - */ - function values(object) { - var index = -1, - props = keys(object), - length = props.length, - result = Array(length); - - while (++index < length) { - result[index] = object[props[index]]; - } - return result; - } - - /*--------------------------------------------------------------------------*/ - - /** - * Creates an array of elements from the specified indexes, or keys, of the - * `collection`. Indexes may be specified as individual arguments or as arrays - * of indexes. - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|string} collection The collection to iterate over. - * @param {...(number|number[]|string|string[])} [index] The indexes of `collection` - * to retrieve, specified as individual indexes or arrays of indexes. - * @returns {Array} Returns a new array of elements corresponding to the - * provided indexes. - * @example - * - * _.at(['a', 'b', 'c', 'd', 'e'], [0, 2, 4]); - * // => ['a', 'c', 'e'] - * - * _.at(['fred', 'barney', 'pebbles'], 0, 2); - * // => ['fred', 'pebbles'] - */ - function at(collection) { - var args = arguments, - index = -1, - props = baseFlatten(args, true, false, 1), - length = (args[2] && args[2][args[1]] === collection) ? 1 : props.length, - result = Array(length); - - if (support.unindexedChars && isString(collection)) { - collection = collection.split(''); - } - while(++index < length) { - result[index] = collection[props[index]]; - } - return result; - } - - /** - * Checks if a given value is present in a collection using strict equality - * for comparisons, i.e. `===`. If `fromIndex` is negative, it is used as the - * offset from the end of the collection. - * - * @static - * @memberOf _ - * @alias include - * @category Collections - * @param {Array|Object|string} collection The collection to iterate over. - * @param {*} target The value to check for. - * @param {number} [fromIndex=0] The index to search from. - * @returns {boolean} Returns `true` if the `target` element is found, else `false`. - * @example - * - * _.contains([1, 2, 3], 1); - * // => true - * - * _.contains([1, 2, 3], 1, 2); - * // => false - * - * _.contains({ 'name': 'fred', 'age': 40 }, 'fred'); - * // => true - * - * _.contains('pebbles', 'eb'); - * // => true - */ - function contains(collection, target, fromIndex) { - var index = -1, - indexOf = getIndexOf(), - length = collection ? collection.length : 0, - result = false; - - fromIndex = (fromIndex < 0 ? nativeMax(0, length + fromIndex) : fromIndex) || 0; - if (isArray(collection)) { - result = indexOf(collection, target, fromIndex) > -1; - } else if (typeof length == 'number') { - result = (isString(collection) ? collection.indexOf(target, fromIndex) : indexOf(collection, target, fromIndex)) > -1; - } else { - baseEach(collection, function(value) { - if (++index >= fromIndex) { - return !(result = value === target); - } - }); - } - return result; - } - - /** - * Creates an object composed of keys generated from the results of running - * each element of `collection` through the callback. The corresponding value - * of each key is the number of times the key was returned by the callback. - * The callback is bound to `thisArg` and invoked with three arguments; - * (value, index|key, collection). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|string} collection The collection to iterate over. - * @param {Function|Object|string} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {Object} Returns the composed aggregate object. - * @example - * - * _.countBy([4.3, 6.1, 6.4], function(num) { return Math.floor(num); }); - * // => { '4': 1, '6': 2 } - * - * _.countBy([4.3, 6.1, 6.4], function(num) { return this.floor(num); }, Math); - * // => { '4': 1, '6': 2 } - * - * _.countBy(['one', 'two', 'three'], 'length'); - * // => { '3': 2, '5': 1 } - */ - var countBy = createAggregator(function(result, value, key) { - (hasOwnProperty.call(result, key) ? result[key]++ : result[key] = 1); - }); - - /** - * Checks if the given callback returns truey value for **all** elements of - * a collection. The callback is bound to `thisArg` and invoked with three - * arguments; (value, index|key, collection). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @alias all - * @category Collections - * @param {Array|Object|string} collection The collection to iterate over. - * @param {Function|Object|string} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {boolean} Returns `true` if all elements passed the callback check, - * else `false`. - * @example - * - * _.every([true, 1, null, 'yes']); - * // => false - * - * var characters = [ - * { 'name': 'barney', 'age': 36 }, - * { 'name': 'fred', 'age': 40 } - * ]; - * - * // using "_.pluck" callback shorthand - * _.every(characters, 'age'); - * // => true - * - * // using "_.where" callback shorthand - * _.every(characters, { 'age': 36 }); - * // => false - */ - function every(collection, callback, thisArg) { - var result = true; - callback = lodash.createCallback(callback, thisArg, 3); - - if (isArray(collection)) { - var index = -1, - length = collection.length; - - while (++index < length) { - if (!(result = !!callback(collection[index], index, collection))) { - break; - } - } - } else { - baseEach(collection, function(value, index, collection) { - return (result = !!callback(value, index, collection)); - }); - } - return result; - } - - /** - * Iterates over elements of a collection, returning an array of all elements - * the callback returns truey for. The callback is bound to `thisArg` and - * invoked with three arguments; (value, index|key, collection). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @alias select - * @category Collections - * @param {Array|Object|string} collection The collection to iterate over. - * @param {Function|Object|string} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {Array} Returns a new array of elements that passed the callback check. - * @example - * - * var evens = _.filter([1, 2, 3, 4, 5, 6], function(num) { return num % 2 == 0; }); - * // => [2, 4, 6] - * - * var characters = [ - * { 'name': 'barney', 'age': 36, 'blocked': false }, - * { 'name': 'fred', 'age': 40, 'blocked': true } - * ]; - * - * // using "_.pluck" callback shorthand - * _.filter(characters, 'blocked'); - * // => [{ 'name': 'fred', 'age': 40, 'blocked': true }] - * - * // using "_.where" callback shorthand - * _.filter(characters, { 'age': 36 }); - * // => [{ 'name': 'barney', 'age': 36, 'blocked': false }] - */ - function filter(collection, callback, thisArg) { - var result = []; - callback = lodash.createCallback(callback, thisArg, 3); - - if (isArray(collection)) { - var index = -1, - length = collection.length; - - while (++index < length) { - var value = collection[index]; - if (callback(value, index, collection)) { - result.push(value); - } - } - } else { - baseEach(collection, function(value, index, collection) { - if (callback(value, index, collection)) { - result.push(value); - } - }); - } - return result; - } - - /** - * Iterates over elements of a collection, returning the first element that - * the callback returns truey for. The callback is bound to `thisArg` and - * invoked with three arguments; (value, index|key, collection). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @alias detect, findWhere - * @category Collections - * @param {Array|Object|string} collection The collection to iterate over. - * @param {Function|Object|string} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {*} Returns the found element, else `undefined`. - * @example - * - * var characters = [ - * { 'name': 'barney', 'age': 36, 'blocked': false }, - * { 'name': 'fred', 'age': 40, 'blocked': true }, - * { 'name': 'pebbles', 'age': 1, 'blocked': false } - * ]; - * - * _.find(characters, function(chr) { - * return chr.age < 40; - * }); - * // => { 'name': 'barney', 'age': 36, 'blocked': false } - * - * // using "_.where" callback shorthand - * _.find(characters, { 'age': 1 }); - * // => { 'name': 'pebbles', 'age': 1, 'blocked': false } - * - * // using "_.pluck" callback shorthand - * _.find(characters, 'blocked'); - * // => { 'name': 'fred', 'age': 40, 'blocked': true } - */ - function find(collection, callback, thisArg) { - callback = lodash.createCallback(callback, thisArg, 3); - - if (isArray(collection)) { - var index = -1, - length = collection.length; - - while (++index < length) { - var value = collection[index]; - if (callback(value, index, collection)) { - return value; - } - } - } else { - var result; - baseEach(collection, function(value, index, collection) { - if (callback(value, index, collection)) { - result = value; - return false; - } - }); - return result; - } - } - - /** - * This method is like `_.find` except that it iterates over elements - * of a `collection` from right to left. - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|string} collection The collection to iterate over. - * @param {Function|Object|string} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {*} Returns the found element, else `undefined`. - * @example - * - * _.findLast([1, 2, 3, 4], function(num) { - * return num % 2 == 1; - * }); - * // => 3 - */ - function findLast(collection, callback, thisArg) { - var result; - callback = lodash.createCallback(callback, thisArg, 3); - forEachRight(collection, function(value, index, collection) { - if (callback(value, index, collection)) { - result = value; - return false; - } - }); - return result; - } - - /** - * Iterates over elements of a collection, executing the callback for each - * element. The callback is bound to `thisArg` and invoked with three arguments; - * (value, index|key, collection). Callbacks may exit iteration early by - * explicitly returning `false`. - * - * Note: As with other "Collections" methods, objects with a `length` property - * are iterated like arrays. To avoid this behavior `_.forIn` or `_.forOwn` - * may be used for object iteration. - * - * @static - * @memberOf _ - * @alias each - * @category Collections - * @param {Array|Object|string} collection The collection to iterate over. - * @param {Function} [callback=identity] The function called per iteration. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {Array|Object|string} Returns `collection`. - * @example - * - * _([1, 2, 3]).forEach(function(num) { console.log(num); }).join(','); - * // => logs each number and returns '1,2,3' - * - * _.forEach({ 'one': 1, 'two': 2, 'three': 3 }, function(num) { console.log(num); }); - * // => logs each number and returns the object (property order is not guaranteed across environments) - */ - function forEach(collection, callback, thisArg) { - if (callback && typeof thisArg == 'undefined' && isArray(collection)) { - var index = -1, - length = collection.length; - - while (++index < length) { - if (callback(collection[index], index, collection) === false) { - break; - } - } - } else { - baseEach(collection, callback, thisArg); - } - return collection; - } - - /** - * This method is like `_.forEach` except that it iterates over elements - * of a `collection` from right to left. - * - * @static - * @memberOf _ - * @alias eachRight - * @category Collections - * @param {Array|Object|string} collection The collection to iterate over. - * @param {Function} [callback=identity] The function called per iteration. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {Array|Object|string} Returns `collection`. - * @example - * - * _([1, 2, 3]).forEachRight(function(num) { console.log(num); }).join(','); - * // => logs each number from right to left and returns '3,2,1' - */ - function forEachRight(collection, callback, thisArg) { - var iterable = collection, - length = collection ? collection.length : 0; - - callback = callback && typeof thisArg == 'undefined' ? callback : baseCreateCallback(callback, thisArg, 3); - if (isArray(collection)) { - while (length--) { - if (callback(collection[length], length, collection) === false) { - break; - } - } - } else { - if (typeof length != 'number') { - var props = keys(collection); - length = props.length; - } else if (support.unindexedChars && isString(collection)) { - iterable = collection.split(''); - } - baseEach(collection, function(value, key, collection) { - key = props ? props[--length] : --length; - return callback(iterable[key], key, collection); - }); - } - return collection; - } - - /** - * Creates an object composed of keys generated from the results of running - * each element of a collection through the callback. The corresponding value - * of each key is an array of the elements responsible for generating the key. - * The callback is bound to `thisArg` and invoked with three arguments; - * (value, index|key, collection). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false` - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|string} collection The collection to iterate over. - * @param {Function|Object|string} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {Object} Returns the composed aggregate object. - * @example - * - * _.groupBy([4.2, 6.1, 6.4], function(num) { return Math.floor(num); }); - * // => { '4': [4.2], '6': [6.1, 6.4] } - * - * _.groupBy([4.2, 6.1, 6.4], function(num) { return this.floor(num); }, Math); - * // => { '4': [4.2], '6': [6.1, 6.4] } - * - * // using "_.pluck" callback shorthand - * _.groupBy(['one', 'two', 'three'], 'length'); - * // => { '3': ['one', 'two'], '5': ['three'] } - */ - var groupBy = createAggregator(function(result, value, key) { - (hasOwnProperty.call(result, key) ? result[key] : result[key] = []).push(value); - }); - - /** - * Creates an object composed of keys generated from the results of running - * each element of the collection through the given callback. The corresponding - * value of each key is the last element responsible for generating the key. - * The callback is bound to `thisArg` and invoked with three arguments; - * (value, index|key, collection). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|string} collection The collection to iterate over. - * @param {Function|Object|string} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {Object} Returns the composed aggregate object. - * @example - * - * var keys = [ - * { 'dir': 'left', 'code': 97 }, - * { 'dir': 'right', 'code': 100 } - * ]; - * - * _.indexBy(keys, 'dir'); - * // => { 'left': { 'dir': 'left', 'code': 97 }, 'right': { 'dir': 'right', 'code': 100 } } - * - * _.indexBy(keys, function(key) { return String.fromCharCode(key.code); }); - * // => { 'a': { 'dir': 'left', 'code': 97 }, 'd': { 'dir': 'right', 'code': 100 } } - * - * _.indexBy(characters, function(key) { this.fromCharCode(key.code); }, String); - * // => { 'a': { 'dir': 'left', 'code': 97 }, 'd': { 'dir': 'right', 'code': 100 } } - */ - var indexBy = createAggregator(function(result, value, key) { - result[key] = value; - }); - - /** - * Invokes the method named by `methodName` on each element in the `collection` - * returning an array of the results of each invoked method. Additional arguments - * will be provided to each invoked method. If `methodName` is a function it - * will be invoked for, and `this` bound to, each element in the `collection`. - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|string} collection The collection to iterate over. - * @param {Function|string} methodName The name of the method to invoke or - * the function invoked per iteration. - * @param {...*} [arg] Arguments to invoke the method with. - * @returns {Array} Returns a new array of the results of each invoked method. - * @example - * - * _.invoke([[5, 1, 7], [3, 2, 1]], 'sort'); - * // => [[1, 5, 7], [1, 2, 3]] - * - * _.invoke([123, 456], String.prototype.split, ''); - * // => [['1', '2', '3'], ['4', '5', '6']] - */ - function invoke(collection, methodName) { - var args = slice(arguments, 2), - index = -1, - isFunc = typeof methodName == 'function', - length = collection ? collection.length : 0, - result = Array(typeof length == 'number' ? length : 0); - - forEach(collection, function(value) { - result[++index] = (isFunc ? methodName : value[methodName]).apply(value, args); - }); - return result; - } - - /** - * Creates an array of values by running each element in the collection - * through the callback. The callback is bound to `thisArg` and invoked with - * three arguments; (value, index|key, collection). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @alias collect - * @category Collections - * @param {Array|Object|string} collection The collection to iterate over. - * @param {Function|Object|string} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {Array} Returns a new array of the results of each `callback` execution. - * @example - * - * _.map([1, 2, 3], function(num) { return num * 3; }); - * // => [3, 6, 9] - * - * _.map({ 'one': 1, 'two': 2, 'three': 3 }, function(num) { return num * 3; }); - * // => [3, 6, 9] (property order is not guaranteed across environments) - * - * var characters = [ - * { 'name': 'barney', 'age': 36 }, - * { 'name': 'fred', 'age': 40 } - * ]; - * - * // using "_.pluck" callback shorthand - * _.map(characters, 'name'); - * // => ['barney', 'fred'] - */ - function map(collection, callback, thisArg) { - var index = -1, - length = collection ? collection.length : 0, - result = Array(typeof length == 'number' ? length : 0); - - callback = lodash.createCallback(callback, thisArg, 3); - if (isArray(collection)) { - while (++index < length) { - result[index] = callback(collection[index], index, collection); - } - } else { - baseEach(collection, function(value, key, collection) { - result[++index] = callback(value, key, collection); - }); - } - return result; - } - - /** - * Retrieves the maximum value of a collection. If the collection is empty or - * falsey `-Infinity` is returned. If a callback is provided it will be executed - * for each value in the collection to generate the criterion by which the value - * is ranked. The callback is bound to `thisArg` and invoked with three - * arguments; (value, index, collection). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|string} collection The collection to iterate over. - * @param {Function|Object|string} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {*} Returns the maximum value. - * @example - * - * _.max([4, 2, 8, 6]); - * // => 8 - * - * var characters = [ - * { 'name': 'barney', 'age': 36 }, - * { 'name': 'fred', 'age': 40 } - * ]; - * - * _.max(characters, function(chr) { return chr.age; }); - * // => { 'name': 'fred', 'age': 40 }; - * - * // using "_.pluck" callback shorthand - * _.max(characters, 'age'); - * // => { 'name': 'fred', 'age': 40 }; - */ - function max(collection, callback, thisArg) { - var computed = -Infinity, - result = computed; - - // allows working with functions like `_.map` without using - // their `index` argument as a callback - if (typeof callback != 'function' && thisArg && thisArg[callback] === collection) { - callback = null; - } - if (callback == null && isArray(collection)) { - var index = -1, - length = collection.length; - - while (++index < length) { - var value = collection[index]; - if (value > result) { - result = value; - } - } - } else { - callback = (callback == null && isString(collection)) - ? charAtCallback - : lodash.createCallback(callback, thisArg, 3); - - baseEach(collection, function(value, index, collection) { - var current = callback(value, index, collection); - if (current > computed) { - computed = current; - result = value; - } - }); - } - return result; - } - - /** - * Retrieves the minimum value of a collection. If the collection is empty or - * falsey `Infinity` is returned. If a callback is provided it will be executed - * for each value in the collection to generate the criterion by which the value - * is ranked. The callback is bound to `thisArg` and invoked with three - * arguments; (value, index, collection). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|string} collection The collection to iterate over. - * @param {Function|Object|string} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {*} Returns the minimum value. - * @example - * - * _.min([4, 2, 8, 6]); - * // => 2 - * - * var characters = [ - * { 'name': 'barney', 'age': 36 }, - * { 'name': 'fred', 'age': 40 } - * ]; - * - * _.min(characters, function(chr) { return chr.age; }); - * // => { 'name': 'barney', 'age': 36 }; - * - * // using "_.pluck" callback shorthand - * _.min(characters, 'age'); - * // => { 'name': 'barney', 'age': 36 }; - */ - function min(collection, callback, thisArg) { - var computed = Infinity, - result = computed; - - // allows working with functions like `_.map` without using - // their `index` argument as a callback - if (typeof callback != 'function' && thisArg && thisArg[callback] === collection) { - callback = null; - } - if (callback == null && isArray(collection)) { - var index = -1, - length = collection.length; - - while (++index < length) { - var value = collection[index]; - if (value < result) { - result = value; - } - } - } else { - callback = (callback == null && isString(collection)) - ? charAtCallback - : lodash.createCallback(callback, thisArg, 3); - - baseEach(collection, function(value, index, collection) { - var current = callback(value, index, collection); - if (current < computed) { - computed = current; - result = value; - } - }); - } - return result; - } - - /** - * Retrieves the value of a specified property from all elements in the collection. - * - * @static - * @memberOf _ - * @type Function - * @category Collections - * @param {Array|Object|string} collection The collection to iterate over. - * @param {string} property The name of the property to pluck. - * @returns {Array} Returns a new array of property values. - * @example - * - * var characters = [ - * { 'name': 'barney', 'age': 36 }, - * { 'name': 'fred', 'age': 40 } - * ]; - * - * _.pluck(characters, 'name'); - * // => ['barney', 'fred'] - */ - var pluck = map; - - /** - * Reduces a collection to a value which is the accumulated result of running - * each element in the collection through the callback, where each successive - * callback execution consumes the return value of the previous execution. If - * `accumulator` is not provided the first element of the collection will be - * used as the initial `accumulator` value. The callback is bound to `thisArg` - * and invoked with four arguments; (accumulator, value, index|key, collection). - * - * @static - * @memberOf _ - * @alias foldl, inject - * @category Collections - * @param {Array|Object|string} collection The collection to iterate over. - * @param {Function} [callback=identity] The function called per iteration. - * @param {*} [accumulator] Initial value of the accumulator. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {*} Returns the accumulated value. - * @example - * - * var sum = _.reduce([1, 2, 3], function(sum, num) { - * return sum + num; - * }); - * // => 6 - * - * var mapped = _.reduce({ 'a': 1, 'b': 2, 'c': 3 }, function(result, num, key) { - * result[key] = num * 3; - * return result; - * }, {}); - * // => { 'a': 3, 'b': 6, 'c': 9 } - */ - function reduce(collection, callback, accumulator, thisArg) { - var noaccum = arguments.length < 3; - callback = lodash.createCallback(callback, thisArg, 4); - - if (isArray(collection)) { - var index = -1, - length = collection.length; - - if (noaccum) { - accumulator = collection[++index]; - } - while (++index < length) { - accumulator = callback(accumulator, collection[index], index, collection); - } - } else { - baseEach(collection, function(value, index, collection) { - accumulator = noaccum - ? (noaccum = false, value) - : callback(accumulator, value, index, collection) - }); - } - return accumulator; - } - - /** - * This method is like `_.reduce` except that it iterates over elements - * of a `collection` from right to left. - * - * @static - * @memberOf _ - * @alias foldr - * @category Collections - * @param {Array|Object|string} collection The collection to iterate over. - * @param {Function} [callback=identity] The function called per iteration. - * @param {*} [accumulator] Initial value of the accumulator. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {*} Returns the accumulated value. - * @example - * - * var list = [[0, 1], [2, 3], [4, 5]]; - * var flat = _.reduceRight(list, function(a, b) { return a.concat(b); }, []); - * // => [4, 5, 2, 3, 0, 1] - */ - function reduceRight(collection, callback, accumulator, thisArg) { - var noaccum = arguments.length < 3; - callback = lodash.createCallback(callback, thisArg, 4); - forEachRight(collection, function(value, index, collection) { - accumulator = noaccum - ? (noaccum = false, value) - : callback(accumulator, value, index, collection); - }); - return accumulator; - } - - /** - * The opposite of `_.filter` this method returns the elements of a - * collection that the callback does **not** return truey for. - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|string} collection The collection to iterate over. - * @param {Function|Object|string} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {Array} Returns a new array of elements that failed the callback check. - * @example - * - * var odds = _.reject([1, 2, 3, 4, 5, 6], function(num) { return num % 2 == 0; }); - * // => [1, 3, 5] - * - * var characters = [ - * { 'name': 'barney', 'age': 36, 'blocked': false }, - * { 'name': 'fred', 'age': 40, 'blocked': true } - * ]; - * - * // using "_.pluck" callback shorthand - * _.reject(characters, 'blocked'); - * // => [{ 'name': 'barney', 'age': 36, 'blocked': false }] - * - * // using "_.where" callback shorthand - * _.reject(characters, { 'age': 36 }); - * // => [{ 'name': 'fred', 'age': 40, 'blocked': true }] - */ - function reject(collection, callback, thisArg) { - callback = lodash.createCallback(callback, thisArg, 3); - return filter(collection, function(value, index, collection) { - return !callback(value, index, collection); - }); - } - - /** - * Retrieves a random element or `n` random elements from a collection. - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|string} collection The collection to sample. - * @param {number} [n] The number of elements to sample. - * @param- {Object} [guard] Allows working with functions like `_.map` - * without using their `index` arguments as `n`. - * @returns {Array} Returns the random sample(s) of `collection`. - * @example - * - * _.sample([1, 2, 3, 4]); - * // => 2 - * - * _.sample([1, 2, 3, 4], 2); - * // => [3, 1] - */ - function sample(collection, n, guard) { - if (collection && typeof collection.length != 'number') { - collection = values(collection); - } else if (support.unindexedChars && isString(collection)) { - collection = collection.split(''); - } - if (n == null || guard) { - return collection ? collection[baseRandom(0, collection.length - 1)] : undefined; - } - var result = shuffle(collection); - result.length = nativeMin(nativeMax(0, n), result.length); - return result; - } - - /** - * Creates an array of shuffled values, using a version of the Fisher-Yates - * shuffle. See http://en.wikipedia.org/wiki/Fisher-Yates_shuffle. - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|string} collection The collection to shuffle. - * @returns {Array} Returns a new shuffled collection. - * @example - * - * _.shuffle([1, 2, 3, 4, 5, 6]); - * // => [4, 1, 6, 3, 5, 2] - */ - function shuffle(collection) { - var index = -1, - length = collection ? collection.length : 0, - result = Array(typeof length == 'number' ? length : 0); - - forEach(collection, function(value) { - var rand = baseRandom(0, ++index); - result[index] = result[rand]; - result[rand] = value; - }); - return result; - } - - /** - * Gets the size of the `collection` by returning `collection.length` for arrays - * and array-like objects or the number of own enumerable properties for objects. - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|string} collection The collection to inspect. - * @returns {number} Returns `collection.length` or number of own enumerable properties. - * @example - * - * _.size([1, 2]); - * // => 2 - * - * _.size({ 'one': 1, 'two': 2, 'three': 3 }); - * // => 3 - * - * _.size('pebbles'); - * // => 7 - */ - function size(collection) { - var length = collection ? collection.length : 0; - return typeof length == 'number' ? length : keys(collection).length; - } - - /** - * Checks if the callback returns a truey value for **any** element of a - * collection. The function returns as soon as it finds a passing value and - * does not iterate over the entire collection. The callback is bound to - * `thisArg` and invoked with three arguments; (value, index|key, collection). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @alias any - * @category Collections - * @param {Array|Object|string} collection The collection to iterate over. - * @param {Function|Object|string} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {boolean} Returns `true` if any element passed the callback check, - * else `false`. - * @example - * - * _.some([null, 0, 'yes', false], Boolean); - * // => true - * - * var characters = [ - * { 'name': 'barney', 'age': 36, 'blocked': false }, - * { 'name': 'fred', 'age': 40, 'blocked': true } - * ]; - * - * // using "_.pluck" callback shorthand - * _.some(characters, 'blocked'); - * // => true - * - * // using "_.where" callback shorthand - * _.some(characters, { 'age': 1 }); - * // => false - */ - function some(collection, callback, thisArg) { - var result; - callback = lodash.createCallback(callback, thisArg, 3); - - if (isArray(collection)) { - var index = -1, - length = collection.length; - - while (++index < length) { - if ((result = callback(collection[index], index, collection))) { - break; - } - } - } else { - baseEach(collection, function(value, index, collection) { - return !(result = callback(value, index, collection)); - }); - } - return !!result; - } - - /** - * Creates an array of elements, sorted in ascending order by the results of - * running each element in a collection through the callback. This method - * performs a stable sort, that is, it will preserve the original sort order - * of equal elements. The callback is bound to `thisArg` and invoked with - * three arguments; (value, index|key, collection). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an array of property names is provided for `callback` the collection - * will be sorted by each property value. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|string} collection The collection to iterate over. - * @param {Array|Function|Object|string} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {Array} Returns a new array of sorted elements. - * @example - * - * _.sortBy([1, 2, 3], function(num) { return Math.sin(num); }); - * // => [3, 1, 2] - * - * _.sortBy([1, 2, 3], function(num) { return this.sin(num); }, Math); - * // => [3, 1, 2] - * - * var characters = [ - * { 'name': 'barney', 'age': 36 }, - * { 'name': 'fred', 'age': 40 }, - * { 'name': 'barney', 'age': 26 }, - * { 'name': 'fred', 'age': 30 } - * ]; - * - * // using "_.pluck" callback shorthand - * _.map(_.sortBy(characters, 'age'), _.values); - * // => [['barney', 26], ['fred', 30], ['barney', 36], ['fred', 40]] - * - * // sorting by multiple properties - * _.map(_.sortBy(characters, ['name', 'age']), _.values); - * // = > [['barney', 26], ['barney', 36], ['fred', 30], ['fred', 40]] - */ - function sortBy(collection, callback, thisArg) { - var index = -1, - isArr = isArray(callback), - length = collection ? collection.length : 0, - result = Array(typeof length == 'number' ? length : 0); - - if (!isArr) { - callback = lodash.createCallback(callback, thisArg, 3); - } - forEach(collection, function(value, key, collection) { - var object = result[++index] = getObject(); - if (isArr) { - object.criteria = map(callback, function(key) { return value[key]; }); - } else { - (object.criteria = getArray())[0] = callback(value, key, collection); - } - object.index = index; - object.value = value; - }); - - length = result.length; - result.sort(compareAscending); - while (length--) { - var object = result[length]; - result[length] = object.value; - if (!isArr) { - releaseArray(object.criteria); - } - releaseObject(object); - } - return result; - } - - /** - * Converts the `collection` to an array. - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|string} collection The collection to convert. - * @returns {Array} Returns the new converted array. - * @example - * - * (function() { return _.toArray(arguments).slice(1); })(1, 2, 3, 4); - * // => [2, 3, 4] - */ - function toArray(collection) { - if (collection && typeof collection.length == 'number') { - return (support.unindexedChars && isString(collection)) - ? collection.split('') - : slice(collection); - } - return values(collection); - } - - /** - * Performs a deep comparison of each element in a `collection` to the given - * `properties` object, returning an array of all elements that have equivalent - * property values. - * - * @static - * @memberOf _ - * @type Function - * @category Collections - * @param {Array|Object|string} collection The collection to iterate over. - * @param {Object} props The object of property values to filter by. - * @returns {Array} Returns a new array of elements that have the given properties. - * @example - * - * var characters = [ - * { 'name': 'barney', 'age': 36, 'pets': ['hoppy'] }, - * { 'name': 'fred', 'age': 40, 'pets': ['baby puss', 'dino'] } - * ]; - * - * _.where(characters, { 'age': 36 }); - * // => [{ 'name': 'barney', 'age': 36, 'pets': ['hoppy'] }] - * - * _.where(characters, { 'pets': ['dino'] }); - * // => [{ 'name': 'fred', 'age': 40, 'pets': ['baby puss', 'dino'] }] - */ - var where = filter; - - /*--------------------------------------------------------------------------*/ - - /** - * Creates an array with all falsey values removed. The values `false`, `null`, - * `0`, `""`, `undefined`, and `NaN` are all falsey. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} array The array to compact. - * @returns {Array} Returns a new array of filtered values. - * @example - * - * _.compact([0, 1, false, 2, '', 3]); - * // => [1, 2, 3] - */ - function compact(array) { - var index = -1, - length = array ? array.length : 0, - result = []; - - while (++index < length) { - var value = array[index]; - if (value) { - result.push(value); - } - } - return result; - } - - /** - * Creates an array excluding all values of the provided arrays using strict - * equality for comparisons, i.e. `===`. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} array The array to process. - * @param {...Array} [values] The arrays of values to exclude. - * @returns {Array} Returns a new array of filtered values. - * @example - * - * _.difference([1, 2, 3, 4, 5], [5, 2, 10]); - * // => [1, 3, 4] - */ - function difference(array) { - return baseDifference(array, baseFlatten(arguments, true, true, 1)); - } - - /** - * This method is like `_.find` except that it returns the index of the first - * element that passes the callback check, instead of the element itself. - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} array The array to search. - * @param {Function|Object|string} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {number} Returns the index of the found element, else `-1`. - * @example - * - * var characters = [ - * { 'name': 'barney', 'age': 36, 'blocked': false }, - * { 'name': 'fred', 'age': 40, 'blocked': true }, - * { 'name': 'pebbles', 'age': 1, 'blocked': false } - * ]; - * - * _.findIndex(characters, function(chr) { - * return chr.age < 20; - * }); - * // => 2 - * - * // using "_.where" callback shorthand - * _.findIndex(characters, { 'age': 36 }); - * // => 0 - * - * // using "_.pluck" callback shorthand - * _.findIndex(characters, 'blocked'); - * // => 1 - */ - function findIndex(array, callback, thisArg) { - var index = -1, - length = array ? array.length : 0; - - callback = lodash.createCallback(callback, thisArg, 3); - while (++index < length) { - if (callback(array[index], index, array)) { - return index; - } - } - return -1; - } - - /** - * This method is like `_.findIndex` except that it iterates over elements - * of a `collection` from right to left. - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} array The array to search. - * @param {Function|Object|string} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {number} Returns the index of the found element, else `-1`. - * @example - * - * var characters = [ - * { 'name': 'barney', 'age': 36, 'blocked': true }, - * { 'name': 'fred', 'age': 40, 'blocked': false }, - * { 'name': 'pebbles', 'age': 1, 'blocked': true } - * ]; - * - * _.findLastIndex(characters, function(chr) { - * return chr.age > 30; - * }); - * // => 1 - * - * // using "_.where" callback shorthand - * _.findLastIndex(characters, { 'age': 36 }); - * // => 0 - * - * // using "_.pluck" callback shorthand - * _.findLastIndex(characters, 'blocked'); - * // => 2 - */ - function findLastIndex(array, callback, thisArg) { - var length = array ? array.length : 0; - callback = lodash.createCallback(callback, thisArg, 3); - while (length--) { - if (callback(array[length], length, array)) { - return length; - } - } - return -1; - } - - /** - * Gets the first element or first `n` elements of an array. If a callback - * is provided elements at the beginning of the array are returned as long - * as the callback returns truey. The callback is bound to `thisArg` and - * invoked with three arguments; (value, index, array). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @alias head, take - * @category Arrays - * @param {Array} array The array to query. - * @param {Function|Object|number|string} [callback] The function called - * per element or the number of elements to return. If a property name or - * object is provided it will be used to create a "_.pluck" or "_.where" - * style callback, respectively. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {*} Returns the first element(s) of `array`. - * @example - * - * _.first([1, 2, 3]); - * // => 1 - * - * _.first([1, 2, 3], 2); - * // => [1, 2] - * - * _.first([1, 2, 3], function(num) { - * return num < 3; - * }); - * // => [1, 2] - * - * var characters = [ - * { 'name': 'barney', 'blocked': true, 'employer': 'slate' }, - * { 'name': 'fred', 'blocked': false, 'employer': 'slate' }, - * { 'name': 'pebbles', 'blocked': true, 'employer': 'na' } - * ]; - * - * // using "_.pluck" callback shorthand - * _.first(characters, 'blocked'); - * // => [{ 'name': 'barney', 'blocked': true, 'employer': 'slate' }] - * - * // using "_.where" callback shorthand - * _.pluck(_.first(characters, { 'employer': 'slate' }), 'name'); - * // => ['barney', 'fred'] - */ - function first(array, callback, thisArg) { - var n = 0, - length = array ? array.length : 0; - - if (typeof callback != 'number' && callback != null) { - var index = -1; - callback = lodash.createCallback(callback, thisArg, 3); - while (++index < length && callback(array[index], index, array)) { - n++; - } - } else { - n = callback; - if (n == null || thisArg) { - return array ? array[0] : undefined; - } - } - return slice(array, 0, nativeMin(nativeMax(0, n), length)); - } - - /** - * Flattens a nested array (the nesting can be to any depth). If `isShallow` - * is truey, the array will only be flattened a single level. If a callback - * is provided each element of the array is passed through the callback before - * flattening. The callback is bound to `thisArg` and invoked with three - * arguments; (value, index, array). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} array The array to flatten. - * @param {boolean} [isShallow=false] A flag to restrict flattening to a single level. - * @param {Function|Object|string} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {Array} Returns a new flattened array. - * @example - * - * _.flatten([1, [2], [3, [[4]]]]); - * // => [1, 2, 3, 4]; - * - * _.flatten([1, [2], [3, [[4]]]], true); - * // => [1, 2, 3, [[4]]]; - * - * var characters = [ - * { 'name': 'barney', 'age': 30, 'pets': ['hoppy'] }, - * { 'name': 'fred', 'age': 40, 'pets': ['baby puss', 'dino'] } - * ]; - * - * // using "_.pluck" callback shorthand - * _.flatten(characters, 'pets'); - * // => ['hoppy', 'baby puss', 'dino'] - */ - function flatten(array, isShallow, callback, thisArg) { - // juggle arguments - if (typeof isShallow != 'boolean' && isShallow != null) { - thisArg = callback; - callback = (typeof isShallow != 'function' && thisArg && thisArg[isShallow] === array) ? null : isShallow; - isShallow = false; - } - if (callback != null) { - array = map(array, callback, thisArg); - } - return baseFlatten(array, isShallow); - } - - /** - * Gets the index at which the first occurrence of `value` is found using - * strict equality for comparisons, i.e. `===`. If the array is already sorted - * providing `true` for `fromIndex` will run a faster binary search. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} array The array to search. - * @param {*} value The value to search for. - * @param {boolean|number} [fromIndex=0] The index to search from or `true` - * to perform a binary search on a sorted array. - * @returns {number} Returns the index of the matched value or `-1`. - * @example - * - * _.indexOf([1, 2, 3, 1, 2, 3], 2); - * // => 1 - * - * _.indexOf([1, 2, 3, 1, 2, 3], 2, 3); - * // => 4 - * - * _.indexOf([1, 1, 2, 2, 3, 3], 2, true); - * // => 2 - */ - function indexOf(array, value, fromIndex) { - if (typeof fromIndex == 'number') { - var length = array ? array.length : 0; - fromIndex = (fromIndex < 0 ? nativeMax(0, length + fromIndex) : fromIndex || 0); - } else if (fromIndex) { - var index = sortedIndex(array, value); - return array[index] === value ? index : -1; - } - return baseIndexOf(array, value, fromIndex); - } - - /** - * Gets all but the last element or last `n` elements of an array. If a - * callback is provided elements at the end of the array are excluded from - * the result as long as the callback returns truey. The callback is bound - * to `thisArg` and invoked with three arguments; (value, index, array). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} array The array to query. - * @param {Function|Object|number|string} [callback=1] The function called - * per element or the number of elements to exclude. If a property name or - * object is provided it will be used to create a "_.pluck" or "_.where" - * style callback, respectively. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {Array} Returns a slice of `array`. - * @example - * - * _.initial([1, 2, 3]); - * // => [1, 2] - * - * _.initial([1, 2, 3], 2); - * // => [1] - * - * _.initial([1, 2, 3], function(num) { - * return num > 1; - * }); - * // => [1] - * - * var characters = [ - * { 'name': 'barney', 'blocked': false, 'employer': 'slate' }, - * { 'name': 'fred', 'blocked': true, 'employer': 'slate' }, - * { 'name': 'pebbles', 'blocked': true, 'employer': 'na' } - * ]; - * - * // using "_.pluck" callback shorthand - * _.initial(characters, 'blocked'); - * // => [{ 'name': 'barney', 'blocked': false, 'employer': 'slate' }] - * - * // using "_.where" callback shorthand - * _.pluck(_.initial(characters, { 'employer': 'na' }), 'name'); - * // => ['barney', 'fred'] - */ - function initial(array, callback, thisArg) { - var n = 0, - length = array ? array.length : 0; - - if (typeof callback != 'number' && callback != null) { - var index = length; - callback = lodash.createCallback(callback, thisArg, 3); - while (index-- && callback(array[index], index, array)) { - n++; - } - } else { - n = (callback == null || thisArg) ? 1 : callback || n; - } - return slice(array, 0, nativeMin(nativeMax(0, length - n), length)); - } - - /** - * Creates an array of unique values present in all provided arrays using - * strict equality for comparisons, i.e. `===`. - * - * @static - * @memberOf _ - * @category Arrays - * @param {...Array} [array] The arrays to inspect. - * @returns {Array} Returns an array of shared values. - * @example - * - * _.intersection([1, 2, 3], [5, 2, 1, 4], [2, 1]); - * // => [1, 2] - */ - function intersection() { - var args = [], - argsIndex = -1, - argsLength = arguments.length, - caches = getArray(), - indexOf = getIndexOf(), - trustIndexOf = indexOf === baseIndexOf, - seen = getArray(); - - while (++argsIndex < argsLength) { - var value = arguments[argsIndex]; - if (isArray(value) || isArguments(value)) { - args.push(value); - caches.push(trustIndexOf && value.length >= largeArraySize && - createCache(argsIndex ? args[argsIndex] : seen)); - } - } - var array = args[0], - index = -1, - length = array ? array.length : 0, - result = []; - - outer: - while (++index < length) { - var cache = caches[0]; - value = array[index]; - - if ((cache ? cacheIndexOf(cache, value) : indexOf(seen, value)) < 0) { - argsIndex = argsLength; - (cache || seen).push(value); - while (--argsIndex) { - cache = caches[argsIndex]; - if ((cache ? cacheIndexOf(cache, value) : indexOf(args[argsIndex], value)) < 0) { - continue outer; - } - } - result.push(value); - } - } - while (argsLength--) { - cache = caches[argsLength]; - if (cache) { - releaseObject(cache); - } - } - releaseArray(caches); - releaseArray(seen); - return result; - } - - /** - * Gets the last element or last `n` elements of an array. If a callback is - * provided elements at the end of the array are returned as long as the - * callback returns truey. The callback is bound to `thisArg` and invoked - * with three arguments; (value, index, array). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} array The array to query. - * @param {Function|Object|number|string} [callback] The function called - * per element or the number of elements to return. If a property name or - * object is provided it will be used to create a "_.pluck" or "_.where" - * style callback, respectively. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {*} Returns the last element(s) of `array`. - * @example - * - * _.last([1, 2, 3]); - * // => 3 - * - * _.last([1, 2, 3], 2); - * // => [2, 3] - * - * _.last([1, 2, 3], function(num) { - * return num > 1; - * }); - * // => [2, 3] - * - * var characters = [ - * { 'name': 'barney', 'blocked': false, 'employer': 'slate' }, - * { 'name': 'fred', 'blocked': true, 'employer': 'slate' }, - * { 'name': 'pebbles', 'blocked': true, 'employer': 'na' } - * ]; - * - * // using "_.pluck" callback shorthand - * _.pluck(_.last(characters, 'blocked'), 'name'); - * // => ['fred', 'pebbles'] - * - * // using "_.where" callback shorthand - * _.last(characters, { 'employer': 'na' }); - * // => [{ 'name': 'pebbles', 'blocked': true, 'employer': 'na' }] - */ - function last(array, callback, thisArg) { - var n = 0, - length = array ? array.length : 0; - - if (typeof callback != 'number' && callback != null) { - var index = length; - callback = lodash.createCallback(callback, thisArg, 3); - while (index-- && callback(array[index], index, array)) { - n++; - } - } else { - n = callback; - if (n == null || thisArg) { - return array ? array[length - 1] : undefined; - } - } - return slice(array, nativeMax(0, length - n)); - } - - /** - * Gets the index at which the last occurrence of `value` is found using strict - * equality for comparisons, i.e. `===`. If `fromIndex` is negative, it is used - * as the offset from the end of the collection. - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} array The array to search. - * @param {*} value The value to search for. - * @param {number} [fromIndex=array.length-1] The index to search from. - * @returns {number} Returns the index of the matched value or `-1`. - * @example - * - * _.lastIndexOf([1, 2, 3, 1, 2, 3], 2); - * // => 4 - * - * _.lastIndexOf([1, 2, 3, 1, 2, 3], 2, 3); - * // => 1 - */ - function lastIndexOf(array, value, fromIndex) { - var index = array ? array.length : 0; - if (typeof fromIndex == 'number') { - index = (fromIndex < 0 ? nativeMax(0, index + fromIndex) : nativeMin(fromIndex, index - 1)) + 1; - } - while (index--) { - if (array[index] === value) { - return index; - } - } - return -1; - } - - /** - * Removes all provided values from the given array using strict equality for - * comparisons, i.e. `===`. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} array The array to modify. - * @param {...*} [value] The values to remove. - * @returns {Array} Returns `array`. - * @example - * - * var array = [1, 2, 3, 1, 2, 3]; - * _.pull(array, 2, 3); - * console.log(array); - * // => [1, 1] - */ - function pull(array) { - var args = arguments, - argsIndex = 0, - argsLength = args.length, - length = array ? array.length : 0; - - while (++argsIndex < argsLength) { - var index = -1, - value = args[argsIndex]; - while (++index < length) { - if (array[index] === value) { - splice.call(array, index--, 1); - length--; - } - } - } - return array; - } - - /** - * Creates an array of numbers (positive and/or negative) progressing from - * `start` up to but not including `end`. If `start` is less than `stop` a - * zero-length range is created unless a negative `step` is specified. - * - * @static - * @memberOf _ - * @category Arrays - * @param {number} [start=0] The start of the range. - * @param {number} end The end of the range. - * @param {number} [step=1] The value to increment or decrement by. - * @returns {Array} Returns a new range array. - * @example - * - * _.range(4); - * // => [0, 1, 2, 3] - * - * _.range(1, 5); - * // => [1, 2, 3, 4] - * - * _.range(0, 20, 5); - * // => [0, 5, 10, 15] - * - * _.range(0, -4, -1); - * // => [0, -1, -2, -3] - * - * _.range(1, 4, 0); - * // => [1, 1, 1] - * - * _.range(0); - * // => [] - */ - function range(start, end, step) { - start = +start || 0; - step = typeof step == 'number' ? step : (+step || 1); - - if (end == null) { - end = start; - start = 0; - } - // use `Array(length)` so engines like Chakra and V8 avoid slower modes - // http://youtu.be/XAqIpGU8ZZk#t=17m25s - var index = -1, - length = nativeMax(0, ceil((end - start) / (step || 1))), - result = Array(length); - - while (++index < length) { - result[index] = start; - start += step; - } - return result; - } - - /** - * Removes all elements from an array that the callback returns truey for - * and returns an array of removed elements. The callback is bound to `thisArg` - * and invoked with three arguments; (value, index, array). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} array The array to modify. - * @param {Function|Object|string} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {Array} Returns a new array of removed elements. - * @example - * - * var array = [1, 2, 3, 4, 5, 6]; - * var evens = _.remove(array, function(num) { return num % 2 == 0; }); - * - * console.log(array); - * // => [1, 3, 5] - * - * console.log(evens); - * // => [2, 4, 6] - */ - function remove(array, callback, thisArg) { - var index = -1, - length = array ? array.length : 0, - result = []; - - callback = lodash.createCallback(callback, thisArg, 3); - while (++index < length) { - var value = array[index]; - if (callback(value, index, array)) { - result.push(value); - splice.call(array, index--, 1); - length--; - } - } - return result; - } - - /** - * The opposite of `_.initial` this method gets all but the first element or - * first `n` elements of an array. If a callback function is provided elements - * at the beginning of the array are excluded from the result as long as the - * callback returns truey. The callback is bound to `thisArg` and invoked - * with three arguments; (value, index, array). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @alias drop, tail - * @category Arrays - * @param {Array} array The array to query. - * @param {Function|Object|number|string} [callback=1] The function called - * per element or the number of elements to exclude. If a property name or - * object is provided it will be used to create a "_.pluck" or "_.where" - * style callback, respectively. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {Array} Returns a slice of `array`. - * @example - * - * _.rest([1, 2, 3]); - * // => [2, 3] - * - * _.rest([1, 2, 3], 2); - * // => [3] - * - * _.rest([1, 2, 3], function(num) { - * return num < 3; - * }); - * // => [3] - * - * var characters = [ - * { 'name': 'barney', 'blocked': true, 'employer': 'slate' }, - * { 'name': 'fred', 'blocked': false, 'employer': 'slate' }, - * { 'name': 'pebbles', 'blocked': true, 'employer': 'na' } - * ]; - * - * // using "_.pluck" callback shorthand - * _.pluck(_.rest(characters, 'blocked'), 'name'); - * // => ['fred', 'pebbles'] - * - * // using "_.where" callback shorthand - * _.rest(characters, { 'employer': 'slate' }); - * // => [{ 'name': 'pebbles', 'blocked': true, 'employer': 'na' }] - */ - function rest(array, callback, thisArg) { - if (typeof callback != 'number' && callback != null) { - var n = 0, - index = -1, - length = array ? array.length : 0; - - callback = lodash.createCallback(callback, thisArg, 3); - while (++index < length && callback(array[index], index, array)) { - n++; - } - } else { - n = (callback == null || thisArg) ? 1 : nativeMax(0, callback); - } - return slice(array, n); - } - - /** - * Uses a binary search to determine the smallest index at which a value - * should be inserted into a given sorted array in order to maintain the sort - * order of the array. If a callback is provided it will be executed for - * `value` and each element of `array` to compute their sort ranking. The - * callback is bound to `thisArg` and invoked with one argument; (value). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} array The array to inspect. - * @param {*} value The value to evaluate. - * @param {Function|Object|string} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {number} Returns the index at which `value` should be inserted - * into `array`. - * @example - * - * _.sortedIndex([20, 30, 50], 40); - * // => 2 - * - * // using "_.pluck" callback shorthand - * _.sortedIndex([{ 'x': 20 }, { 'x': 30 }, { 'x': 50 }], { 'x': 40 }, 'x'); - * // => 2 - * - * var dict = { - * 'wordToNumber': { 'twenty': 20, 'thirty': 30, 'fourty': 40, 'fifty': 50 } - * }; - * - * _.sortedIndex(['twenty', 'thirty', 'fifty'], 'fourty', function(word) { - * return dict.wordToNumber[word]; - * }); - * // => 2 - * - * _.sortedIndex(['twenty', 'thirty', 'fifty'], 'fourty', function(word) { - * return this.wordToNumber[word]; - * }, dict); - * // => 2 - */ - function sortedIndex(array, value, callback, thisArg) { - var low = 0, - high = array ? array.length : low; - - // explicitly reference `identity` for better inlining in Firefox - callback = callback ? lodash.createCallback(callback, thisArg, 1) : identity; - value = callback(value); - - while (low < high) { - var mid = (low + high) >>> 1; - (callback(array[mid]) < value) - ? low = mid + 1 - : high = mid; - } - return low; - } - - /** - * Creates an array of unique values, in order, of the provided arrays using - * strict equality for comparisons, i.e. `===`. - * - * @static - * @memberOf _ - * @category Arrays - * @param {...Array} [array] The arrays to inspect. - * @returns {Array} Returns an array of combined values. - * @example - * - * _.union([1, 2, 3], [5, 2, 1, 4], [2, 1]); - * // => [1, 2, 3, 5, 4] - */ - function union() { - return baseUniq(baseFlatten(arguments, true, true)); - } - - /** - * Creates a duplicate-value-free version of an array using strict equality - * for comparisons, i.e. `===`. If the array is sorted, providing - * `true` for `isSorted` will use a faster algorithm. If a callback is provided - * each element of `array` is passed through the callback before uniqueness - * is computed. The callback is bound to `thisArg` and invoked with three - * arguments; (value, index, array). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @alias unique - * @category Arrays - * @param {Array} array The array to process. - * @param {boolean} [isSorted=false] A flag to indicate that `array` is sorted. - * @param {Function|Object|string} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {Array} Returns a duplicate-value-free array. - * @example - * - * _.uniq([1, 2, 1, 3, 1]); - * // => [1, 2, 3] - * - * _.uniq([1, 1, 2, 2, 3], true); - * // => [1, 2, 3] - * - * _.uniq(['A', 'b', 'C', 'a', 'B', 'c'], function(letter) { return letter.toLowerCase(); }); - * // => ['A', 'b', 'C'] - * - * _.uniq([1, 2.5, 3, 1.5, 2, 3.5], function(num) { return this.floor(num); }, Math); - * // => [1, 2.5, 3] - * - * // using "_.pluck" callback shorthand - * _.uniq([{ 'x': 1 }, { 'x': 2 }, { 'x': 1 }], 'x'); - * // => [{ 'x': 1 }, { 'x': 2 }] - */ - function uniq(array, isSorted, callback, thisArg) { - // juggle arguments - if (typeof isSorted != 'boolean' && isSorted != null) { - thisArg = callback; - callback = (typeof isSorted != 'function' && thisArg && thisArg[isSorted] === array) ? null : isSorted; - isSorted = false; - } - if (callback != null) { - callback = lodash.createCallback(callback, thisArg, 3); - } - return baseUniq(array, isSorted, callback); - } - - /** - * Creates an array excluding all provided values using strict equality for - * comparisons, i.e. `===`. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} array The array to filter. - * @param {...*} [value] The values to exclude. - * @returns {Array} Returns a new array of filtered values. - * @example - * - * _.without([1, 2, 1, 0, 3, 1, 4], 0, 1); - * // => [2, 3, 4] - */ - function without(array) { - return baseDifference(array, slice(arguments, 1)); - } - - /** - * Creates an array that is the symmetric difference of the provided arrays. - * See http://en.wikipedia.org/wiki/Symmetric_difference. - * - * @static - * @memberOf _ - * @category Arrays - * @param {...Array} [array] The arrays to inspect. - * @returns {Array} Returns an array of values. - * @example - * - * _.xor([1, 2, 3], [5, 2, 1, 4]); - * // => [3, 5, 4] - * - * _.xor([1, 2, 5], [2, 3, 5], [3, 4, 5]); - * // => [1, 4, 5] - */ - function xor() { - var index = -1, - length = arguments.length; - - while (++index < length) { - var array = arguments[index]; - if (isArray(array) || isArguments(array)) { - var result = result - ? baseUniq(baseDifference(result, array).concat(baseDifference(array, result))) - : array; - } - } - return result || []; - } - - /** - * Creates an array of grouped elements, the first of which contains the first - * elements of the given arrays, the second of which contains the second - * elements of the given arrays, and so on. - * - * @static - * @memberOf _ - * @alias unzip - * @category Arrays - * @param {...Array} [array] Arrays to process. - * @returns {Array} Returns a new array of grouped elements. - * @example - * - * _.zip(['fred', 'barney'], [30, 40], [true, false]); - * // => [['fred', 30, true], ['barney', 40, false]] - */ - function zip() { - var array = arguments.length > 1 ? arguments : arguments[0], - index = -1, - length = array ? max(pluck(array, 'length')) : 0, - result = Array(length < 0 ? 0 : length); - - while (++index < length) { - result[index] = pluck(array, index); - } - return result; - } - - /** - * Creates an object composed from arrays of `keys` and `values`. Provide - * either a single two dimensional array, i.e. `[[key1, value1], [key2, value2]]` - * or two arrays, one of `keys` and one of corresponding `values`. - * - * @static - * @memberOf _ - * @alias object - * @category Arrays - * @param {Array} keys The array of keys. - * @param {Array} [values=[]] The array of values. - * @returns {Object} Returns an object composed of the given keys and - * corresponding values. - * @example - * - * _.zipObject(['fred', 'barney'], [30, 40]); - * // => { 'fred': 30, 'barney': 40 } - */ - function zipObject(keys, values) { - var index = -1, - length = keys ? keys.length : 0, - result = {}; - - if (!values && length && !isArray(keys[0])) { - values = []; - } - while (++index < length) { - var key = keys[index]; - if (values) { - result[key] = values[index]; - } else if (key) { - result[key[0]] = key[1]; - } - } - return result; - } - - /*--------------------------------------------------------------------------*/ - - /** - * Creates a function that executes `func`, with the `this` binding and - * arguments of the created function, only after being called `n` times. - * - * @static - * @memberOf _ - * @category Functions - * @param {number} n The number of times the function must be called before - * `func` is executed. - * @param {Function} func The function to restrict. - * @returns {Function} Returns the new restricted function. - * @example - * - * var saves = ['profile', 'settings']; - * - * var done = _.after(saves.length, function() { - * console.log('Done saving!'); - * }); - * - * _.forEach(saves, function(type) { - * asyncSave({ 'type': type, 'complete': done }); - * }); - * // => logs 'Done saving!', after all saves have completed - */ - function after(n, func) { - if (!isFunction(func)) { - throw new TypeError; - } - return function() { - if (--n < 1) { - return func.apply(this, arguments); - } - }; - } - - /** - * Creates a function that, when called, invokes `func` with the `this` - * binding of `thisArg` and prepends any additional `bind` arguments to those - * provided to the bound function. - * - * @static - * @memberOf _ - * @category Functions - * @param {Function} func The function to bind. - * @param {*} [thisArg] The `this` binding of `func`. - * @param {...*} [arg] Arguments to be partially applied. - * @returns {Function} Returns the new bound function. - * @example - * - * var func = function(greeting) { - * return greeting + ' ' + this.name; - * }; - * - * func = _.bind(func, { 'name': 'fred' }, 'hi'); - * func(); - * // => 'hi fred' - */ - function bind(func, thisArg) { - return arguments.length > 2 - ? createWrapper(func, 17, slice(arguments, 2), null, thisArg) - : createWrapper(func, 1, null, null, thisArg); - } - - /** - * Binds methods of an object to the object itself, overwriting the existing - * method. Method names may be specified as individual arguments or as arrays - * of method names. If no method names are provided all the function properties - * of `object` will be bound. - * - * @static - * @memberOf _ - * @category Functions - * @param {Object} object The object to bind and assign the bound methods to. - * @param {...string} [methodName] The object method names to - * bind, specified as individual method names or arrays of method names. - * @returns {Object} Returns `object`. - * @example - * - * var view = { - * 'label': 'docs', - * 'onClick': function() { console.log('clicked ' + this.label); } - * }; - * - * _.bindAll(view); - * jQuery('#docs').on('click', view.onClick); - * // => logs 'clicked docs', when the button is clicked - */ - function bindAll(object) { - var funcs = arguments.length > 1 ? baseFlatten(arguments, true, false, 1) : functions(object), - index = -1, - length = funcs.length; - - while (++index < length) { - var key = funcs[index]; - object[key] = createWrapper(object[key], 1, null, null, object); - } - return object; - } - - /** - * Creates a function that, when called, invokes the method at `object[key]` - * and prepends any additional `bindKey` arguments to those provided to the bound - * function. This method differs from `_.bind` by allowing bound functions to - * reference methods that will be redefined or don't yet exist. - * See http://michaux.ca/articles/lazy-function-definition-pattern. - * - * @static - * @memberOf _ - * @category Functions - * @param {Object} object The object the method belongs to. - * @param {string} key The key of the method. - * @param {...*} [arg] Arguments to be partially applied. - * @returns {Function} Returns the new bound function. - * @example - * - * var object = { - * 'name': 'fred', - * 'greet': function(greeting) { - * return greeting + ' ' + this.name; - * } - * }; - * - * var func = _.bindKey(object, 'greet', 'hi'); - * func(); - * // => 'hi fred' - * - * object.greet = function(greeting) { - * return greeting + 'ya ' + this.name + '!'; - * }; - * - * func(); - * // => 'hiya fred!' - */ - function bindKey(object, key) { - return arguments.length > 2 - ? createWrapper(key, 19, slice(arguments, 2), null, object) - : createWrapper(key, 3, null, null, object); - } - - /** - * Creates a function that is the composition of the provided functions, - * where each function consumes the return value of the function that follows. - * For example, composing the functions `f()`, `g()`, and `h()` produces `f(g(h()))`. - * Each function is executed with the `this` binding of the composed function. - * - * @static - * @memberOf _ - * @category Functions - * @param {...Function} [func] Functions to compose. - * @returns {Function} Returns the new composed function. - * @example - * - * var realNameMap = { - * 'pebbles': 'penelope' - * }; - * - * var format = function(name) { - * name = realNameMap[name.toLowerCase()] || name; - * return name.charAt(0).toUpperCase() + name.slice(1).toLowerCase(); - * }; - * - * var greet = function(formatted) { - * return 'Hiya ' + formatted + '!'; - * }; - * - * var welcome = _.compose(greet, format); - * welcome('pebbles'); - * // => 'Hiya Penelope!' - */ - function compose() { - var funcs = arguments, - length = funcs.length; - - while (length--) { - if (!isFunction(funcs[length])) { - throw new TypeError; - } - } - return function() { - var args = arguments, - length = funcs.length; - - while (length--) { - args = [funcs[length].apply(this, args)]; - } - return args[0]; - }; - } - - /** - * Creates a function which accepts one or more arguments of `func` that when - * invoked either executes `func` returning its result, if all `func` arguments - * have been provided, or returns a function that accepts one or more of the - * remaining `func` arguments, and so on. The arity of `func` can be specified - * if `func.length` is not sufficient. - * - * @static - * @memberOf _ - * @category Functions - * @param {Function} func The function to curry. - * @param {number} [arity=func.length] The arity of `func`. - * @returns {Function} Returns the new curried function. - * @example - * - * var curried = _.curry(function(a, b, c) { - * console.log(a + b + c); - * }); - * - * curried(1)(2)(3); - * // => 6 - * - * curried(1, 2)(3); - * // => 6 - * - * curried(1, 2, 3); - * // => 6 - */ - function curry(func, arity) { - arity = typeof arity == 'number' ? arity : (+arity || func.length); - return createWrapper(func, 4, null, null, null, arity); - } - - /** - * Creates a function that will delay the execution of `func` until after - * `wait` milliseconds have elapsed since the last time it was invoked. - * Provide an options object to indicate that `func` should be invoked on - * the leading and/or trailing edge of the `wait` timeout. Subsequent calls - * to the debounced function will return the result of the last `func` call. - * - * Note: If `leading` and `trailing` options are `true` `func` will be called - * on the trailing edge of the timeout only if the the debounced function is - * invoked more than once during the `wait` timeout. - * - * @static - * @memberOf _ - * @category Functions - * @param {Function} func The function to debounce. - * @param {number} wait The number of milliseconds to delay. - * @param {Object} [options] The options object. - * @param {boolean} [options.leading=false] Specify execution on the leading edge of the timeout. - * @param {number} [options.maxWait] The maximum time `func` is allowed to be delayed before it's called. - * @param {boolean} [options.trailing=true] Specify execution on the trailing edge of the timeout. - * @returns {Function} Returns the new debounced function. - * @example - * - * // avoid costly calculations while the window size is in flux - * var lazyLayout = _.debounce(calculateLayout, 150); - * jQuery(window).on('resize', lazyLayout); - * - * // execute `sendMail` when the click event is fired, debouncing subsequent calls - * jQuery('#postbox').on('click', _.debounce(sendMail, 300, { - * 'leading': true, - * 'trailing': false - * }); - * - * // ensure `batchLog` is executed once after 1 second of debounced calls - * var source = new EventSource('/stream'); - * source.addEventListener('message', _.debounce(batchLog, 250, { - * 'maxWait': 1000 - * }, false); - */ - function debounce(func, wait, options) { - var args, - maxTimeoutId, - result, - stamp, - thisArg, - timeoutId, - trailingCall, - lastCalled = 0, - maxWait = false, - trailing = true; - - if (!isFunction(func)) { - throw new TypeError; - } - wait = nativeMax(0, wait) || 0; - if (options === true) { - var leading = true; - trailing = false; - } else if (isObject(options)) { - leading = options.leading; - maxWait = 'maxWait' in options && (nativeMax(wait, options.maxWait) || 0); - trailing = 'trailing' in options ? options.trailing : trailing; - } - var delayed = function() { - var remaining = wait - (now() - stamp); - if (remaining <= 0) { - if (maxTimeoutId) { - clearTimeout(maxTimeoutId); - } - var isCalled = trailingCall; - maxTimeoutId = timeoutId = trailingCall = undefined; - if (isCalled) { - lastCalled = now(); - result = func.apply(thisArg, args); - if (!timeoutId && !maxTimeoutId) { - args = thisArg = null; - } - } - } else { - timeoutId = setTimeout(delayed, remaining); - } - }; - - var maxDelayed = function() { - if (timeoutId) { - clearTimeout(timeoutId); - } - maxTimeoutId = timeoutId = trailingCall = undefined; - if (trailing || (maxWait !== wait)) { - lastCalled = now(); - result = func.apply(thisArg, args); - if (!timeoutId && !maxTimeoutId) { - args = thisArg = null; - } - } - }; - - return function() { - args = arguments; - stamp = now(); - thisArg = this; - trailingCall = trailing && (timeoutId || !leading); - - if (maxWait === false) { - var leadingCall = leading && !timeoutId; - } else { - if (!maxTimeoutId && !leading) { - lastCalled = stamp; - } - var remaining = maxWait - (stamp - lastCalled), - isCalled = remaining <= 0; - - if (isCalled) { - if (maxTimeoutId) { - maxTimeoutId = clearTimeout(maxTimeoutId); - } - lastCalled = stamp; - result = func.apply(thisArg, args); - } - else if (!maxTimeoutId) { - maxTimeoutId = setTimeout(maxDelayed, remaining); - } - } - if (isCalled && timeoutId) { - timeoutId = clearTimeout(timeoutId); - } - else if (!timeoutId && wait !== maxWait) { - timeoutId = setTimeout(delayed, wait); - } - if (leadingCall) { - isCalled = true; - result = func.apply(thisArg, args); - } - if (isCalled && !timeoutId && !maxTimeoutId) { - args = thisArg = null; - } - return result; - }; - } - - /** - * Defers executing the `func` function until the current call stack has cleared. - * Additional arguments will be provided to `func` when it is invoked. - * - * @static - * @memberOf _ - * @category Functions - * @param {Function} func The function to defer. - * @param {...*} [arg] Arguments to invoke the function with. - * @returns {number} Returns the timer id. - * @example - * - * _.defer(function(text) { console.log(text); }, 'deferred'); - * // logs 'deferred' after one or more milliseconds - */ - function defer(func) { - if (!isFunction(func)) { - throw new TypeError; - } - var args = slice(arguments, 1); - return setTimeout(function() { func.apply(undefined, args); }, 1); - } - - /** - * Executes the `func` function after `wait` milliseconds. Additional arguments - * will be provided to `func` when it is invoked. - * - * @static - * @memberOf _ - * @category Functions - * @param {Function} func The function to delay. - * @param {number} wait The number of milliseconds to delay execution. - * @param {...*} [arg] Arguments to invoke the function with. - * @returns {number} Returns the timer id. - * @example - * - * _.delay(function(text) { console.log(text); }, 1000, 'later'); - * // => logs 'later' after one second - */ - function delay(func, wait) { - if (!isFunction(func)) { - throw new TypeError; - } - var args = slice(arguments, 2); - return setTimeout(function() { func.apply(undefined, args); }, wait); - } - - /** - * Creates a function that memoizes the result of `func`. If `resolver` is - * provided it will be used to determine the cache key for storing the result - * based on the arguments provided to the memoized function. By default, the - * first argument provided to the memoized function is used as the cache key. - * The `func` is executed with the `this` binding of the memoized function. - * The result cache is exposed as the `cache` property on the memoized function. - * - * @static - * @memberOf _ - * @category Functions - * @param {Function} func The function to have its output memoized. - * @param {Function} [resolver] A function used to resolve the cache key. - * @returns {Function} Returns the new memoizing function. - * @example - * - * var fibonacci = _.memoize(function(n) { - * return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2); - * }); - * - * fibonacci(9) - * // => 34 - * - * var data = { - * 'fred': { 'name': 'fred', 'age': 40 }, - * 'pebbles': { 'name': 'pebbles', 'age': 1 } - * }; - * - * // modifying the result cache - * var get = _.memoize(function(name) { return data[name]; }, _.identity); - * get('pebbles'); - * // => { 'name': 'pebbles', 'age': 1 } - * - * get.cache.pebbles.name = 'penelope'; - * get('pebbles'); - * // => { 'name': 'penelope', 'age': 1 } - */ - function memoize(func, resolver) { - if (!isFunction(func)) { - throw new TypeError; - } - var memoized = function() { - var cache = memoized.cache, - key = resolver ? resolver.apply(this, arguments) : keyPrefix + arguments[0]; - - return hasOwnProperty.call(cache, key) - ? cache[key] - : (cache[key] = func.apply(this, arguments)); - } - memoized.cache = {}; - return memoized; - } - - /** - * Creates a function that is restricted to execute `func` once. Repeat calls to - * the function will return the value of the first call. The `func` is executed - * with the `this` binding of the created function. - * - * @static - * @memberOf _ - * @category Functions - * @param {Function} func The function to restrict. - * @returns {Function} Returns the new restricted function. - * @example - * - * var initialize = _.once(createApplication); - * initialize(); - * initialize(); - * // `initialize` executes `createApplication` once - */ - function once(func) { - var ran, - result; - - if (!isFunction(func)) { - throw new TypeError; - } - return function() { - if (ran) { - return result; - } - ran = true; - result = func.apply(this, arguments); - - // clear the `func` variable so the function may be garbage collected - func = null; - return result; - }; - } - - /** - * Creates a function that, when called, invokes `func` with any additional - * `partial` arguments prepended to those provided to the new function. This - * method is similar to `_.bind` except it does **not** alter the `this` binding. - * - * @static - * @memberOf _ - * @category Functions - * @param {Function} func The function to partially apply arguments to. - * @param {...*} [arg] Arguments to be partially applied. - * @returns {Function} Returns the new partially applied function. - * @example - * - * var greet = function(greeting, name) { return greeting + ' ' + name; }; - * var hi = _.partial(greet, 'hi'); - * hi('fred'); - * // => 'hi fred' - */ - function partial(func) { - return createWrapper(func, 16, slice(arguments, 1)); - } - - /** - * This method is like `_.partial` except that `partial` arguments are - * appended to those provided to the new function. - * - * @static - * @memberOf _ - * @category Functions - * @param {Function} func The function to partially apply arguments to. - * @param {...*} [arg] Arguments to be partially applied. - * @returns {Function} Returns the new partially applied function. - * @example - * - * var defaultsDeep = _.partialRight(_.merge, _.defaults); - * - * var options = { - * 'variable': 'data', - * 'imports': { 'jq': $ } - * }; - * - * defaultsDeep(options, _.templateSettings); - * - * options.variable - * // => 'data' - * - * options.imports - * // => { '_': _, 'jq': $ } - */ - function partialRight(func) { - return createWrapper(func, 32, null, slice(arguments, 1)); - } - - /** - * Creates a function that, when executed, will only call the `func` function - * at most once per every `wait` milliseconds. Provide an options object to - * indicate that `func` should be invoked on the leading and/or trailing edge - * of the `wait` timeout. Subsequent calls to the throttled function will - * return the result of the last `func` call. - * - * Note: If `leading` and `trailing` options are `true` `func` will be called - * on the trailing edge of the timeout only if the the throttled function is - * invoked more than once during the `wait` timeout. - * - * @static - * @memberOf _ - * @category Functions - * @param {Function} func The function to throttle. - * @param {number} wait The number of milliseconds to throttle executions to. - * @param {Object} [options] The options object. - * @param {boolean} [options.leading=true] Specify execution on the leading edge of the timeout. - * @param {boolean} [options.trailing=true] Specify execution on the trailing edge of the timeout. - * @returns {Function} Returns the new throttled function. - * @example - * - * // avoid excessively updating the position while scrolling - * var throttled = _.throttle(updatePosition, 100); - * jQuery(window).on('scroll', throttled); - * - * // execute `renewToken` when the click event is fired, but not more than once every 5 minutes - * jQuery('.interactive').on('click', _.throttle(renewToken, 300000, { - * 'trailing': false - * })); - */ - function throttle(func, wait, options) { - var leading = true, - trailing = true; - - if (!isFunction(func)) { - throw new TypeError; - } - if (options === false) { - leading = false; - } else if (isObject(options)) { - leading = 'leading' in options ? options.leading : leading; - trailing = 'trailing' in options ? options.trailing : trailing; - } - debounceOptions.leading = leading; - debounceOptions.maxWait = wait; - debounceOptions.trailing = trailing; - - return debounce(func, wait, debounceOptions); - } - - /** - * Creates a function that provides `value` to the wrapper function as its - * first argument. Additional arguments provided to the function are appended - * to those provided to the wrapper function. The wrapper is executed with - * the `this` binding of the created function. - * - * @static - * @memberOf _ - * @category Functions - * @param {*} value The value to wrap. - * @param {Function} wrapper The wrapper function. - * @returns {Function} Returns the new function. - * @example - * - * var p = _.wrap(_.escape, function(func, text) { - * return '

    ' + func(text) + '

    '; - * }); - * - * p('Fred, Wilma, & Pebbles'); - * // => '

    Fred, Wilma, & Pebbles

    ' - */ - function wrap(value, wrapper) { - return createWrapper(wrapper, 16, [value]); - } - - /*--------------------------------------------------------------------------*/ - - /** - * Creates a function that returns `value`. - * - * @static - * @memberOf _ - * @category Utilities - * @param {*} value The value to return from the new function. - * @returns {Function} Returns the new function. - * @example - * - * var object = { 'name': 'fred' }; - * var getter = _.constant(object); - * getter() === object; - * // => true - */ - function constant(value) { - return function() { - return value; - }; - } - - /** - * Produces a callback bound to an optional `thisArg`. If `func` is a property - * name the created callback will return the property value for a given element. - * If `func` is an object the created callback will return `true` for elements - * that contain the equivalent object properties, otherwise it will return `false`. - * - * @static - * @memberOf _ - * @category Utilities - * @param {*} [func=identity] The value to convert to a callback. - * @param {*} [thisArg] The `this` binding of the created callback. - * @param {number} [argCount] The number of arguments the callback accepts. - * @returns {Function} Returns a callback function. - * @example - * - * var characters = [ - * { 'name': 'barney', 'age': 36 }, - * { 'name': 'fred', 'age': 40 } - * ]; - * - * // wrap to create custom callback shorthands - * _.createCallback = _.wrap(_.createCallback, function(func, callback, thisArg) { - * var match = /^(.+?)__([gl]t)(.+)$/.exec(callback); - * return !match ? func(callback, thisArg) : function(object) { - * return match[2] == 'gt' ? object[match[1]] > match[3] : object[match[1]] < match[3]; - * }; - * }); - * - * _.filter(characters, 'age__gt38'); - * // => [{ 'name': 'fred', 'age': 40 }] - */ - function createCallback(func, thisArg, argCount) { - var type = typeof func; - if (func == null || type == 'function') { - return baseCreateCallback(func, thisArg, argCount); - } - // handle "_.pluck" style callback shorthands - if (type != 'object') { - return property(func); - } - var props = keys(func), - key = props[0], - a = func[key]; - - // handle "_.where" style callback shorthands - if (props.length == 1 && a === a && !isObject(a)) { - // fast path the common case of providing an object with a single - // property containing a primitive value - return function(object) { - var b = object[key]; - return a === b && (a !== 0 || (1 / a == 1 / b)); - }; - } - return function(object) { - var length = props.length, - result = false; - - while (length--) { - if (!(result = baseIsEqual(object[props[length]], func[props[length]], null, true))) { - break; - } - } - return result; - }; - } - - /** - * Converts the characters `&`, `<`, `>`, `"`, and `'` in `string` to their - * corresponding HTML entities. - * - * @static - * @memberOf _ - * @category Utilities - * @param {string} string The string to escape. - * @returns {string} Returns the escaped string. - * @example - * - * _.escape('Fred, Wilma, & Pebbles'); - * // => 'Fred, Wilma, & Pebbles' - */ - function escape(string) { - return string == null ? '' : String(string).replace(reUnescapedHtml, escapeHtmlChar); - } - - /** - * This method returns the first argument provided to it. - * - * @static - * @memberOf _ - * @category Utilities - * @param {*} value Any value. - * @returns {*} Returns `value`. - * @example - * - * var object = { 'name': 'fred' }; - * _.identity(object) === object; - * // => true - */ - function identity(value) { - return value; - } - - /** - * Adds function properties of a source object to the destination object. - * If `object` is a function methods will be added to its prototype as well. - * - * @static - * @memberOf _ - * @category Utilities - * @param {Function|Object} [object=lodash] object The destination object. - * @param {Object} source The object of functions to add. - * @param {Object} [options] The options object. - * @param {boolean} [options.chain=true] Specify whether the functions added are chainable. - * @example - * - * function capitalize(string) { - * return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase(); - * } - * - * _.mixin({ 'capitalize': capitalize }); - * _.capitalize('fred'); - * // => 'Fred' - * - * _('fred').capitalize().value(); - * // => 'Fred' - * - * _.mixin({ 'capitalize': capitalize }, { 'chain': false }); - * _('fred').capitalize(); - * // => 'Fred' - */ - function mixin(object, source, options) { - var chain = true, - methodNames = source && functions(source); - - if (!source || (!options && !methodNames.length)) { - if (options == null) { - options = source; - } - ctor = lodashWrapper; - source = object; - object = lodash; - methodNames = functions(source); - } - if (options === false) { - chain = false; - } else if (isObject(options) && 'chain' in options) { - chain = options.chain; - } - var ctor = object, - isFunc = isFunction(ctor); - - forEach(methodNames, function(methodName) { - var func = object[methodName] = source[methodName]; - if (isFunc) { - ctor.prototype[methodName] = function() { - var chainAll = this.__chain__, - value = this.__wrapped__, - args = [value]; - - push.apply(args, arguments); - var result = func.apply(object, args); - if (chain || chainAll) { - if (value === result && isObject(result)) { - return this; - } - result = new ctor(result); - result.__chain__ = chainAll; - } - return result; - }; - } - }); - } - - /** - * Reverts the '_' variable to its previous value and returns a reference to - * the `lodash` function. - * - * @static - * @memberOf _ - * @category Utilities - * @returns {Function} Returns the `lodash` function. - * @example - * - * var lodash = _.noConflict(); - */ - function noConflict() { - context._ = oldDash; - return this; - } - - /** - * A no-operation function. - * - * @static - * @memberOf _ - * @category Utilities - * @example - * - * var object = { 'name': 'fred' }; - * _.noop(object) === undefined; - * // => true - */ - function noop() { - // no operation performed - } - - /** - * Gets the number of milliseconds that have elapsed since the Unix epoch - * (1 January 1970 00:00:00 UTC). - * - * @static - * @memberOf _ - * @category Utilities - * @example - * - * var stamp = _.now(); - * _.defer(function() { console.log(_.now() - stamp); }); - * // => logs the number of milliseconds it took for the deferred function to be called - */ - var now = isNative(now = Date.now) && now || function() { - return new Date().getTime(); - }; - - /** - * Converts the given value into an integer of the specified radix. - * If `radix` is `undefined` or `0` a `radix` of `10` is used unless the - * `value` is a hexadecimal, in which case a `radix` of `16` is used. - * - * Note: This method avoids differences in native ES3 and ES5 `parseInt` - * implementations. See http://es5.github.io/#E. - * - * @static - * @memberOf _ - * @category Utilities - * @param {string} value The value to parse. - * @param {number} [radix] The radix used to interpret the value to parse. - * @returns {number} Returns the new integer value. - * @example - * - * _.parseInt('08'); - * // => 8 - */ - var parseInt = nativeParseInt(whitespace + '08') == 8 ? nativeParseInt : function(value, radix) { - // Firefox < 21 and Opera < 15 follow the ES3 specified implementation of `parseInt` - return nativeParseInt(isString(value) ? value.replace(reLeadingSpacesAndZeros, '') : value, radix || 0); - }; - - /** - * Creates a "_.pluck" style function, which returns the `key` value of a - * given object. - * - * @static - * @memberOf _ - * @category Utilities - * @param {string} key The name of the property to retrieve. - * @returns {Function} Returns the new function. - * @example - * - * var characters = [ - * { 'name': 'fred', 'age': 40 }, - * { 'name': 'barney', 'age': 36 } - * ]; - * - * var getName = _.property('name'); - * - * _.map(characters, getName); - * // => ['barney', 'fred'] - * - * _.sortBy(characters, getName); - * // => [{ 'name': 'barney', 'age': 36 }, { 'name': 'fred', 'age': 40 }] - */ - function property(key) { - return function(object) { - return object[key]; - }; - } - - /** - * Produces a random number between `min` and `max` (inclusive). If only one - * argument is provided a number between `0` and the given number will be - * returned. If `floating` is truey or either `min` or `max` are floats a - * floating-point number will be returned instead of an integer. - * - * @static - * @memberOf _ - * @category Utilities - * @param {number} [min=0] The minimum possible value. - * @param {number} [max=1] The maximum possible value. - * @param {boolean} [floating=false] Specify returning a floating-point number. - * @returns {number} Returns a random number. - * @example - * - * _.random(0, 5); - * // => an integer between 0 and 5 - * - * _.random(5); - * // => also an integer between 0 and 5 - * - * _.random(5, true); - * // => a floating-point number between 0 and 5 - * - * _.random(1.2, 5.2); - * // => a floating-point number between 1.2 and 5.2 - */ - function random(min, max, floating) { - var noMin = min == null, - noMax = max == null; - - if (floating == null) { - if (typeof min == 'boolean' && noMax) { - floating = min; - min = 1; - } - else if (!noMax && typeof max == 'boolean') { - floating = max; - noMax = true; - } - } - if (noMin && noMax) { - max = 1; - } - min = +min || 0; - if (noMax) { - max = min; - min = 0; - } else { - max = +max || 0; - } - if (floating || min % 1 || max % 1) { - var rand = nativeRandom(); - return nativeMin(min + (rand * (max - min + parseFloat('1e-' + ((rand +'').length - 1)))), max); - } - return baseRandom(min, max); - } - - /** - * Resolves the value of property `key` on `object`. If `key` is a function - * it will be invoked with the `this` binding of `object` and its result returned, - * else the property value is returned. If `object` is falsey then `undefined` - * is returned. - * - * @static - * @memberOf _ - * @category Utilities - * @param {Object} object The object to inspect. - * @param {string} key The name of the property to resolve. - * @returns {*} Returns the resolved value. - * @example - * - * var object = { - * 'cheese': 'crumpets', - * 'stuff': function() { - * return 'nonsense'; - * } - * }; - * - * _.result(object, 'cheese'); - * // => 'crumpets' - * - * _.result(object, 'stuff'); - * // => 'nonsense' - */ - function result(object, key) { - if (object) { - var value = object[key]; - return isFunction(value) ? object[key]() : value; - } - } - - /** - * A micro-templating method that handles arbitrary delimiters, preserves - * whitespace, and correctly escapes quotes within interpolated code. - * - * Note: In the development build, `_.template` utilizes sourceURLs for easier - * debugging. See http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/#toc-sourceurl - * - * For more information on precompiling templates see: - * http://lodash.com/custom-builds - * - * For more information on Chrome extension sandboxes see: - * http://developer.chrome.com/stable/extensions/sandboxingEval.html - * - * @static - * @memberOf _ - * @category Utilities - * @param {string} text The template text. - * @param {Object} data The data object used to populate the text. - * @param {Object} [options] The options object. - * @param {RegExp} [options.escape] The "escape" delimiter. - * @param {RegExp} [options.evaluate] The "evaluate" delimiter. - * @param {Object} [options.imports] An object to import into the template as local variables. - * @param {RegExp} [options.interpolate] The "interpolate" delimiter. - * @param {string} [sourceURL] The sourceURL of the template's compiled source. - * @param {string} [variable] The data object variable name. - * @returns {Function|string} Returns a compiled function when no `data` object - * is given, else it returns the interpolated text. - * @example - * - * // using the "interpolate" delimiter to create a compiled template - * var compiled = _.template('hello <%= name %>'); - * compiled({ 'name': 'fred' }); - * // => 'hello fred' - * - * // using the "escape" delimiter to escape HTML in data property values - * _.template('<%- value %>', { 'value': ' + + + + + + + + + + + + + + + + + URL: + + + + + + + + +
    +
    + {{ message.from }}: + {{ message.content }} +
    +
    + + +
    + + + +
    + + + + + diff --git a/tests/protractor/chat/chat.js b/tests/protractor/chat/chat.js new file mode 100644 index 00000000..e481168a --- /dev/null +++ b/tests/protractor/chat/chat.js @@ -0,0 +1,66 @@ +var app = angular.module('chat', ['firebase.database']); + +app.controller('ChatCtrl', function Chat($scope, $firebaseObject, $firebaseArray) { + // Get a reference to the Firebase + var rootRef = firebase.database().ref(); + + // Store the data at a random push ID + var chatRef = rootRef.child('chat').push(); + + // Put the Firebase URL into the scope so the tests can grab it. + $scope.url = chatRef.toString() + + var messagesRef = chatRef.child('messages').limitToLast(2); + + // Get the chat data as an object + $scope.chat = $firebaseObject(chatRef); + + // Get the chat messages as an array + $scope.messages = $firebaseArray(messagesRef); + + // Verify that $inst() works + verify($scope.chat.$ref() === chatRef, 'Something is wrong with $firebaseObject.$ref().'); + verify($scope.messages.$ref() === messagesRef, 'Something is wrong with $firebaseArray.$ref().'); + + // Initialize $scope variables + $scope.message = ''; + $scope.username = 'Default Guest'; + + /* Clears the chat Firebase reference */ + $scope.clearRef = function () { + chatRef.remove(); + }; + + /* Adds a new message to the messages list and updates the messages count */ + $scope.addMessage = function() { + if ($scope.message !== '') { + // Add a new message to the messages list + $scope.messages.$add({ + from: $scope.username, + content: $scope.message + }); + + // Reset the message input + $scope.message = ''; + } + }; + + /* Destroys all AngularFire bindings */ + $scope.destroy = function() { + $scope.chat.$destroy(); + $scope.messages.$destroy(); + }; + + $scope.$on('destroy', function() { + $scope.chat.$destroy(); + $scope.messages.$destroy(); + }); + + /* Logs a message and throws an error if the inputted expression is false */ + function verify(expression, message) { + if (!expression) { + console.log(message); + throw new Error(message); + } + } +}); diff --git a/tests/protractor/chat/chat.spec.js b/tests/protractor/chat/chat.spec.js new file mode 100644 index 00000000..6c71094c --- /dev/null +++ b/tests/protractor/chat/chat.spec.js @@ -0,0 +1,165 @@ +var protractor = require('protractor'); +var firebase = require('firebase'); +require('../../initialize-node.js'); + +// Various messages sent to demo +const MESSAGES_PREFAB = [ + { + from: "Default Guest 1", + content: 'Hey there!' + }, + { + from: "Default Guest 2", + content: 'Oh Hi, how are you?' + }, + { + from: "Default Guest 1", + content: "Pretty fantastic!" + } +]; + +describe('Chat App', function () { + // Reference to the Firebase which stores the data for this demo + var firebaseRef; + + // Boolean used to load the page on the first test only + var isPageLoaded = false; + + // Reference to the messages repeater + var messages = element.all(by.repeater('message in messages')); + + var flow = protractor.promise.controlFlow(); + + function waitOne() { + return protractor.promise.delayed(500); + } + + function sleep() { + flow.execute(waitOne); + } + + function clearFirebaseRef() { + var deferred = protractor.promise.defer(); + + firebaseRef.remove(function(err) { + if (err) { + deferred.reject(err); + } else { + deferred.fulfill(); + } + }); + + return deferred.promise; + } + + beforeEach(function (done) { + if (!isPageLoaded) { + isPageLoaded = true; + + browser.get('chat/chat.html').then(function () { + return browser.waitForAngular() + }).then(function() { + return element(by.id('url')).evaluate('url'); + }).then(function (url) { + // Get the random push ID where the data is being stored + return firebase.database().refFromURL(url); + }).then(function(ref) { + // Update the Firebase ref to point to the random push ID + firebaseRef = ref; + + // Clear the Firebase ref + return clearFirebaseRef(); + }).then(done) + } else { + done() + } + }); + + it('loads', function () { + expect(browser.getTitle()).toEqual('AngularFire Chat e2e Test'); + }); + + it('starts with an empty list of messages', function () { + expect(messages.count()).toBe(0); + }); + + it('adds new messages', function () { + // Add three new messages by typing into the input and pressing enter + var usernameInput = element(by.model('username')); + var newMessageInput = element(by.model('message')); + + MESSAGES_PREFAB.forEach(function (msg) { + usernameInput.clear(); + usernameInput.sendKeys(msg.from); + newMessageInput.sendKeys(msg.content + '\n'); + }); + + sleep(); + + // We should only have two messages in the repeater since we did a limit query + expect(messages.count()).toBe(2); + }); + + it('updates upon new remote messages', function (done) { + var message = { + from: 'Guest 2000', + content: 'Remote message detected' + }; + + flow.execute(function() { + var def = protractor.promise.defer(); + + // Simulate a message being added remotely + firebaseRef.child('messages').push(message, function(err) { + if( err ) { + def.reject(err); + } + else { + def.fulfill(); + } + }); + + return def.promise; + }).then(function () { + return messages.get(1).getText(); + }).then(function (text) { + expect(text).toBe(message.from + ": " + message.content); + done(); + }) + + // We should only have two messages in the repeater since we did a limit query + expect(messages.count()).toBe(2); + }); + + it('updates upon removed remote messages', function (done) { + flow.execute(function() { + var def = protractor.promise.defer(); + // Simulate a message being deleted remotely + var onCallback = firebaseRef.child('messages').limitToLast(1).on('child_added', function(childSnapshot) { + firebaseRef.child('messages').off('child_added', onCallback); + childSnapshot.ref.remove(function(err) { + if( err ) { def.reject(err); } + else { def.fulfill(); } + }); + }); + return def.promise; + }).then(function () { + return messages.get(1).getText(); + }).then(function (text) { + expect(text).toBe(MESSAGES_PREFAB[2].from + ": " + MESSAGES_PREFAB[2].content); + done(); + }); + + // We should only have two messages in the repeater since we did a limit query + expect(messages.count()).toBe(2); + }); + + it('stops updating once the AngularFire bindings are destroyed', function () { + // Destroy the AngularFire bindings + $('#destroyButton').click(); + + sleep(); + + expect(messages.count()).toBe(0); + }); +}); diff --git a/tests/protractor/priority/priority.css b/tests/protractor/priority/priority.css new file mode 100644 index 00000000..e69de29b diff --git a/tests/protractor/priority/priority.html b/tests/protractor/priority/priority.html new file mode 100644 index 00000000..d8b4d927 --- /dev/null +++ b/tests/protractor/priority/priority.html @@ -0,0 +1,56 @@ + + + + AngularFire Priority e2e Test + + + + + + + + + + + + + + + + + + + + URL: + + + + + + + + +
    + + +
    + + +
    +
    + {{ message.from }}: + {{ message.content }} + Priority: {{ message.$priority }} +
    +
    + + +
    + + +
    + + + + + diff --git a/tests/protractor/priority/priority.js b/tests/protractor/priority/priority.js new file mode 100644 index 00000000..b94d44c2 --- /dev/null +++ b/tests/protractor/priority/priority.js @@ -0,0 +1,66 @@ +var app = angular.module('priority', ['firebase.database']); +app.controller('PriorityCtrl', function Chat($scope, $firebaseArray, $firebaseObject) { + // Get a reference to the Firebase + var rootRef = firebase.database().ref(); + + // Store the data at a random push ID + var messagesRef = rootRef.child('priority').push(); + + // Put the Firebase URL into the scope so the tests can grab it. + $scope.url = messagesRef.toString() + + // Get the chat messages as an array + $scope.messages = $firebaseArray(messagesRef); + + // Verify that $inst() works + verify($scope.messages.$ref() === messagesRef, 'Something is wrong with $firebaseArray.$ref().'); + + // Initialize $scope variables + $scope.message = ''; + $scope.username = 'Default Guest'; + + /* Clears the priority Firebase reference */ + $scope.clearRef = function () { + messagesRef.remove(); + }; + + /* Adds a new message to the messages list */ + $scope.addMessage = function () { + if ($scope.message !== '') { + // Add a new message to the messages list + var priority = $scope.messages.length; + $scope.messages.$add({ + from: $scope.username, + content: $scope.message + }).then(function (ref) { + var newItem = $firebaseObject(ref); + + newItem.$loaded().then(function (data) { + verify(newItem === data, '$firebaseObject.$loaded() does not return correct value.'); + + // Update the message's priority + newItem.$priority = priority; + newItem.$save(); + }); + }, function (error) { + verify(false, 'Something is wrong with $firebaseArray.$add().'); + }); + + // Reset the message input + $scope.message = ''; + }; + }; + + /* Destroys all AngularFire bindings */ + $scope.destroy = function() { + $scope.messages.$destroy(); + }; + + /* Logs a message and throws an error if the inputted expression is false */ + function verify(expression, message) { + if (!expression) { + console.log(message); + throw new Error(message); + } + } +}); diff --git a/tests/protractor/priority/priority.spec.js b/tests/protractor/priority/priority.spec.js new file mode 100644 index 00000000..a8389c96 --- /dev/null +++ b/tests/protractor/priority/priority.spec.js @@ -0,0 +1,153 @@ +var protractor = require('protractor'); +var firebase = require('firebase'); +require('../../initialize-node.js'); + +// Various messages sent to demo +const MESSAGES_PREFAB = [ + { + from: "Default Guest 1", + content: 'Hey there!' + }, + { + from: "Default Guest 2", + content: 'Oh Hi, how are you?' + }, + { + from: "Default Guest 1", + content: "Pretty fantastic!" + } +]; + +describe('Priority App', function () { + // Reference to the message repeater + var messages = element.all(by.repeater('message in messages')); + + // Reference to the Firebase which stores the data for this demo + var firebaseRef; + + // Boolean used to load the page on the first test only + var isPageLoaded = false; + + var flow = protractor.promise.controlFlow(); + + function waitOne() { + return protractor.promise.delayed(500); + } + + function sleep() { + flow.execute(waitOne); + } + + function clearFirebaseRef() { + var deferred = protractor.promise.defer(); + + firebaseRef.remove(function(err) { + if (err) { + deferred.reject(err); + } else { + deferred.fulfill(); + } + }); + + return deferred.promise; + } + + beforeEach(function (done) { + if (!isPageLoaded) { + isPageLoaded = true; + + // Navigate to the priority app + browser.get('priority/priority.html').then(function() { + return browser.waitForAngular() + }).then(function() { + return element(by.id('url')).evaluate('url'); + }).then(function (url) { + // Get the random push ID where the data is being stored + return firebase.database().refFromURL(url); + }).then(function(ref) { + // Update the Firebase ref to point to the random push ID + firebaseRef = ref; + + // Clear the Firebase ref + return clearFirebaseRef(); + }).then(done) + } else { + done(); + } + }); + + + it('loads', function () { + expect(browser.getTitle()).toEqual('AngularFire Priority e2e Test'); + }); + + it('starts with an empty list of messages', function () { + // Make sure the page has no messages + expect(messages.count()).toBe(0); + }); + + it('adds new messages with the correct priority', function () { + // Add three new messages by typing into the input and pressing enter + var usernameInput = element(by.model('username')); + var newMessageInput = element(by.model('message')); + + MESSAGES_PREFAB.forEach(function (msg) { + usernameInput.clear(); + usernameInput.sendKeys(msg.from); + newMessageInput.sendKeys(msg.content + '\n'); + }); + + sleep(); + + // Make sure the page has three messages + expect(messages.count()).toBe(3); + + // Make sure the priority of each message is correct + expect($('.message:nth-of-type(1) .priority').getText()).toEqual('0'); + expect($('.message:nth-of-type(2) .priority').getText()).toEqual('1'); + expect($('.message:nth-of-type(3) .priority').getText()).toEqual('2'); + + // Make sure the content of each message is correct + expect($('.message:nth-of-type(1) .content').getText()).toEqual(MESSAGES_PREFAB[0].content); + expect($('.message:nth-of-type(2) .content').getText()).toEqual(MESSAGES_PREFAB[1].content); + expect($('.message:nth-of-type(3) .content').getText()).toEqual(MESSAGES_PREFAB[2].content); + }); + + it('responds to external priority updates', function () { + flow.execute(moveRecords); + flow.execute(waitOne); + + + expect(messages.count()).toBe(3); + expect($('.message:nth-of-type(1) .priority').getText()).toEqual('0'); + expect($('.message:nth-of-type(2) .priority').getText()).toEqual('1'); + expect($('.message:nth-of-type(3) .priority').getText()).toEqual('4'); + + // Make sure the content of each message is correct + expect($('.message:nth-of-type(1) .content').getText()).toEqual(MESSAGES_PREFAB[2].content); + expect($('.message:nth-of-type(2) .content').getText()).toEqual(MESSAGES_PREFAB[1].content); + expect($('.message:nth-of-type(3) .content').getText()).toEqual(MESSAGES_PREFAB[0].content); + + + function moveRecords() { + return setPriority(null, 4) + .then(setPriority.bind(null, 2, 0)); + } + + function setPriority(start, pri) { + var def = protractor.promise.defer(); + firebaseRef.startAt(start).limitToFirst(1).once('child_added', function(snap) { + var data = snap.val(); + //todo https://github.com/firebase/angularFire/issues/333 + //todo makeItChange just forces Angular to update the dom since it won't change + //todo when a $ variable updates + data.makeItChange = true; + snap.ref.setWithPriority(data, pri, function(err) { + if( err ) { def.reject(err); } + else { def.fulfill(snap.key); } + }) + }, def.reject); + return def.promise; + } + }); +}); diff --git a/tests/protractor/test_todo-omnibinder.html b/tests/protractor/test_todo-omnibinder.html deleted file mode 100644 index 1f691d69..00000000 --- a/tests/protractor/test_todo-omnibinder.html +++ /dev/null @@ -1,91 +0,0 @@ - - - - - AngularFire TODO Test - - - - - - - - - - -
    - -
    -
    -
    -
    - - - -
    -
    -
    - - - diff --git a/tests/protractor/test_todo-omnibinder.js b/tests/protractor/test_todo-omnibinder.js deleted file mode 100644 index a6adbda5..00000000 --- a/tests/protractor/test_todo-omnibinder.js +++ /dev/null @@ -1,34 +0,0 @@ -var protractor = require('protractor'), - tractor = protractor.getInstance(), - cleared; - -describe('OmniBinder Todo', function () { - describe('child_added', function () { - beforeEach(function () { - - tractor.get('http://localhost:8080/tests/e2e/test_todo-omnibinder.html'); - - if (!cleared) { - //Clear all firebase data - tractor.findElement(protractor.By.css('#clearRef')). - click(); - cleared = true; - } - - expect(tractor.getTitle()).toBe('AngularFire TODO Test'); - }); - - it('should no-op', function () { - //Forced reload - }); - - it('should have an empty list of todos', function () { - //Wait for items to be populated - tractor.sleep(1000); - - tractor.findElements(protractor.By.css('#messagesDiv > div')).then(function (listItems) { - expect(listItems.length).toBe(0); - }); - }); - }); -}); diff --git a/tests/protractor/tictactoe/tictactoe.css b/tests/protractor/tictactoe/tictactoe.css new file mode 100644 index 00000000..b653a533 --- /dev/null +++ b/tests/protractor/tictactoe/tictactoe.css @@ -0,0 +1,11 @@ +.row { + clear: both; +} + +.cell { + float: left; + border: solid 2px black; + height: 10px; + width: 10px; + padding: 40px; +} \ No newline at end of file diff --git a/tests/protractor/tictactoe/tictactoe.html b/tests/protractor/tictactoe/tictactoe.html new file mode 100644 index 00000000..dfd48aba --- /dev/null +++ b/tests/protractor/tictactoe/tictactoe.html @@ -0,0 +1,44 @@ + + + + AngularFire TicTacToe e2e Test + + + + + + + + + + + + + + + + + + + + URL: + + + + + + + + +
    +
    +
    + {{ cell }} +
    +
    +
    + + + + + diff --git a/tests/protractor/tictactoe/tictactoe.js b/tests/protractor/tictactoe/tictactoe.js new file mode 100644 index 00000000..197ac03e --- /dev/null +++ b/tests/protractor/tictactoe/tictactoe.js @@ -0,0 +1,74 @@ +var app = angular.module('tictactoe', ['firebase.database']); +app.controller('TicTacToeCtrl', function Chat($scope, $firebaseObject) { + $scope.board = {}; + + // Get a reference to the Firebase + var rootRef = firebase.database().ref('tictactoe'); + var boardRef; + + // If the query string contains a push ID, use that as the child for data storage; + // otherwise, generate a new random push ID + var pushId; + if (window.location && window.location.search) { + pushId = window.location.search.substr(1).split('=')[1]; + } + + if (pushId) { + boardRef = rootRef.child(pushId); + } else { + // Store the data at a random push ID + boardRef = rootRef.push(); + } + + // Put the Firebase URL into the scope so the tests can grab it. + $scope.url = boardRef.toString() + + // Get the board as an AngularFire object + $scope.boardObject = $firebaseObject(boardRef); + + // Create a 3-way binding to Firebase + $scope.boardObject.$bindTo($scope, 'board'); + + // Verify that $inst() works + verify($scope.boardObject.$ref() === boardRef, 'Something is wrong with $firebaseObject.$ref().'); + + // Initialize $scope variables + $scope.whoseTurn = 'X'; + + /* Resets the tictactoe Firebase reference */ + $scope.resetRef = function () { + ['x0', 'x1', 'x2'].forEach(function (xCoord) { + $scope.board[xCoord] = { + y0: '', + y1: '', + y2: '' + }; + }); + }; + + + /* Makes a move at the current cell */ + $scope.makeMove = function(rowId, columnId) { + // Only make a move if the current cell is not already taken + if ($scope.board[rowId][columnId] === '') { + // Update the board + $scope.board[rowId][columnId] = $scope.whoseTurn; + + // Change whose turn it is + $scope.whoseTurn = ($scope.whoseTurn === 'X') ? 'O' : 'X'; + } + }; + + /* Destroys all AngularFire bindings */ + $scope.destroy = function() { + $scope.boardObject.$destroy(); + }; + + /* Logs a message and throws an error if the inputted expression is false */ + function verify(expression, message) { + if (!expression) { + console.log(message); + throw new Error(message); + } + } +}); diff --git a/tests/protractor/tictactoe/tictactoe.spec.js b/tests/protractor/tictactoe/tictactoe.spec.js new file mode 100644 index 00000000..34f8814c --- /dev/null +++ b/tests/protractor/tictactoe/tictactoe.spec.js @@ -0,0 +1,151 @@ +var protractor = require('protractor'); +var firebase = require('firebase'); +require('../../initialize-node.js'); + +describe('TicTacToe App', function () { + // Reference to the Firebase which stores the data for this demo + var firebaseRef; + + // Boolean used to load the page on the first test only + var isPageLoaded = false; + + // Reference to the messages repeater + //var cells = $$('.cell'); + var cells = element.all(by.css('.cell')); + + var flow = protractor.promise.controlFlow(); + + function waitOne() { + return protractor.promise.delayed(1500); + } + + function sleep() { + // flow.execute takes a function and if the + // function returns a promise it waits for it + // to be resolved + flow.execute(waitOne); + } + + function clearFirebaseRef() { + var deferred = protractor.promise.defer(); + + firebaseRef.remove(function(err) { + if (err) { + deferred.reject(err); + } else { + deferred.fulfill(); + } + }); + + return deferred.promise; + } + + beforeEach(function (done) { + if (!isPageLoaded) { + isPageLoaded = true; + + // Navigate to the tictactoe app + browser.get('tictactoe/tictactoe.html').then(function() { + return browser.waitForAngular() + }).then(function() { + return element(by.id('url')).evaluate('url'); + }).then(function (url) { + // Get the random push ID where the data is being stored + return firebase.database().refFromURL(url); + }).then(function(ref) { + // Update the Firebase ref to point to the random push ID + firebaseRef = ref; + + // Clear the Firebase ref + return clearFirebaseRef(); + }).then(done) + } else { + done(); + } + }); + + it('loads', function () { + expect(browser.getTitle()).toEqual('AngularFire TicTacToe e2e Test'); + }); + + it('starts with an empty board', function () { + // Reset the board + $('#resetRef').click(); + + // Wait for the board to reset + sleep(); + + // Make sure the board has 9 cells + var cells = element.all(by.css('.cell')); + expect(cells.count()).toBe(9); + + // Make sure the board is empty + cells.each(function(element) { + expect(element.getText()).toBe(''); + }); + }); + + it('updates the board when cells are clicked', function () { + // Make sure the board has 9 cells + expect(cells.count()).toBe(9); + + // Make three moves by clicking the cells + cells.get(0).click(); + cells.get(2).click(); + cells.get(6).click(); + + sleep(); + + // Make sure the content of each clicked cell is correct + expect(cells.get(0).getText()).toBe('X'); + expect(cells.get(2).getText()).toBe('O'); + expect(cells.get(6).getText()).toBe('X'); + }); + + it('persists state across refresh', function(done) { + // Refresh the page, passing the push ID to use for data storage + browser.get('tictactoe/tictactoe.html?pushId=' + firebaseRef.key).then(function() { + // Wait for AngularFire to sync the initial state + sleep(); + sleep(); + + // Make sure the board has 9 cells + expect(cells.count()).toBe(9); + + // Make sure the content of each clicked cell is correct + expect(cells.get(0).getText()).toBe('X'); + expect(cells.get(2).getText()).toBe('O'); + expect(cells.get(6).getText()).toBe('X'); + + done(); + }); + }); + + it('stops updating Firebase once the AngularFire bindings are destroyed', function () { + // Make sure the board has 9 cells + expect(cells.count()).toBe(9); + + // Destroy the AngularFire bindings + $('#destroyButton').click(); + $('#resetRef').click(); + + // Click the middle cell + cells.get(4).click(); + + sleep(); + + expect(cells.get(4).getText()).toBe('X'); + + sleep(); + + // make sure values are not changed on the server + flow.execute(function() { + var def = protractor.promise.defer(); + firebaseRef.child('x1/y2').once('value', function (dataSnapshot) { + expect(dataSnapshot.val()).toBe(''); + def.fulfill(); + }); + return def.promise; + }); + }); +}); diff --git a/tests/protractor/todo/todo.css b/tests/protractor/todo/todo.css new file mode 100644 index 00000000..e1eed3e7 --- /dev/null +++ b/tests/protractor/todo/todo.css @@ -0,0 +1,7 @@ +#newTodoButton { + width: 200px; +} + +.edit { + width: 250px; +} \ No newline at end of file diff --git a/tests/protractor/todo/todo.html b/tests/protractor/todo/todo.html new file mode 100644 index 00000000..af3a9a27 --- /dev/null +++ b/tests/protractor/todo/todo.html @@ -0,0 +1,54 @@ + + + + AngularFire Todo e2e Test + + + + + + + + + + + + + + + + + + +
    + + URL: + + + + + + + + + +
    + + +
    + +
    + + +
    +
    + + + +
    +
    + + + + + diff --git a/tests/protractor/todo/todo.js b/tests/protractor/todo/todo.js new file mode 100644 index 00000000..f4d6f54a --- /dev/null +++ b/tests/protractor/todo/todo.js @@ -0,0 +1,61 @@ +var app = angular.module('todo', ['firebase.database']); +app. controller('TodoCtrl', function Todo($scope, $firebaseArray) { + // Get a reference to the Firebase + var rootRef = firebase.database().ref(); + + // Store the data at a random push ID + var todosRef = rootRef.child('todo').push(); + + // Put the Firebase URL into the scope so the tests can grab it. + $scope.url = todosRef.toString() + + // Get the todos as an array + $scope.todos = $firebaseArray(todosRef); + + // Verify that $ref() works + verify($scope.todos.$ref() === todosRef, "Something is wrong with $firebaseArray.$ref()."); + + /* Clears the todos Firebase reference */ + $scope.clearRef = function () { + todosRef.remove(); + }; + + /* Adds a new todo item */ + $scope.addTodo = function() { + if ($scope.newTodo !== '') { + $scope.todos.$add({ + title: $scope.newTodo, + completed: false + }); + + $scope.newTodo = ''; + } + }; + + /* Adds a random todo item */ + $scope.addRandomTodo = function () { + $scope.newTodo = 'Todo ' + new Date().getTime(); + $scope.addTodo(); + }; + + /* Removes the todo item with the inputted ID */ + $scope.removeTodo = function(id) { + // Verify that $indexFor() and $keyAt() work + verify($scope.todos.$indexFor($scope.todos.$keyAt(id)) === id, "Something is wrong with $firebaseArray.$indexFor() or FirebaseArray.$keyAt()."); + + $scope.todos.$remove(id); + }; + + /* Unbinds the todos array */ + $scope.destroyArray = function() { + $scope.todos.$destroy(); + }; + + /* Logs a message and throws an error if the inputted expression is false */ + function verify(expression, message) { + if (!expression) { + console.log(message); + throw new Error(message); + } + } +}); diff --git a/tests/protractor/todo/todo.spec.js b/tests/protractor/todo/todo.spec.js new file mode 100644 index 00000000..1ee476b6 --- /dev/null +++ b/tests/protractor/todo/todo.spec.js @@ -0,0 +1,165 @@ +var protractor = require('protractor'); +var firebase = require('firebase'); +require('../../initialize-node.js'); + +describe('Todo App', function () { + // Reference to the Firebase which stores the data for this demo + var firebaseRef; + + // Boolean used to load the page on the first test only + var isPageLoaded = false; + + // Reference to the todos repeater + var todos = element.all(by.repeater('(id, todo) in todos')); + var flow = protractor.promise.controlFlow(); + + function waitOne() { + return protractor.promise.delayed(500); + } + + function sleep() { + flow.execute(waitOne); + } + + function clearFirebaseRef() { + var deferred = protractor.promise.defer(); + + firebaseRef.remove(function(err) { + if (err) { + deferred.reject(err); + } else { + deferred.fulfill(); + } + }); + + return deferred.promise; + } + + beforeEach(function (done) { + if (!isPageLoaded) { + isPageLoaded = true; + + // Navigate to the todo app + browser.get('todo/todo.html').then(function() { + return browser.waitForAngular() + }).then(function() { + return element(by.id('url')).evaluate('url'); + }).then(function (url) { + // Get the random push ID where the data is being stored + return firebase.database().refFromURL(url); + }).then(function(ref) { + // Update the Firebase ref to point to the random push ID + firebaseRef = ref; + + // Clear the Firebase ref + return clearFirebaseRef(); + }).then(done) + } else { + done(); + } + }); + + it('loads', function () { + expect(browser.getTitle()).toEqual('AngularFire Todo e2e Test'); + }); + + it('starts with an empty list of Todos', function () { + expect(todos.count()).toBe(0); + }); + + it('adds new Todos', function () { + // Add three new todos by typing into the input and pressing enter + var newTodoInput = element(by.model('newTodo')); + newTodoInput.sendKeys('Buy groceries\n'); + newTodoInput.sendKeys('Run 10 miles\n'); + newTodoInput.sendKeys('Build Firebase\n'); + + sleep(); + + expect(todos.count()).toBe(3); + }); + + it('adds random Todos', function () { + // Add a three new random todos via the provided button + var addRandomTodoButton = $('#addRandomTodoButton'); + addRandomTodoButton.click(); + addRandomTodoButton.click(); + addRandomTodoButton.click(); + + sleep(); + + expect(todos.count()).toBe(6); + }); + + it('removes Todos', function () { + // Remove two of the todos via the provided buttons + $('.todo:nth-of-type(2) .removeTodoButton').click(); + $('.todo:nth-of-type(3) .removeTodoButton').click(); + + sleep(); + + expect(todos.count()).toBe(4); + }); + + it('updates when a new Todo is added remotely', function (done) { + // Simulate a todo being added remotely + + expect(todos.count()).toBe(4); + flow.execute(function() { + var def = protractor.promise.defer(); + firebaseRef.push({ + title: 'Wash the dishes', + completed: false + }, function(err) { + if( err ) { def.reject(err); } + else { def.fulfill(); } + }); + return def.promise; + }).then(function () { + browser.wait(function() { + return element(by.id('todo-4')).isPresent() + }, 10000); + }).then(function () { + expect(todos.count()).toBe(5); + done(); + }); + }) + + it('updates when an existing Todo is removed remotely', function (done) { + // Simulate a todo being removed remotely + + expect(todos.count()).toBe(5); + flow.execute(function() { + var def = protractor.promise.defer(); + var onCallback = firebaseRef.limitToLast(1).on("child_added", function(childSnapshot) { + // Make sure we only remove a child once + firebaseRef.off("child_added", onCallback); + + childSnapshot.ref.remove(function(err) { + if( err ) { def.reject(err); } + else { def.fulfill(); } + }); + }); + return def.promise; + }).then(function () { + browser.wait(function() { + return todos.count(function (count) { + return count == 4; + }); + }, 10000); + }).then(function () { + expect(todos.count()).toBe(4); + done(); + }); + + }); + + it('stops updating once the sync array is destroyed', function () { + // Destroy the sync array + $('#destroyArrayButton').click(); + + sleep(); + + expect(todos.count()).toBe(0); + }); +}); diff --git a/tests/protractor/upload/logo.png b/tests/protractor/upload/logo.png new file mode 100644 index 00000000..a0787ab4 Binary files /dev/null and b/tests/protractor/upload/logo.png differ diff --git a/tests/protractor/upload/upload.html b/tests/protractor/upload/upload.html new file mode 100644 index 00000000..575b1047 --- /dev/null +++ b/tests/protractor/upload/upload.html @@ -0,0 +1,45 @@ + + + + AngularFire Upload e2e Test + + + + + + + + + + + + + + + + +
    + + +
    + +

    + Canceled +

    + +
    + + {{((metadata.bytesTransferred / metadata.totalBytes)*100) || 0}}%
    +
    + +
    +
    {{metadata.downloadURL}}
    + +
    + {{ error | json }} +
    + + + + + diff --git a/tests/protractor/upload/upload.js b/tests/protractor/upload/upload.js new file mode 100644 index 00000000..48903ff4 --- /dev/null +++ b/tests/protractor/upload/upload.js @@ -0,0 +1,64 @@ +var app = angular.module('upload', ['firebase.storage']); + +app.controller('UploadCtrl', function Upload($scope, $firebaseStorage, $timeout) { + // Create a reference + const storageRef = firebase.storage().ref('user/1.png'); + // Create the storage binding + const storageFire = $firebaseStorage(storageRef); + + var file; + + $scope.select = function (event) { + file = event.files[0]; + } + + $scope.upload = function() { + $scope.metadata = {bytesTransferred: 0, totalBytes: 1}; + $scope.error = null; + + // upload the file + $scope.task = storageFire.$put(file); + + // pause, wait, then resume. + $scope.task.$pause(); + setTimeout(() => { + $scope.task.$resume(); + }, 500); + + // monitor progress state + $scope.task.$progress(metadata => { + if (metadata.state === 'running') { + $scope.isCanceled = false; + $scope.isUploading = true; + } + + $scope.metadata = metadata; + }); + // log a possible error + $scope.task.$error(error => { + $scope.error = error; + }); + // log when the upload completes + $scope.task.$complete(metadata => { + $scope.isUploading = false; + $scope.metadata = metadata; + }); + + $scope.task.then(snapshot => { + $scope.snapshot = snapshot; + }); + + $scope.task.catch(error => { + $scope.error = error; + }); + + } + + $scope.cancel = function() { + if ($scope.task && $scope.task.$cancel()) { + $scope.isCanceled = true; + $scope.isUploading = false; + } + } + +}); diff --git a/tests/protractor/upload/upload.manual.js b/tests/protractor/upload/upload.manual.js new file mode 100644 index 00000000..1c25fc29 --- /dev/null +++ b/tests/protractor/upload/upload.manual.js @@ -0,0 +1,89 @@ +var protractor = require('protractor'); +var firebase = require('firebase'); +var path = require('path'); +require('../../initialize-node.js'); + +describe('Upload App', function () { + // Reference to the Firebase which stores the data for this demo + var firebaseRef; + + // Boolean used to load the page on the first test only + var isPageLoaded = false; + + var flow = protractor.promise.controlFlow(); + + function waitOne() { + return protractor.promise.delayed(500); + } + + function sleep() { + flow.execute(waitOne); + } + + function clearFirebaseRef() { + var deferred = protractor.promise.defer(); + + firebaseRef.remove(function (err) { + if (err) { + deferred.reject(err); + } else { + deferred.fulfill(); + } + }); + + return deferred.promise; + } + + beforeEach(function (done) { + if (!isPageLoaded) { + isPageLoaded = true; + + browser.get('upload/upload.html').then(function () { + return browser.waitForAngular() + }).then(done) + } else { + done() + } + }); + + it('loads', function () { + expect(browser.getTitle()).toEqual('AngularFire Upload e2e Test'); + }); + + it('uploads a file, cancels the upload task, and tries uploading again', function (done) { + var fileToUpload = './upload/logo.png'; + var absolutePath = path.resolve(__dirname, fileToUpload); + + $('input[type="file"]').sendKeys(absolutePath); + $('#submit').click(); + + var el; + var cancelEl = element(by.id('cancel')); + + browser.driver.wait(protractor.until.elementIsVisible(cancelEl.getWebElement())) + .then(function () { + $('#cancel').click(); + + var canceledEl = element(by.id('canceled')); + return browser.driver.wait(protractor.until.elementIsVisible(canceledEl.getWebElement())) + }) + .then(function () { + var submitEl = element(by.id('submit')); + return browser.driver.wait(protractor.until.elementIsVisible(submitEl.getWebElement())) + }) + .then(function () { + $('#submit').click(); + + el = element(by.id('url')); + return browser.driver.wait(protractor.until.elementIsVisible(el.getWebElement())) + }) + .then(function () { + return el.getText(); + }) + .then(function (text) { + var result = "https://firebasestorage.googleapis.com/v0/b/oss-test.appspot.com/o/user%2F1.png"; + expect(text.slice(0, result.length)).toEqual(result); + done(); + }); + }); +}); diff --git a/tests/protractorConf.js b/tests/protractorConf.js deleted file mode 100644 index a0ac335d..00000000 --- a/tests/protractorConf.js +++ /dev/null @@ -1,33 +0,0 @@ -// An example configuration file. -exports.config = { - // The address of a running selenium server. If this is specified, - // seleniumServerJar and seleniumPort will be ignored. - // seleniumAddress: 'http://localhost:4444/wd/hub', - seleniumServerJar: './selenium/selenium-server-standalone-2.37.0.jar', - seleniumPort: 4444, - - chromeDriver: './selenium/chromedriver', - - seleniumArgs: [], - - // A base URL for your application under test. Calls to protractor.get() - // with relative paths will be prepended with this. - baseUrl: '', - - // Capabilities to be passed to the webdriver instance. - capabilities: { - 'browserName': 'chrome' - }, - specs: [ - 'tests/protractor/test_todo-omnibinder.js' - ], - // Options to be passed to Jasmine-node. - jasmineNodeOpts: { - // onComplete will be called before the driver quits. - onComplete: null, - isVerbose: true, - showColors: true, - includeStackTrace: true, - defaultTimeoutInterval: 10000 - } -}; diff --git a/tests/sauce_karma.conf.js b/tests/sauce_karma.conf.js new file mode 100644 index 00000000..25a46e68 --- /dev/null +++ b/tests/sauce_karma.conf.js @@ -0,0 +1,51 @@ +// Configuration file for Karma +// http://karma-runner.github.io/0.10/config/configuration-file.html + +module.exports = function(config) { + var customLaunchers = require('./browsers.json') + .reduce(function (browsers, browser) { + browsers[(browser.name + '_v' + browser.version).replace(/(\.|\s)/g, '_')] = { + base: 'SauceLabs', + browserName: browser.name, + platform: browser.platform, + version: browser.version + }; + return browsers; + }, {}); + var browsers = Object.keys(customLaunchers); + + config.set({ + basePath: '', + frameworks: ['jasmine'], + files: [ + '../node_modules/angular/angular.js', + '../node_modules/angular-mocks/angular-mocks.js', + '../node_modules/mockfirebase/browser/mockfirebase.js', + 'lib/**/*.js', + '../dist/angularfire.js', + 'mocks/**/*.js', + 'initialize.js', + 'unit/**/*.spec.js' + ], + + logLevel: config.LOG_INFO, + + transports: ['xhr-polling'], + + sauceLabs: { + testName: 'AngularFire Unit Tests', + startConnect: false, + tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER + }, + + captureTimeout: 0, + browserNoActivityTimeout: 120000, + + //Recommend starting Chrome manually with experimental javascript flag enabled, and open localhost:9876. + customLaunchers: customLaunchers, + browsers: browsers, + reporters: ['dots', 'saucelabs'], + singleRun: true + + }); +}; diff --git a/tests/sauce_protractor.conf.js b/tests/sauce_protractor.conf.js new file mode 100644 index 00000000..24bccb29 --- /dev/null +++ b/tests/sauce_protractor.conf.js @@ -0,0 +1,50 @@ +exports.config = { + // Locally, we should just use the default standalone Selenium server + // In Travis, we set up the Selenium serving via Sauce Labs + sauceUser: process.env.SAUCE_USERNAME, + sauceKey: process.env.SAUCE_ACCESS_KEY, + + // Tests to run + specs: [ + './protractor/**/*.spec.js' + ], + + // Capabilities to be passed to the webdriver instance + // For a full list of available capabilities, see https://code.google.com/p/selenium/wiki/DesiredCapabilities + capabilities: { + 'browserName': 'chrome', + 'tunnel-identifier': process.env.TRAVIS_JOB_NUMBER, + 'build': process.env.TRAVIS_BUILD_NUMBER, + 'name': 'AngularFire Protractor Tests Build ' + process.env.TRAVIS_BUILD_NUMBER + }, + + // Calls to protractor.get() with relative paths will be prepended with the baseUrl + baseUrl: 'http://localhost:3030/tests/protractor/', + + // Selector for the element housing the angular app + rootElement: 'body', + + // Options to be passed to Jasmine-node. + onPrepare: function() { + require('jasmine-spec-reporter'); + // add jasmine spec reporter + jasmine.getEnv().addReporter(new jasmine.SpecReporter({ + displaySkippedSpec: true, + displaySpecDuration: true + })); + }, + + // Options to be passed to minijasminenode + jasmineNodeOpts: { + // onComplete will be called just before the driver quits. + onComplete: null, + // If true, display spec names. + isVerbose: true, + // If true, print colors to the terminal. + showColors: true, + // If true, include stack traces in failures. + includeStackTrace: true, + // Default time to wait in ms before a test fails. + defaultTimeoutInterval: 20000 + } +}; diff --git a/tests/travis.sh b/tests/travis.sh new file mode 100755 index 00000000..f13aebd2 --- /dev/null +++ b/tests/travis.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e +grunt build +grunt test:unit +grunt test:e2e +if [ $TRAVIS_TAG ]; then + grunt sauce:unit; +fi diff --git a/tests/unit/AngularFire.spec.js b/tests/unit/AngularFire.spec.js deleted file mode 100644 index 1d10506b..00000000 --- a/tests/unit/AngularFire.spec.js +++ /dev/null @@ -1,144 +0,0 @@ -describe('AngularFire', function () { - var $firebase, $filter, $timeout; - beforeEach(module('firebase')); - beforeEach(inject(function (_$firebase_, _$filter_, _$timeout_) { - $firebase = _$firebase_; - $filter = _$filter_; - $timeout = _$timeout_; - })); - - describe('$on', function() { - describe("loaded", function() { - it('should give the current value', function() { - var fb = new Firebase('Mock://').child('data').autoFlush(); - var spy = jasmine.createSpy(); - $firebase(fb).$on('loaded', spy); - flush(); - expect(spy).toHaveBeenCalledWith(fb.getData()); - }); - - it('should trigger if declared before data loads', function() { - var fb = new Firebase('Mock://').child('data'); - var spy = jasmine.createSpy(); - $firebase(fb).$on('loaded', spy); - flush(fb); - expect(spy).toHaveBeenCalled(); - }); - - it('$getIndex() should work inside loaded function (#262)', function() { - var fb = new Firebase('Mock://').child('data').autoFlush(); - var called = false; - var ref = $firebase(fb).$on('loaded', function(data) { - called = true; - // this assertion must be inside the callback - expect(ref.$getIndex()).toEqual(Firebase._.keys(data)); - }); - flush(); - expect(called).toBe(true); - }); - - it('should have a snapshot if declared after data loads (#265)', function() { - var fb = new Firebase('Mock://').child('data'); - var spy = jasmine.createSpy(); - var ref = $firebase(fb); - fb.flush(); - ref.$on('loaded', spy); - flush(); - expect(spy.mostRecentCall.args[0]).not.toBeNull(); - }); - - it("should only trigger once", function() { - var fb = new Firebase('Mock://').child('data').autoFlush(); - var spy = jasmine.createSpy(); - var ref = $firebase(fb); - ref.$on('loaded', spy); - flush(); - fb.set('foo'); - flush(); - expect(spy.callCount).toBe(1); - }); - - it('should allow $bind within the loaded callback (#260)', inject(function($rootScope) { - var $scope = $rootScope.$new(); - var fb = new Firebase('Mock://').child('data').autoFlush(); - var called = false; - var ref = $firebase(fb).$on('loaded', function(data) { - called = true; - ref.$bind($scope, 'test'); - // these assertions must be inside the callback - expect(ref.$getIndex().length).toBeGreaterThan(0); - expect(ref.$getIndex()).toEqual($scope.test.$getIndex()); - }); - flush(); - expect(called).toBe(true); - })); - }); - - describe("value", function() { - it('should contain the correct event snapshot (#267)', function() { - var fb = new Firebase('Mock://').child('data').autoFlush(); - var spy = jasmine.createSpy(); - $firebase(fb).$on('value', spy); - flush(); - expect(spy).toHaveBeenCalledWith({ - snapshot: { - name: 'data', - value: fb.getData() - }, - prevChild: null - }); - }); - - it('should trigger if declared before data loads', function() { - var fb = new Firebase('Mock://').child('data'), spy = jasmine.createSpy(); - $firebase(fb).$on('value', spy); - fb.flush(); - flush(); - expect(spy).toHaveBeenCalled(); - }); - - it('should trigger if declared after data loads', function() { - var fb = new Firebase('Mock://').child('data'), spy = jasmine.createSpy(); - var ref = $firebase(fb); - fb.flush(); - ref.$on('value', spy); - flush(); - expect(spy).toHaveBeenCalled(); - }); - - it('should trigger if data changes', function() { - var fb = new Firebase('Mock://').child('data').autoFlush(), spy = jasmine.createSpy(); - $firebase(fb).$on('value', spy); - flush(); - fb.set('foo'); - flush(); - expect(spy.callCount).toBe(2); - }); - - it('should not blow up if synced value is null', function() { - var fb = new Firebase('EmptyMock://', null).autoFlush(), spy = jasmine.createSpy(); - $firebase(fb).$on('value', spy); - flush(); - fb.set('foo'); - flush(); - expect(spy.callCount).toBe(2); - }); - }); - - //todo child_added - //todo child_removed - //todo child_moved - }); - - // flush blows up if you call it and no items are queued, however, we often need to make sure - // $timeout hasn't been called so this is simpler than guessing how many times the internal - // code may call $timeout and trying to make sure we account for them; just call it and ignore - // the error; also flush fb if passed in - function flush(fb) { - fb && fb.flush(); - try { - $timeout.flush(); - } - catch(e) {} - } -}); diff --git a/tests/unit/FirebaseArray.spec.js b/tests/unit/FirebaseArray.spec.js new file mode 100644 index 00000000..e288298c --- /dev/null +++ b/tests/unit/FirebaseArray.spec.js @@ -0,0 +1,1091 @@ +'use strict'; +describe('$firebaseArray', function () { + + var DEFAULT_ID = 'REC1'; + var STUB_DATA = { + 'a': { + aString: 'alpha', + aNumber: 1, + aBoolean: false + }, + 'b': { + aString: 'bravo', + aNumber: 2, + aBoolean: true + }, + 'c': { + aString: 'charlie', + aNumber: 3, + aBoolean: true + }, + 'd': { + aString: 'delta', + aNumber: 4, + aBoolean: true + }, + 'e': { + aString: 'echo', + aNumber: 5 + } + }; + + var arr, $firebaseArray, $utils, $timeout, $rootScope, $q, tick, testutils; + beforeEach(function() { + module('firebase.database'); + module('testutils'); + inject(function (_$firebaseArray_, $firebaseUtils, _$timeout_, _$rootScope_, _$q_, _testutils_) { + testutils = _testutils_; + $timeout = _$timeout_; + $firebaseArray = _$firebaseArray_; + $utils = $firebaseUtils; + $rootScope = _$rootScope_; + $q = _$q_; + + firebase.database.enableLogging(function () {tick()}); + tick = function () { + setTimeout(function() { + $q.defer(); + $rootScope.$digest(); + try { + $timeout.flush(); + } catch (err) { + // This throws an error when there is nothing to flush... + } + }) + }; + + arr = stubArray(STUB_DATA); + }); + }); + + describe('', function() { + beforeEach(function() { + inject(function($firebaseUtils, $firebaseArray) { + this.$utils = $firebaseUtils; + this.$firebaseArray = $firebaseArray; + }); + }); + + it('should return a valid array', function() { + expect(Array.isArray(arr)).toBe(true); + }); + + it('should have API methods', function() { + var i = 0; + this.$utils.getPublicMethods($firebaseArray, function(v,k) { + expect(typeof arr[k]).toBe('function'); + i++; + }); + expect(i).toBeGreaterThan(0); + }); + }); + + describe('$add', function() { + it('should call $push on $firebase', function() { + var spy = spyOn(firebase.database.Reference.prototype, 'push').and.callThrough(); + var data = {foo: 'bar'}; + arr.$add(data); + expect(spy).toHaveBeenCalled(); + }); + + it('should return a promise', function() { + expect(arr.$add({foo: 'bar'})).toBeAPromise(); + }); + + it('should resolve to ref for new record', function(done) { + arr.$add({foo: 'bar'}) + .then(function (ref) { + expect(ref.toString()).toBe(arr.$ref().child(ref.key).toString()) + done(); + }); + }); + + it('should wait for promise resolution to update array', function () { + var queue = []; + function addPromise(snap, prevChild){ + return $q( + function(resolve) { + queue.push(resolve); + }).then(function(name) { + var data = $firebaseArray.prototype.$$added.call(arr, snap, prevChild); + data.name = name; + return data; + }); + } + arr = stubArray(null, $firebaseArray.$extend({$$added:addPromise})); + expect(arr.length).toBe(0); + arr.$add({userId:'1234'}); + expect(arr.length).toBe(0); + expect(queue.length).toBe(1); + queue[0]('James'); + $timeout.flush(); + expect(arr.length).toBe(1); + expect(arr[0].name).toBe('James'); + }); + + it('should wait to resolve $loaded until $$added promise is resolved', function () { + var queue = []; + function addPromise(snap, prevChild){ + return $q( + function(resolve) { + queue.push(resolve); + }).then(function(name) { + var data = $firebaseArray.prototype.$$added.call(arr, snap, prevChild); + data.name = name; + return data; + }); + } + var called = false; + var ref = stubRef(); + arr = stubArray(null, $firebaseArray.$extend({$$added:addPromise}), ref); + arr.$loaded().then(function(){ + expect(arr.length).toBe(1); + called = true; + }); + ref.set({'-Jwgx':{username:'James', email:'james@internet.com'}}); + + $timeout.flush(); + queue[0]('James'); + $timeout.flush(); + expect(called, 'called').toBe(true); + }); + + it('should reject promise on fail', function(done) { + var whiteSpy = jasmine.createSpy('resolve'); + var blackSpy = jasmine.createSpy('reject'); + + arr.$add({"bad/key": "will fail!"}) + .then(whiteSpy, blackSpy) + .finally(function () { + expect(whiteSpy).not.toHaveBeenCalled(); + expect(blackSpy).toHaveBeenCalled(); + done(); + }); + }); + + it('should work with a primitive value', function(done) { + arr.$add('hello').then(function (ref) { + ref.once("value", function (ss) { + expect(ss.val()).toEqual('hello'); + done(); + }); + }); + }); + + it('should throw error if array is destroyed', function() { + arr.$destroy(); + expect(function() { + arr.$add({foo: 'bar'}); + }).toThrowError(Error); + }); + + it('should store priorities', function() { + var arr = stubArray(); + addAndProcess(arr, testutils.snap('one', 'b', 1), null); + addAndProcess(arr, testutils.snap('two', 'a', 2), 'b'); + addAndProcess(arr, testutils.snap('three', 'd', 3), 'd'); + addAndProcess(arr, testutils.snap('four', 'c', 4), 'c'); + addAndProcess(arr, testutils.snap('five', 'e', 5), 'e'); + + $rootScope.$digest() + + for(var i=1; i <= 5; i++) { + expect(arr[i-1].$priority).toBe(i); + } + }); + + it('should observe $priority and $value meta keys if present', function(done) { + var spy = jasmine.createSpy('$add').and.callFake(function (r) {return r;}); + var arr = stubArray(); + arr.$add({$value: 'foo', $priority: 99}) + .then(spy) + .then(function (ref) { + expect(spy).toHaveBeenCalled(); + ref.on("value", function (ss) { + expect(ss.getPriority()).toBe(99); + expect(ss.val()).toBe('foo'); + done(); + }); + }); + }); + + it('should work on a query', function() { + var ref = stubRef(); + var query = ref.limitToLast(2); + var arr = $firebaseArray(query); + addAndProcess(arr, testutils.snap('one', 'b', 1), null); + expect(arr.length).toBe(1); + }); + }); + + describe('$save', function() { + beforeEach(function () { + arr.$ref().set(STUB_DATA); + $rootScope.$digest(); + }); + + it('should accept an array index', function(done) { + arr[2].number = 99; + + arr.$save(2).then(function (ref) { + ref.once("value", function (ss) { + expect(ss.val().number).toEqual(99); + done(); + }); + }); + }); + + it('should accept an item from the array', function(done) { + arr[2].number = 99; + + arr.$save(arr[2]).then(function (ref) { + ref.once("value", function (ss) { + expect(ss.val().number).toEqual(99); + done(); + }); + }); + }); + + it('should return a promise', function() { + expect(arr.$save(1)).toBeAPromise(); + }); + + it('should resolve promise on sync', function(done) { + var spy = jasmine.createSpy(); + arr.$save(1).then(spy).then(function () { + expect(spy).toHaveBeenCalled(); + done(); + }); + expect(spy).not.toHaveBeenCalled(); + }); + + it('should reject promise on failure', function(done) { + var whiteSpy = jasmine.createSpy('resolve'); + var blackSpy = jasmine.createSpy('reject'); + arr[2]["invalid/key"] = "Oh noes!"; + arr.$save(2) + .then(whiteSpy, blackSpy) + .finally(function () { + expect(whiteSpy).not.toHaveBeenCalled(); + expect(blackSpy).toHaveBeenCalled(); + done(); + }); + }); + + it('should reject promise on bad index', function(done) { + var whiteSpy = jasmine.createSpy('resolve'); + var blackSpy = jasmine.createSpy('reject'); + arr.$save(99) + .then(whiteSpy, blackSpy) + .finally(function () { + expect(whiteSpy).not.toHaveBeenCalled(); + + expect(blackSpy.calls.argsFor(0)[0]).toMatch(/invalid/i); + done(); + }); + }); + + it('should reject promise on bad object', function(done) { + var whiteSpy = jasmine.createSpy('resolve'); + var blackSpy = jasmine.createSpy('reject'); + arr.$save({foo: 'baz'}).then(whiteSpy, blackSpy).then(function () { + expect(whiteSpy).not.toHaveBeenCalled(); + expect(blackSpy.calls.argsFor(0)[0]).toMatch(/invalid/i); + done(); + }); + }); + + it('should accept a primitive', function() { + var key = arr.$keyAt(1); + var ref = arr.$ref().child(key); + arr[1] = {$value: 'happy', $id: key}; + arr.$save(1).then(function (ref) { + ref.once("value", function (ss) { + expect(ss.val()).toBe('happy'); + }) + }); + }); + + it('should throw error if object is destroyed', function() { + arr.$destroy(); + expect(function() { + arr.$save(0); + }).toThrowError(Error); + }); + + it('should trigger watch event', function(done) { + var spy = jasmine.createSpy('$watch'); + arr.$watch(spy); + var key = arr.$keyAt(1); + arr[1].foo = 'watchtest'; + arr.$save(1).then(function () { + expect(spy).toHaveBeenCalledWith(jasmine.objectContaining({event: 'child_changed', key: key})); + done() + }); + }); + + it('should work on a query', function(done) { + var whiteSpy = jasmine.createSpy('resolve'); + var blackSpy = jasmine.createSpy('reject').and.callFake(function(e) { + console.error(e); + }); + var ref = stubRef(); + ref.set(STUB_DATA); + + var query = ref.limitToLast(5); + var arr = $firebaseArray(query); + + arr.$loaded().then(function () { + var key = arr.$keyAt(1); + arr[1].foo = 'watchtest'; + + arr.$save(1).then(whiteSpy, blackSpy).then(function () { + expect(whiteSpy).toHaveBeenCalled(); + expect(blackSpy).not.toHaveBeenCalled(); + done(); + }); + }) + }); + }); + + describe('$remove', function() { + beforeEach(function () { + arr.$ref().set(STUB_DATA); + $rootScope.$digest(); + }); + + it('should call remove on Firebase ref', function(done) { + arr.$remove(1).then(function (ref) { + ref.once("value", function (ss) { + expect(ss.val()).toBe(null); + done(); + }); + }); + }); + + it('should return a promise', function() { + expect(arr.$remove(1)).toBeAPromise(); + }); + + it('should resolve promise to ref on success', function(done) { + var whiteSpy = jasmine.createSpy('resolve'); + var blackSpy = jasmine.createSpy('reject'); + var expName = arr.$keyAt(1); + arr.$remove(1).then(whiteSpy, blackSpy).then(function () { + var resRef = whiteSpy.calls.argsFor(0)[0]; + expect(whiteSpy).toHaveBeenCalled(); + expect(resRef).toBeAFirebaseRef(); + expect(resRef.key).toBe(expName); + expect(blackSpy).not.toHaveBeenCalled(); + done(); + }); + }); + + it('should reject promise on failure', function() { + var whiteSpy = jasmine.createSpy('resolve'); + var blackSpy = jasmine.createSpy('reject'); + var err = new Error('test_fail_remove'); + + spyOn(firebase.database.Reference.prototype, "remove").and.callFake(function (cb) { + cb(err); + }); + + arr.$remove(1) + .then(whiteSpy, blackSpy) + .then(function () { + expect(whiteSpy).not.toHaveBeenCalled(); + expect(blackSpy).toHaveBeenCalledWith(err); + done(); + }); + }); + + it('should reject promise if bad int', function(done) { + var whiteSpy = jasmine.createSpy('resolve'); + var blackSpy = jasmine.createSpy('reject'); + arr.$remove(-99) + .then(whiteSpy, blackSpy) + .finally(function () { + expect(whiteSpy).not.toHaveBeenCalled(); + expect(blackSpy.calls.argsFor(0)[0]).toMatch(/invalid/i); + done(); + }); + }); + + it('should reject promise if bad object', function() { + var whiteSpy = jasmine.createSpy('resolve'); + var blackSpy = jasmine.createSpy('reject'); + arr.$remove({foo: false}) + .then(whiteSpy, blackSpy) + .then(function () { + expect(whiteSpy).not.toHaveBeenCalled(); + expect(blackSpy).toHaveBeenCalled(); + expect(blackSpy.calls.argsFor(0)[0]).toMatch(/invalid/i); + }); + }); + + it('should work on a query', function(done) { + var ref = stubRef(); + ref.set(STUB_DATA); + + var whiteSpy = jasmine.createSpy('resolve'); + var blackSpy = jasmine.createSpy('reject').and.callFake(function(e) { + console.error(e); + }); + var query = ref.limitToLast(5); + var arr = $firebaseArray(query); + + arr.$loaded() + .then(function () { + return arr.$remove(1); + }) + .then(whiteSpy, blackSpy) + .then(function () { + expect(whiteSpy).toHaveBeenCalled(); + expect(blackSpy).not.toHaveBeenCalled(); + done(); + }); + }); + + it('should throw Error if array destroyed', function() { + arr.$destroy(); + expect(function () { + arr.$remove(0); + }).toThrowError(Error); + }); + }); + + describe('$keyAt', function() { + beforeEach(function () { + arr.$ref().set(STUB_DATA); + $rootScope.$digest(); + }); + + it('should return key for an integer', function() { + expect(arr.$keyAt(2)).toBe('c'); + }); + + it('should return key for an object', function() { + expect(arr.$keyAt(arr[2])).toBe('c'); + }); + + it('should return null if invalid object', function() { + expect(arr.$keyAt({foo: false})).toBe(null); + }); + + it('should return null if invalid integer', function() { + expect(arr.$keyAt(-99)).toBe(null); + }); + }); + + describe('$indexFor', function() { + beforeEach(function () { + arr.$ref().set(STUB_DATA); + $rootScope.$digest(); + }); + + it('should return integer for valid key', function() { + expect(arr.$indexFor('c')).toBe(2); + }); + + it('should return -1 for invalid key', function() { + expect(arr.$indexFor('notarealkey')).toBe(-1); + }); + + it('should not show up after removing the item', function() { + var rec = arr.$getRecord('b'); + expect(rec).not.toBe(null); + arr.$$removed(testutils.refSnap(testutils.ref('b'))); + arr.$$process('child_removed', rec); + expect(arr.$indexFor('b')).toBe(-1); + }); + }); + + describe('$loaded', function() { + beforeEach(function () { + arr.$ref().set(STUB_DATA); + $rootScope.$digest(); + }); + + it('should return a promise', function() { + expect(arr.$loaded()).toBeAPromise(); + }); + + it('should resolve when values are received', function(done) { + var whiteSpy = jasmine.createSpy('resolve'); + var blackSpy = jasmine.createSpy('reject'); + + arr.$loaded().then(whiteSpy, blackSpy).then(function () { + expect(whiteSpy).toHaveBeenCalled(); + expect(blackSpy).not.toHaveBeenCalled(); + done(); + }); + + $rootScope.$digest(); + }); + + it('should resolve to the array', function(done) { + var spy = jasmine.createSpy('resolve'); + arr.$loaded().then(spy).then(function () { + expect(spy).toHaveBeenCalledWith(arr); + done(); + }) + + $rootScope.$digest(); + }); + + it('should have all data loaded when it resolves', function(done) { + var spy = jasmine.createSpy('resolve'); + arr.$loaded().then(spy).then(function () { + var list = spy.calls.argsFor(0)[0]; + expect(list.length).toBe(5); + done(); + }); + + $rootScope.$digest(); + }); + + it('should reject when error fetching records', function(done) { + var whiteSpy = jasmine.createSpy('resolve'); + var blackSpy = jasmine.createSpy('reject'); + var err = new Error('test_fail'); + + spyOn(firebase.database.Reference.prototype, "on").and.callFake(function (event, cb, cancel_cb) { + cancel_cb(err); + }); + + arr = $firebaseArray(stubRef()); + + arr.$loaded() + .then(whiteSpy, blackSpy) + .finally(function () { + expect(whiteSpy).not.toHaveBeenCalled(); + expect(blackSpy).toHaveBeenCalledWith(err); + done(); + }); + }); + + it('should resolve if function passed directly into $loaded', function(done) { + arr.$loaded(function (a) { + expect(a).toBe(arr); + done(); + }); + + $rootScope.$digest(); + }); + + it('should reject properly when function passed directly into $loaded', function() { + var whiteSpy = jasmine.createSpy('resolve'); + var err = new Error('test_fail'); + + spyOn(firebase.database.Reference.prototype, "on").and.callFake(function (event, cb, cancel_cb) { + cancel_cb(err); + }); + + arr = $firebaseArray(stubRef()); + + arr.$loaded(whiteSpy, function () { + expect(whiteSpy).not.toHaveBeenCalled(); + done(); + }); + }); + }); + + describe('$resolved', function () { + it('should return false on init', function () { + arr = $firebaseArray(stubRef()); + expect(arr.$resolved).toBe(false); + }); + + it('should return true once $loaded() promise is resolved', function () { + arr = $firebaseArray(stubRef()); + + arr.$loaded() + .finally(function () { + expect(arr.$resolved).toBe(true); + done(); + }); + }); + + it('should return true once $loaded() promise is rejected', function () { + var err = new Error('test_fail'); + + spyOn(firebase.database.Reference.prototype, "once").and.callFake(function (event, cb, cancel_cb) { + cancel_cb(err); + }); + + arr = $firebaseArray(stubRef()); + + arr.$loaded() + .finally(function () { + expect(arr.$resolved).toBe(true); + done(); + }); + }); + }); + + describe('$ref', function() { + it('should return Firebase instance it was created with', function() { + var ref = stubRef(); + var arr = $firebaseArray(ref); + expect(arr.$ref()).toBe(ref); + }); + }); + + describe('$watch', function() { + it('should get notified when $$notify is called', function() { + var spy = jasmine.createSpy('$watch'); + arr.$watch(spy); + arr.$$notify('child_removed', 'removedkey123', 'prevkey456'); + expect(spy).toHaveBeenCalledWith({event: 'child_removed', key: 'removedkey123', prevChild: 'prevkey456'}); + }); + + it('should return a dispose function', function() { + expect(arr.$watch(function() {})).toBeA('function'); + }); + + it('should not get notified after dispose function is called', function() { + var spy = jasmine.createSpy('$watch'); + var off = arr.$watch(spy); + off(); + arr.$$notify('child_removed', 'removedkey123', 'prevkey456'); + expect(spy).not.toHaveBeenCalled(); + }); + + it('calling the deregistration function twice should be silently ignored', function(){ + var spy = jasmine.createSpy('$watch'); + var off = arr.$watch(spy); + off(); + off(); + arr.$$notify('child_removed', 'removedkey123', 'prevkey456'); + expect(spy).not.toHaveBeenCalled(); + }); + }); + + describe('$destroy', function() { + beforeEach(function () { + arr.$ref().set(STUB_DATA); + $rootScope.$digest(); + }); + + it('should call off on ref', function() { + var spy = spyOn(arr.$ref(), 'off'); + arr.$destroy(); + expect(spy).toHaveBeenCalled(); + }); + + it('should empty the array', function() { + expect(arr.length).toBeGreaterThan(0); + arr.$destroy() + expect(arr.length).toBe(0); + }); + + it('should reject $loaded() if not completed yet', function(done) { + var whiteSpy = jasmine.createSpy('resolve'); + var blackSpy = jasmine.createSpy('reject'); + var arr = stubArray(); + arr.$loaded().then(whiteSpy, blackSpy).then(function () { + expect(whiteSpy).not.toHaveBeenCalled(); + expect(blackSpy.calls.argsFor(0)[0]).toMatch(/destroyed/i); + done(); + }); + arr.$destroy(); + + $rootScope.$digest(); + }); + }); + + describe('$$added', function() { + beforeEach(function () { + arr.$ref().set(STUB_DATA); + $rootScope.$digest(); + }); + + it('should return an object', function() { + var snap = testutils.snap({foo: 'bar'}, 'newObj'); + var res = arr.$$added(snap); + expect(res).toEqual(jasmine.objectContaining({foo: 'bar'})); + }); + + it('should return false if key already exists', function() { + var snap = testutils.snap({foo: 'bar'}, 'a'); + var res = arr.$$added(snap); + expect(res).toBe(false); + }); + + it('should accept a primitive', function() { + var res = arr.$$added(testutils.snap(true, 'newPrimitive'), null); + expect(res.$value).toBe(true); + }); + + it('should apply $$defaults if they exist', function() { + var arr = stubArray(null, $firebaseArray.$extend({ + $$defaults: {aString: 'not_applied', foo: 'foo'} + })); + var res = arr.$$added(testutils.snap(STUB_DATA.a)); + expect(res.aString).toBe(STUB_DATA.a.aString); + expect(res.foo).toBe('foo'); + }); + }); + + describe('$$updated', function() { + beforeEach(function () { + arr.$ref().set(STUB_DATA); + $rootScope.$digest(); + }); + + it('should return true if data changes', function() { + var res = arr.$$updated(testutils.snap('foo', 'b')); + expect(res).toBe(true); + }); + + it('should return false if data does not change', function() { + var i = arr.$indexFor('b'); + var res = arr.$$updated(testutils.snap(arr[i], 'b')); + expect(res).toBe(false); + }); + + it('should update local data', function() { + var i = arr.$indexFor('b'); + expect(i).toBeGreaterThan(-1); + arr.$$updated(testutils.snap('foo', 'b')); + expect(arr[i]).toEqual(jasmine.objectContaining({'$value': 'foo'})); + }); + + it('should ignore if not found', function() { + var len = arr.length; + expect(len).toBeGreaterThan(0); + var copy = testutils.deepCopyObject(arr); + arr.$$updated(testutils.snap('foo', 'notarealkey')); + expect(len).toEqual(copy.length); + for (var i = 0; i < len; i++) { + expect(arr[i]).toEqual(copy[i]); + } + }); + + it('should preserve ids', function() { + var pos = arr.$indexFor('b'); + expect(pos).toBeGreaterThan(-1); + arr.$$updated(testutils.snap({foo: 'bar'}, 'b')); + expect(arr[pos].$id).toBe('b'); + }); + + it('should set priorities', function() { + var pos = arr.$indexFor('b'); + expect(pos).toBeGreaterThan(-1); + arr.$$updated(testutils.snap({foo: 'bar'}, 'b', 250)); + expect(arr[pos].$priority).toBe(250); + }); + + it('should apply $$defaults if they exist', function() { + var arr = stubArray(STUB_DATA, $firebaseArray.$extend({ + $$defaults: {aString: 'not_applied', foo: 'foo'} + })); + $rootScope.$digest(); + + var rec = arr.$getRecord('a'); + expect(rec.aString).toBe(STUB_DATA.a.aString); + expect(rec.foo).toBe('foo'); + delete rec.foo; + arr.$$updated(testutils.snap($utils.toJSON(rec), 'a')); + expect(rec.aString).toBe(STUB_DATA.a.aString); + expect(rec.foo).toBe('foo'); + }); + }); + + describe('$$moved', function() { + beforeEach(function () { + arr.$ref().set(STUB_DATA); + $rootScope.$digest(); + }); + + it('should set $priority', function() { + var rec = arr.$getRecord('c'); + expect(rec.$priority).not.toBe(999); + arr.$$moved(testutils.snap($utils.toJSON(rec), 'c', 999), 'd'); + expect(rec.$priority).toBe(999); + }); + + it('should return true if record exists', function() { + var rec = arr.$getRecord('a'); + var res = arr.$$moved(testutils.snap($utils.toJSON(rec), 'a'), 'c'); + expect(res).toBe(true); + }); + + it('should return false record not found', function() { + var res = arr.$$moved(testutils.snap(true, 'notarecord'), 'c'); + expect(res).toBe(false); + }); + }); + + describe('$$removed', function() { + beforeEach(function () { + arr.$ref().set(STUB_DATA); + $rootScope.$digest(); + }); + + it('should return true if exists in data', function() { + var res = arr.$$removed(testutils.snap(null, 'e')); + expect(res).toBe(true); + }); + + it('should return false if does not exist in data', function() { + var res = arr.$$removed(testutils.snap(null, 'notarecord')); + expect(res).toBe(false); + }); + }); + + describe('$$error', function() { + it('should call $destroy', function() { + var spy = jasmine.createSpy('$destroy'); + var arr = stubArray(STUB_DATA, $firebaseArray.$extend({ $destroy: spy })); + spy.calls.reset(); + arr.$$error('test_err'); + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('$$notify', function() { + it('should notify $watch listeners', function() { + var spy1 = jasmine.createSpy('$watch1'); + var spy2 = jasmine.createSpy('$watch2'); + arr.$watch(spy1); + arr.$watch(spy2); + arr.$$notify('added', 'e', 'd'); + expect(spy1).toHaveBeenCalled(); + expect(spy2).toHaveBeenCalled(); + }); + + it('should pass an object containing key, event, and prevChild if present', function() { + var spy = jasmine.createSpy('$watch1'); + arr.$watch(spy); + arr.$$notify('child_added', 'e', 'd'); + expect(spy).toHaveBeenCalledWith({event: 'child_added', key: 'e', prevChild: 'd'}); + }); + }); + + describe('$$process', function() { + beforeEach(function () { + arr.$ref().set(STUB_DATA); + $rootScope.$digest(); + }); + + /////////////// ADD + it('should add to local array', function() { + var len = arr.length; + var rec = arr.$$added(testutils.snap({hello: 'world'}, 'addz'), 'b'); + arr.$$process('child_added', rec, 'b'); + expect(arr.length).toBe(len+1); + }); + + it('should position after prev child', function() { + var pos = arr.$indexFor('b'); + expect(pos).toBeGreaterThan(-1); + var rec = arr.$$added(testutils.snap({hello: 'world'}, 'addAfterB'), 'b'); + arr.$$process('child_added', rec, 'b'); + expect(arr.$keyAt(pos+1)).toBe('addAfterB'); + }); + + it('should position first if prevChild is null', function() { + var rec = arr.$$added(testutils.snap({hello: 'world'}, 'addFirst'), null); + arr.$$process('child_added', rec, null); + expect(arr.$keyAt(0)).toBe('addFirst'); + }); + + it('should position last if prevChild not found', function() { + var len = arr.length; + var rec = arr.$$added(testutils.snap({hello: 'world'}, 'addLast'), 'notarealkeyinarray'); + arr.$$process('child_added', rec, 'notrealkeyinarray'); + expect(arr.$keyAt(len)).toBe('addLast'); + }); + + it('should invoke $$notify with "child_added" event', function() { + var spy = jasmine.createSpy('$$notify'); + var arr = stubArray(STUB_DATA, $firebaseArray.$extend({ $$notify: spy })); + spy.calls.reset(); + var rec = arr.$$added(testutils.snap({hello: 'world'}, 'addFirst'), null); + arr.$$process('child_added', rec, null); + expect(spy).toHaveBeenCalled(); + }); + + it('"child_added" should not invoke $$notify if it already exists after prevChild', function() { + var spy = jasmine.createSpy('$$notify'); + var arr = stubArray(STUB_DATA, $firebaseArray.$extend({ $$notify: spy })); + $rootScope.$digest(); + var index = arr.$indexFor('e'); + var prevChild = arr.$$getKey(arr[index -1]); + spy.calls.reset(); + arr.$$process('child_added', arr.$getRecord('e'), prevChild); + expect(spy).not.toHaveBeenCalled(); + }); + + ///////////////// UPDATE + + it('should invoke $$notify with "child_changed" event', function() { + var spy = jasmine.createSpy('$$notify'); + var arr = stubArray(STUB_DATA, $firebaseArray.$extend({ $$notify: spy })); + $rootScope.$digest(); + spy.calls.reset(); + arr.$$updated(testutils.snap({hello: 'world'}, 'a')); + arr.$$process('child_changed', arr.$getRecord('a')); + expect(spy).toHaveBeenCalled(); + }); + + ///////////////// MOVE + it('should move local record', function() { + var b = arr.$indexFor('b'); + var c = arr.$indexFor('c'); + arr.$$moved(testutils.refSnap(testutils.ref('b')), 'c'); + arr.$$process('child_moved', arr.$getRecord('b'), 'c'); + expect(arr.$indexFor('c')).toBe(b); + expect(arr.$indexFor('b')).toBe(c); + }); + + it('should position at 0 if prevChild is null', function() { + var b = arr.$indexFor('b'); + expect(b).toBeGreaterThan(0); + arr.$$moved(testutils.snap(null, 'b'), null); + arr.$$process('child_moved', arr.$getRecord('b'), null); + expect(arr.$indexFor('b')).toBe(0); + }); + + it('should position at end if prevChild not found', function() { + var b = arr.$indexFor('b'); + expect(b).toBeLessThan(arr.length-1); + arr.$$moved(testutils.refSnap(testutils.ref('b')), 'notarealkey'); + arr.$$process('child_moved', arr.$getRecord('b'), 'notarealkey'); + expect(arr.$indexFor('b')).toBe(arr.length-1); + }); + + it('should invoke $$notify with "child_moved" event', function() { + var spy = jasmine.createSpy('$$notify'); + var arr = stubArray(STUB_DATA, $firebaseArray.$extend({ $$notify: spy })); + spy.calls.reset(); + arr.$$moved(testutils.refSnap(testutils.ref('b')), 'notarealkey'); + arr.$$process('child_moved', arr.$getRecord('b'), 'notarealkey'); + expect(spy).toHaveBeenCalled(); + }); + + it('"child_moved" should not trigger $$notify if prevChild is already the previous element' , function() { + var spy = jasmine.createSpy('$$notify'); + var arr = stubArray(STUB_DATA, $firebaseArray.$extend({ $$notify: spy })); + $rootScope.$digest(); + var index = arr.$indexFor('e'); + var prevChild = arr.$$getKey(arr[index - 1]); + spy.calls.reset(); + arr.$$process('child_moved', arr.$getRecord('e'), prevChild); + expect(spy).not.toHaveBeenCalled(); + }); + + ///////////////// REMOVE + it('should remove from local array', function() { + var len = arr.length; + expect(arr.$indexFor('b')).toBe(1); + arr.$$removed(testutils.refSnap(testutils.ref('b'))); + arr.$$process('child_removed', arr.$getRecord('b')); + expect(arr.length).toBe(len-1); + expect(arr.$indexFor('b')).toBe(-1); + }); + + it('should trigger $$notify with "child_removed" event', function() { + var spy = jasmine.createSpy('$$notify'); + var arr = stubArray(STUB_DATA, $firebaseArray.$extend({ $$notify: spy })); + $rootScope.$digest(); + spy.calls.reset(); + arr.$$removed(testutils.refSnap(testutils.ref('e'))); + arr.$$process('child_removed', arr.$getRecord('e')); + expect(spy).toHaveBeenCalled(); + }); + + it('"child_removed" should not trigger $$notify if the record is not in the array' , function() { + var spy = jasmine.createSpy('$$notify'); + var arr = stubArray(STUB_DATA, $firebaseArray.$extend({ $$notify: spy })); + $rootScope.$digest(); + spy.calls.reset(); + arr.$$process('child_removed', {$id:'f'}); + expect(spy).not.toHaveBeenCalled(); + }); + + //////////////// OTHER + it('should throw an error for an unknown event type',function(){ + var arr = stubArray(STUB_DATA); + expect(function(){ + arr.$$process('unknown_event', arr.$getRecord('e')); + }).toThrow(); + }); + + }); + + describe('$extend', function() { + beforeEach(function () { + arr.$ref().set(STUB_DATA); + $rootScope.$digest(); + }); + + it('should return a valid array', function() { + var F = $firebaseArray.$extend({}); + expect(Array.isArray(F(stubRef()))).toBe(true); + }); + + it('should preserve child prototype', function() { + function Extend() { $firebaseArray.apply(this, arguments); } + Extend.prototype.foo = function() {}; + $firebaseArray.$extend(Extend); + var arr = new Extend(stubRef()); + expect(typeof(arr.foo)).toBe('function'); + }); + + it('should return child class', function() { + function A() {} + var res = $firebaseArray.$extend(A); + expect(res).toBe(A); + }); + + it('should be instanceof $firebaseArray', function() { + function A() {} + $firebaseArray.$extend(A); + expect(new A(stubRef()) instanceof $firebaseArray).toBe(true); + }); + + it('should add on methods passed into function', function() { + function foo() { return 'foo'; } + var F = $firebaseArray.$extend({foo: foo}); + var res = F(stubRef()); + expect(typeof res.$$updated).toBe('function'); + expect(typeof res.foo).toBe('function'); + expect(res.foo()).toBe('foo'); + }); + + it('should work with the new keyword', function() { + var fn = function() {}; + var Res = $firebaseArray.$extend({foo: fn}); + expect(new Res(stubRef()).foo).toBeA('function'); + }); + + it('should work without the new keyword', function() { + var fn = function() {}; + var Res = $firebaseArray.$extend({foo: fn}); + expect(Res(stubRef()).foo).toBeA('function'); + }); + }); + + function stubRef() { + return firebase.database().ref().push(); + } + + function stubArray(initialData, Factory, ref) { + if( !Factory ) { Factory = $firebaseArray; } + if( !ref ) { + ref = stubRef(); + } + var arr = new Factory(ref); + if( initialData ) { + ref.set(initialData); + } + return arr; + } + + function addAndProcess(arr, snap, prevChild) { + arr.$$process('child_added', arr.$$added(snap, prevChild), prevChild); + } + +}); diff --git a/tests/unit/FirebaseAuth.spec.js b/tests/unit/FirebaseAuth.spec.js new file mode 100644 index 00000000..4f524d32 --- /dev/null +++ b/tests/unit/FirebaseAuth.spec.js @@ -0,0 +1,684 @@ +describe('FirebaseAuth',function(){ + 'use strict'; + + var $firebaseAuth, ref, authService, auth, result, failure, status, tick, $timeout, log, fakePromise, fakePromiseResolve, fakePromiseReject; + + beforeEach(function(){ + + log = { + warn:[] + }; + // + // module('firebase.utils'); + module('firebase.auth',function($provide){ + $provide.value('$log',{ + warn:function(){ + log.warn.push(Array.prototype.slice.call(arguments,0)); + } + }) + }); + module('testutils'); + + result = undefined; + failure = undefined; + status = null; + + fakePromise = function () { + var resolve; + var reject; + var obj = { + then: function (_resolve, _reject) { + resolve = _resolve; + reject = _reject; + }, + resolve: function (result) { + resolve(result); + }, + reject: function (err) { + reject(err); + } + }; + fakePromiseReject = obj.reject; + fakePromiseResolve = obj.resolve; + return obj; + } + + //offAuth, signInWithToken, updatePassword, changeEmail, removeUser + auth = firebase.auth(); + ['signInWithCustomToken','signInAnonymously','signInWithEmailAndPassword', + 'signInWithPopup','signInWithRedirect', 'signInWithCredential', + 'signOut', + 'createUserWithEmailAndPassword','sendPasswordResetEmail' + ].forEach(function (funcName) { + spyOn(auth, funcName).and.callFake(fakePromise); + }); + spyOn(auth, 'onAuthStateChanged').and.callFake(function (cb) { + fakePromiseResolve = function (result) { + cb(result); + } + return function () {/* Deregister */}; + }); + + inject(function(_$firebaseAuth_,_$timeout_, $q, $rootScope){ + $firebaseAuth = _$firebaseAuth_; + authService = $firebaseAuth(auth); + $timeout = _$timeout_; + + firebase.database.enableLogging(function () {tick()}); + tick = function () { + setTimeout(function() { + $q.defer(); + $rootScope.$digest(); + try { + $timeout.flush(); + } catch (err) { + // This throws an error when there is nothing to flush... + } + }) + }; + }); + + }); + + function wrapPromise(promise){ + promise.then(function(_result_){ + status = 'resolved'; + result = _result_; + },function(_failure_){ + status = 'rejected'; + failure = _failure_; + }); + } + + describe('Constructor', function() { + it('will throw an error if a string is used in place of a Firebase auth instance',function(){ + expect(function(){ + $firebaseAuth('https://some-firebase.firebaseio.com/'); + }).toThrow(); + }); + + it('will throw an error if a database instance is used in place of a Firebase auth instance',function(){ + expect(function(){ + $firebaseAuth(firebase.database()); + }).toThrow(); + }); + }); + + it('will throw an error if a database reference is used in place of a Firebase auth instance',function(){ + expect(function(){ + $firebaseAuth(firebase.database().ref()); + }).toThrow(); + }); + + it('will not throw an error if an auth instance is provided',function(){ + $firebaseAuth(firebase.auth()); + }); + + describe('$signInWithCustomToken',function(){ + it('should return a promise', function() { + expect(authService.$signInWithCustomToken('myToken')).toBeAPromise(); + }); + + it('passes custom token to underlying method',function(){ + authService.$signInWithCustomToken('myToken'); + expect(auth.signInWithCustomToken).toHaveBeenCalledWith('myToken'); + }); + + it('will reject the promise if authentication fails',function(){ + var promise = authService.$signInWithCustomToken('myToken'); + wrapPromise(promise); + fakePromiseReject('myError'); + $timeout.flush(); + expect(failure).toEqual('myError'); + }); + + it('will resolve the promise upon authentication',function(){ + var promise = authService.$signInWithCustomToken('myToken'); + wrapPromise(promise); + fakePromiseResolve('myResult'); + $timeout.flush(); + expect(result).toEqual('myResult'); + }); + }); + + describe('$signInAnonymously',function(){ + it('should return a promise', function() { + expect(authService.$signInAnonymously()).toBeAPromise(); + }); + + it('passes options object to underlying method',function(){ + authService.$signInAnonymously(); + expect(auth.signInAnonymously).toHaveBeenCalled(); + }); + + it('will reject the promise if authentication fails',function(){ + var promise = authService.$signInAnonymously('myToken'); + wrapPromise(promise); + fakePromiseReject('myError'); + $timeout.flush(); + expect(failure).toEqual('myError'); + }); + + it('will resolve the promise upon authentication',function(){ + var promise = authService.$signInAnonymously('myToken'); + wrapPromise(promise); + fakePromiseResolve('myResult'); + $timeout.flush(); + expect(result).toEqual('myResult'); + }); + }); + + describe('$signInWithEmailWithPassword',function(){ + it('should return a promise', function() { + var email = 'abe@abe.abe'; + var password = 'abeabeabe'; + expect(authService.$signInWithEmailAndPassword(email, password)).toBeAPromise(); + }); + + it('passes options and credentials object to underlying method',function(){ + var email = 'abe@abe.abe'; + var password = 'abeabeabe'; + authService.$signInWithEmailAndPassword(email, password); + expect(auth.signInWithEmailAndPassword).toHaveBeenCalledWith( + email, password + ); + }); + + it('will reject the promise if authentication fails',function(){ + var promise = authService.$signInWithEmailAndPassword('', ''); + wrapPromise(promise); + fakePromiseReject('myError'); + $timeout.flush(); + expect(failure).toEqual('myError'); + }); + + it('will resolve the promise upon authentication',function(){ + var promise = authService.$signInWithEmailAndPassword('', ''); + wrapPromise(promise); + fakePromiseResolve('myResult'); + $timeout.flush(); + expect(result).toEqual('myResult'); + }); + }); + + describe('$signInWithPopup',function(){ + it('should return a promise', function() { + var provider = new firebase.auth.FacebookAuthProvider(); + expect(authService.$signInWithPopup(provider)).toBeAPromise(); + }); + + it('passes AuthProvider to underlying method',function(){ + var provider = new firebase.auth.FacebookAuthProvider(); + authService.$signInWithPopup(provider); + expect(auth.signInWithPopup).toHaveBeenCalledWith( + provider + ); + }); + + it('turns string to AuthProvider for underlying method',function(){ + var provider = 'facebook'; + authService.$signInWithPopup(provider); + expect(auth.signInWithPopup).toHaveBeenCalledWith( + jasmine.any(firebase.auth.FacebookAuthProvider) + ); + }); + + it('will reject the promise if authentication fails',function(){ + var promise = authService.$signInWithPopup('google'); + wrapPromise(promise); + fakePromiseReject('myError'); + $timeout.flush(); + expect(failure).toEqual('myError'); + }); + + it('will resolve the promise upon authentication',function(){ + var promise = authService.$signInWithPopup('google'); + wrapPromise(promise); + fakePromiseResolve('myResult'); + $timeout.flush(); + expect(result).toEqual('myResult'); + }); + }); + + describe('$signInWithRedirect',function(){ + it('should return a promise', function() { + var provider = new firebase.auth.FacebookAuthProvider(); + expect(authService.$signInWithRedirect(provider)).toBeAPromise(); + }); + + it('passes AuthProvider to underlying method',function(){ + var provider = new firebase.auth.FacebookAuthProvider(); + authService.$signInWithRedirect(provider); + expect(auth.signInWithRedirect).toHaveBeenCalledWith( + provider + ); + }); + + it('turns string to AuthProvider for underlying method',function(){ + var provider = 'facebook'; + authService.$signInWithRedirect(provider); + expect(auth.signInWithRedirect).toHaveBeenCalledWith( + jasmine.any(firebase.auth.FacebookAuthProvider) + ); + }); + + it('will reject the promise if authentication fails',function(){ + var promise = authService.$signInWithRedirect('google'); + wrapPromise(promise); + fakePromiseReject('myError'); + $timeout.flush(); + expect(failure).toEqual('myError'); + }); + + it('will resolve the promise upon authentication',function(){ + var promise = authService.$signInWithRedirect('google'); + wrapPromise(promise); + fakePromiseResolve('myResult'); + $timeout.flush(); + expect(result).toEqual('myResult'); + }); + }); + + describe('$signInWithCredential',function(){ + it('should return a promise', function() { + expect(authService.$signInWithCredential('CREDENTIAL')).toBeAPromise(); + }); + + it('passes credential object to underlying method',function(){ + var credential = '!!!!'; + authService.$signInWithCredential(credential); + expect(auth.signInWithCredential).toHaveBeenCalledWith( + credential + ); + }); + + it('will reject the promise if authentication fails',function(){ + var promise = authService.$signInWithCredential('CREDENTIAL'); + wrapPromise(promise); + fakePromiseReject('myError'); + $timeout.flush(); + expect(failure).toEqual('myError'); + }); + + it('will resolve the promise upon authentication',function(){ + var promise = authService.$signInWithCredential('CREDENTIAL'); + wrapPromise(promise); + fakePromiseResolve('myResult'); + $timeout.flush(); + expect(result).toEqual('myResult'); + }); + }); + + describe('$getAuth()',function(){ + it('returns getAuth() from backing auth instance',function(){ + expect(authService.$getAuth()).toEqual(auth.currentUser); + }); + }); + + describe('$signOut()',function(){ + it('should return a promise', function() { + expect(authService.$signOut()).toBeAPromise(); + }); + + it('will call signOut() on backing auth instance when user is signed in',function(){ + spyOn(authService._, 'getAuth').and.callFake(function () { + return {provider: 'facebook'}; + }); + authService.$signOut(); + expect(auth.signOut).toHaveBeenCalled(); + }); + + it('will call not signOut() on backing auth instance when user is not signed in',function(){ + spyOn(authService._, 'getAuth').and.callFake(function () { + return null; + }); + authService.$signOut(); + expect(auth.signOut).not.toHaveBeenCalled(); + }); + }); + + describe('$onAuthStateChanged()',function(){ + it('calls onAuthStateChanged() on the backing auth instance', function() { + function cb() {} + var ctx = {}; + authService.$onAuthStateChanged(cb, ctx); + expect(auth.onAuthStateChanged).toHaveBeenCalledWith(jasmine.any(Function)); + }); + + it('returns a deregistration function', function(){ + var cb = function () {}; + var ctx = {}; + expect(authService.$onAuthStateChanged(cb, ctx)).toEqual(jasmine.any(Function)) + }); + }); + + describe('$requireSignIn()',function(){ + it('will be resolved if user is logged in', function(done){ + var credentials = {provider: 'facebook'}; + spyOn(authService._, 'getAuth').and.callFake(function () { + return credentials; + }); + + authService.$requireSignIn() + .then(function (result) { + expect(result).toEqual(credentials); + done(); + }); + + fakePromiseResolve(credentials); + tick(); + }); + + it('will be rejected if user is not logged in', function(done){ + spyOn(authService._, 'getAuth').and.callFake(function () { + return null; + }); + + authService.$requireSignIn() + .catch(function (error) { + expect(error).toEqual('AUTH_REQUIRED'); + done(); + }); + + fakePromiseResolve(); + tick(); + }); + }); + + describe('$requireSignIn(requireEmailVerification)',function(){ + it('will be resolved if user is logged in and has a verified email address', function(done){ + var credentials = {provider: 'facebook', emailVerified: true}; + spyOn(authService._, 'getAuth').and.callFake(function () { + return credentials; + }); + + authService.$requireSignIn(true) + .then(function (result) { + expect(result).toEqual(credentials); + done(); + }); + + fakePromiseResolve(credentials); + tick(); + }); + + it('will be resolved if user is logged in and we ignore email verification', function(done){ + var credentials = {provider: 'facebook', emailVerified: false}; + spyOn(authService._, 'getAuth').and.callFake(function () { + return credentials; + }); + + authService.$requireSignIn(false) + .then(function (result) { + expect(result).toEqual(credentials); + done(); + }); + + fakePromiseResolve(credentials); + tick(); + }); + + it('will be rejected if user does not have a verified email address', function(done){ + var credentials = {provider: 'facebook', emailVerified: false}; + spyOn(authService._, 'getAuth').and.callFake(function () { + return credentials; + }); + + authService.$requireSignIn(true) + .catch(function (error) { + expect(error).toEqual('EMAIL_VERIFICATION_REQUIRED'); + done(); + }); + + fakePromiseResolve(credentials); + tick(); + }); + }); + + describe('$waitForSignIn()',function(){ + it('will be resolved with authData if user is logged in', function(done){ + var credentials = {provider: 'facebook'}; + spyOn(authService._, 'getAuth').and.callFake(function () { + return credentials; + }); + + authService.$waitForSignIn().then(function (result) { + expect(result).toEqual(credentials); + done(); + }); + + fakePromiseResolve(credentials); + tick(); + }); + + it('will be resolved with null if user is not logged in', function(done){ + spyOn(authService._, 'getAuth').and.callFake(function () { + return null; + }); + + authService.$waitForSignIn().then(function (result) { + expect(result).toEqual(null); + done(); + }); + + fakePromiseResolve(); + tick(); + }); + }); + + describe('$createUserWithEmailAndPassword()',function(){ + it('should return a promise', function() { + var email = 'somebody@somewhere.com'; + var password = '12345'; + expect(authService.$createUserWithEmailAndPassword(email, password)).toBeAPromise(); + }); + + it('passes email/password to method on backing ref',function(){ + var email = 'somebody@somewhere.com'; + var password = '12345'; + authService.$createUserWithEmailAndPassword(email, password); + expect(auth.createUserWithEmailAndPassword).toHaveBeenCalledWith( + email, password); + }); + + it('will reject the promise if creation fails',function(){ + var promise = authService.$createUserWithEmailAndPassword('abe@abe.abe', '12345'); + wrapPromise(promise); + fakePromiseReject('myError'); + $timeout.flush(); + expect(failure).toEqual('myError'); + }); + + it('will resolve the promise upon creation',function(){ + var promise = authService.$createUserWithEmailAndPassword('abe@abe.abe', '12345'); + wrapPromise(promise); + fakePromiseResolve('myResult'); + $timeout.flush(); + expect(result).toEqual('myResult'); + }); + }); + + describe('$updatePassword()',function() { + it('should return a promise', function() { + var newPassword = 'CatInDatHat'; + expect(authService.$updatePassword(newPassword)).toBeAPromise(); + }); + + it('passes new password to method on backing auth instance',function(done) { + spyOn(authService._, 'getAuth').and.callFake(function () { + return { + updatePassword: function (password) { + expect(password).toBe(newPassword); + done(); + } + }; + }); + + var newPassword = 'CatInDatHat'; + authService.$updatePassword(newPassword); + }); + + it('will reject the promise if creation fails',function(){ + spyOn(authService._, 'getAuth').and.callFake(function () { + return { + updatePassword: function (password) { + return fakePromise(); + } + }; + }); + + var promise = authService.$updatePassword('PASSWORD'); + wrapPromise(promise); + fakePromiseReject('myError'); + $timeout.flush(); + expect(failure).toEqual('myError'); + }); + + it('will resolve the promise upon creation',function(){ + spyOn(authService._, 'getAuth').and.callFake(function () { + return { + updatePassword: function (password) { + return fakePromise(); + } + }; + }); + + var promise = authService.$updatePassword('PASSWORD'); + wrapPromise(promise); + fakePromiseResolve('myResult'); + $timeout.flush(); + expect(result).toEqual('myResult'); + }); + }); + + describe('$updateEmail()',function() { + it('should return a promise', function() { + var newEmail = 'abe@abe.abe'; + expect(authService.$updateEmail(newEmail)).toBeAPromise(); + }); + + it('passes new email to method on backing auth instance',function(done) { + spyOn(authService._, 'getAuth').and.callFake(function () { + return { + updateEmail: function (email) { + expect(email).toBe(newEmail); + done(); + } + }; + }); + + var newEmail = 'abe@abe.abe'; + authService.$updateEmail(newEmail); + }); + + it('will reject the promise if creation fails',function(){ + spyOn(authService._, 'getAuth').and.callFake(function () { + return { + updateEmail: function (email) { + return fakePromise(); + } + }; + }); + + var promise = authService.$updateEmail('abe@abe.abe'); + wrapPromise(promise); + fakePromiseReject('myError'); + $timeout.flush(); + expect(failure).toEqual('myError'); + }); + + it('will resolve the promise upon creation',function(){ + spyOn(authService._, 'getAuth').and.callFake(function () { + return { + updateEmail: function (email) { + return fakePromise(); + } + }; + }); + + var promise = authService.$updateEmail('abe@abe.abe'); + wrapPromise(promise); + fakePromiseResolve('myResult'); + $timeout.flush(); + expect(result).toEqual('myResult'); + }); + }); + + describe('$deleteUser()',function(){ + it('should return a promise', function() { + expect(authService.$deleteUser()).toBeAPromise(); + }); + + it('calls delete on backing auth instance',function(done) { + spyOn(authService._, 'getAuth').and.callFake(function () { + return { + delete: function () { + done(); + } + }; + }); + authService.$deleteUser(); + }); + + it('will reject the promise if creation fails',function(){ + spyOn(authService._, 'getAuth').and.callFake(function () { + return { + delete: function () { + return fakePromise(); + } + }; + }); + + var promise = authService.$deleteUser(); + wrapPromise(promise); + fakePromiseReject('myError'); + $timeout.flush(); + expect(failure).toEqual('myError'); + }); + + it('will resolve the promise upon creation',function(){ + spyOn(authService._, 'getAuth').and.callFake(function () { + return { + delete: function () { + return fakePromise(); + } + }; + }); + + var promise = authService.$deleteUser(); + wrapPromise(promise); + fakePromiseResolve('myResult'); + $timeout.flush(); + expect(result).toEqual('myResult'); + }); + }); + + describe('$sendPasswordResetEmail()',function(){ + it('should return a promise', function() { + var email = 'somebody@somewhere.com'; + expect(authService.$sendPasswordResetEmail(email)).toBeAPromise(); + }); + + it('passes email to method on backing auth instance',function(){ + var email = 'somebody@somewhere.com'; + authService.$sendPasswordResetEmail(email); + expect(auth.sendPasswordResetEmail).toHaveBeenCalledWith(email); + }); + + it('will reject the promise if creation fails',function(){ + var promise = authService.$sendPasswordResetEmail('abe@abe.abe'); + wrapPromise(promise); + fakePromiseReject('myError'); + $timeout.flush(); + expect(failure).toEqual('myError'); + }); + + it('will resolve the promise upon creation',function(){ + var promise = authService.$sendPasswordResetEmail('abe@abe.abe'); + wrapPromise(promise); + fakePromiseResolve('myResult'); + $timeout.flush(); + expect(result).toEqual('myResult'); + }); + }); +}); diff --git a/tests/unit/FirebaseAuthService.spec.js b/tests/unit/FirebaseAuthService.spec.js new file mode 100644 index 00000000..f5c70ac1 --- /dev/null +++ b/tests/unit/FirebaseAuthService.spec.js @@ -0,0 +1,22 @@ +'use strict'; +describe('$firebaseAuthService', function () { + beforeEach(function () { + module('firebase.auth') + }); + + describe('', function() { + + var $firebaseAuthService; + beforeEach(function() { + module('firebase'); + inject(function (_$firebaseAuthService_) { + $firebaseAuthService = _$firebaseAuthService_; + }); + }); + + it('should exist', inject(function() { + expect($firebaseAuthService).not.toBe(null); + })); + + }); +}); diff --git a/tests/unit/FirebaseObject.spec.js b/tests/unit/FirebaseObject.spec.js new file mode 100644 index 00000000..d2ff1b5a --- /dev/null +++ b/tests/unit/FirebaseObject.spec.js @@ -0,0 +1,835 @@ +describe('$firebaseObject', function() { + 'use strict'; + var $firebaseObject, $utils, $rootScope, $timeout, $q, tick, obj, testutils, $interval, log; + + var DEFAULT_ID = 'REC1'; + var FIXTURE_DATA = { + aString: 'alpha', + aNumber: 1, + aBoolean: false, + anObject: { bString: 'bravo' } + }; + + beforeEach(function () { + log = { + error:[] + }; + + module('firebase.database'); + module('testutils',function($provide){ + $provide.value('$log',{ + error:function(){ + log.error.push(Array.prototype.slice.call(arguments)); + } + }) + }); + inject(function (_$interval_, _$firebaseObject_, _$timeout_, $firebaseUtils, _$rootScope_, _$q_, _testutils_) { + $firebaseObject = _$firebaseObject_; + $timeout = _$timeout_; + $interval = _$interval_; + $utils = $firebaseUtils; + $rootScope = _$rootScope_; + $q = _$q_; + testutils = _testutils_; + + firebase.database.enableLogging(function () {tick()}); + tick = function () { + setTimeout(function() { + $q.defer(); + $rootScope.$digest(); + try { + $timeout.flush(); + } catch (err) { + // This throws an error when there is nothing to flush... + } + }) + }; + + obj = makeObject(FIXTURE_DATA); + }); + }); + + describe('constructor', function() { + it('should set the record id', function() { + expect(obj.$id).toEqual(obj.$ref().key); + }); + + it('should accept a query', function() { + var obj = makeObject(FIXTURE_DATA, stubRef().limitToLast(1).startAt(null)); + + obj.$$updated(testutils.snap({foo: 'bar'})); + expect(obj).toEqual(jasmine.objectContaining({foo: 'bar'})); + }); + + it('should apply $$defaults if they exist', function() { + var F = $firebaseObject.$extend({ + $$defaults: {aNum: 0, aStr: 'foo', aBool: false} + }); + var ref = stubRef(); + var obj = new F(ref); + + expect(obj).toEqual(jasmine.objectContaining({aNum: 0, aStr: 'foo', aBool: false})); + }) + }); + + describe('$save', function () { + it('should call $firebase.$set', function (done) { + var spy = spyOn(firebase.database.Reference.prototype, 'set').and.callThrough(); + obj.foo = 'bar'; + obj.$save().then(function () { + expect(spy).toHaveBeenCalled(); + done(); + }); + }); + + it('should return a promise', function () { + expect(obj.$save()).toBeAPromise(); + }); + + it('should resolve promise to the ref for this object', function (done) { + var whiteSpy = jasmine.createSpy('resolve'); + var blackSpy = jasmine.createSpy('reject'); + obj.$save() + .then(whiteSpy, blackSpy) + .then(function () { + expect(whiteSpy).toHaveBeenCalled(); + expect(blackSpy).not.toHaveBeenCalled(); + done(); + }); + }); + + it('should reject promise on failure', function (done) { + var whiteSpy = jasmine.createSpy('resolve'); + var blackSpy = jasmine.createSpy('reject'); + + obj['child/invalid'] = true; + obj.$save().then(whiteSpy, blackSpy) + .finally(function () { + expect(blackSpy).toHaveBeenCalled(); + expect(whiteSpy).not.toHaveBeenCalled(); + done(); + }); + }); + + it('should trigger watch event', function(done) { + var spy = jasmine.createSpy('$watch'); + obj.$watch(spy); + obj.foo = 'watchtest'; + obj.$save() + .then(function () { + expect(spy).toHaveBeenCalledWith(jasmine.objectContaining({event: 'value', key: obj.$id})); + done(); + }); + }); + + it('should work on a query', function(done) { + var ref = stubRef(); + ref.set({foo: 'baz'}); + var query = ref.limitToLast(3); + var spy = spyOn(firebase.database.Reference.prototype, 'update').and.callThrough(); + var obj = $firebaseObject(query); + + obj.foo = 'bar'; + obj.$save().then(function () { + expect(spy).toHaveBeenCalledWith({foo: 'bar'}, jasmine.any(Function)); + done(); + }); + }); + }); + + describe('$loaded', function () { + it('should return a promise', function () { + expect(obj.$loaded()).toBeAPromise(); + }); + + it('should resolve when all server data is downloaded', function (done) { + var whiteSpy = jasmine.createSpy('resolve'); + var blackSpy = jasmine.createSpy('reject'); + var obj = makeObject(); + + obj.$loaded() + .then(whiteSpy, blackSpy) + .then(function () { + expect(whiteSpy).toHaveBeenCalledWith(obj); + expect(blackSpy).not.toHaveBeenCalled(); + done(); + }); + + obj.key = "value"; + obj.$save(); + }); + + it('should reject if the ready promise is rejected', function (done) { + var whiteSpy = jasmine.createSpy('resolve'); + var blackSpy = jasmine.createSpy('reject'); + var err = new Error('test_fail'); + + spyOn(firebase.database.Reference.prototype, "once").and.callFake(function (event, cb, cancel_cb) { + cancel_cb(err); + }); + + var obj = makeObject(); + + obj.$loaded() + .then(whiteSpy, blackSpy) + .finally(function () { + expect(whiteSpy).not.toHaveBeenCalled(); + expect(blackSpy).toHaveBeenCalledWith(err); + done(); + }); + }); + + it('should resolve to the FirebaseObject instance', function (done) { + var spy = jasmine.createSpy('loaded'); + obj.$loaded().then(spy).then(function () { + expect(spy).toHaveBeenCalledWith(obj); + done() + }); + }); + + it('should contain all data at the time $loaded is called', function (done) { + var obj = makeObject(FIXTURE_DATA); + obj.$loaded().then(function (data) { + expect(data).toEqual(jasmine.objectContaining(FIXTURE_DATA)); + done(); + }); + obj.$ref().set(FIXTURE_DATA); + }); + + it('should trigger if attached before load completes', function(done) { + var obj = makeObject(FIXTURE_DATA); + + obj.$loaded().then(function (data) { + expect(data).toEqual(jasmine.objectContaining(FIXTURE_DATA)); + done(); + }); + }); + + it('should trigger if attached after load completes', function(done) { + var obj = makeObject(FIXTURE_DATA); + + obj.$loaded().then(function (data) { + expect(data).toEqual(jasmine.objectContaining(FIXTURE_DATA)); + done(); + }); + }); + + it('should resolve properly if function passed directly into $loaded', function(done) { + var obj = makeObject(FIXTURE_DATA); + + obj.$loaded(function (data) { + expect(data).toEqual(jasmine.objectContaining(FIXTURE_DATA)); + done(); + }); + }); + + it('should reject properly if function passed directly into $loaded', function(done) { + var whiteSpy = jasmine.createSpy('resolve'); + var err = new Error('test_fail'); + + spyOn(firebase.database.Reference.prototype, "on").and.callFake(function (event, cb, cancel_cb) { + cancel_cb(err); + }); + + var obj = $firebaseObject(stubRef()); + + obj.$loaded(whiteSpy, function () { + expect(whiteSpy).not.toHaveBeenCalled(); + done(); + }); + }); + }); + + describe('$resolved', function () { + it('should return false on init', function () { + var ref = stubRef(); + var obj = $firebaseObject(ref); + expect(obj.$resolved).toBe(false); + }); + + it('should return true once $loaded() promise is resolved', function () { + var obj = makeObject(); + + obj.$loaded() + .finally(function () { + expect(obj.$resolved).toBe(true); + done(); + }); + }); + + it('should return true once $loaded() promise is rejected', function () { + var err = new Error('test_fail'); + + spyOn(firebase.database.Reference.prototype, "once").and.callFake(function (event, cb, cancel_cb) { + cancel_cb(err); + }); + + var obj = makeObject(); + + obj.$loaded() + .finally(function () { + expect(obj.$resolved).toBe(true); + done(); + }); + }); + }); + + describe('$ref', function () { + it('should return the Firebase instance that created it', function () { + var ref = stubRef(); + var obj = $firebaseObject(ref); + expect(obj.$ref()).toBe(ref); + }); + }); + + describe('$bindTo', function () { + it('should return a promise', function () { + var res = obj.$bindTo($rootScope.$new(), 'test'); + expect(res).toBeAPromise(); + }); + + it('should resolve to an off function', function (done) { + obj.$bindTo($rootScope.$new(), 'test').then(function (off) { + expect(off).toBeA('function'); + done(); + }); + }); + + it('should have data when it resolves', function (done) { + var obj = makeObject(FIXTURE_DATA); + + obj.$bindTo($rootScope.$new(), 'test').then(function () { + expect(obj).toEqual(jasmine.objectContaining(FIXTURE_DATA)); + done(); + }); + }); + + it('should have data in $scope when resolved', function(done) { + var data = {"a": true}; + var obj = makeObject(data); + var $scope = $rootScope.$new(); + + obj.$bindTo($scope, 'test').then(function () { + expect($scope.test).toEqual(jasmine.objectContaining(data)); + expect($scope.test.$id).toBe(obj.$id); + done(); + }); + }); + + it('should send local changes to $firebase.$set', function (done) { + var obj = makeObject(FIXTURE_DATA); + var spy = spyOn(firebase.database.Reference.prototype, 'set').and.callThrough(); + var $scope = $rootScope.$new(); + var ready = false; + + obj.$bindTo($scope, 'test') + .then(function () { + $scope.test.bar = 'baz'; + ready = true; + }); + + obj.$ref().on('value', function (snapshot) { + if (!ready) return; + expect(spy).toHaveBeenCalledWith(jasmine.objectContaining({bar: 'baz'}), jasmine.any(Function)); + done(); + }); + }); + + it('should allow data to be set inside promise callback', function (done) { + var ref = obj.$ref(); + spyOn(ref, 'set'); + var $scope = $rootScope.$new(); + var oldData = { 'old': true } + var newData = { 'bar': 'foo' }; + var spy = jasmine.createSpy('resolve').and.callFake(function () { + $scope.test = newData; + }); + obj.$bindTo($scope, 'test').then(spy).then(function () { + expect(spy).toHaveBeenCalled(); + expect($scope.test).toEqual(jasmine.objectContaining(newData)); + expect(ref.set).toHaveBeenCalledWith(oldData); + done(); + }); + + ref.set(oldData); + }); + + it('should apply server changes to scope variable', function () { + var $scope = $rootScope.$new(); + obj.$bindTo($scope, 'test'); + $timeout.flush(); + obj.$$updated(fakeSnap({foo: 'bar'})); + obj.$$notify(); + expect($scope.test).toEqual({foo: 'bar', $id: obj.$id, $priority: obj.$priority}); + }); + + it('will replace the object on scope if new server value is not deeply equal', function () { + var $scope = $rootScope.$new(); + obj.$bindTo($scope, 'test'); + $timeout.flush(); + obj.$$updated(fakeSnap({foo: 'bar'})); + obj.$$notify(); + var oldTest = $scope.test; + obj.$$updated(fakeSnap({foo: 'baz'})); + obj.$$notify(); + expect($scope.test === oldTest).toBe(false); + }); + + it('will leave the scope value alone if new server value is deeply equal', function () { + var $scope = $rootScope.$new(); + obj.$bindTo($scope, 'test'); + $timeout.flush(); + obj.$$updated(fakeSnap({foo: 'bar'})); + obj.$$notify(); + var oldTest = $scope.test; + obj.$$updated(fakeSnap({foo: 'bar'})); + obj.$$notify(); + expect($scope.test === oldTest).toBe(true); + }); + + it('should stop binding when off function is called', function (done) { + var origData = $utils.scopeData(obj); + var $scope = $rootScope.$new(); + var spy = jasmine.createSpy('$bindTo').and.callFake(function (off) { + expect($scope.obj).toEqual(origData); + off(); + }); + obj.$bindTo($scope, 'obj').then(spy).then(function () { + obj.$$updated(fakeSnap({foo: 'bar'})) + expect(spy).toHaveBeenCalled(); + expect($scope.obj).toEqual(origData); + done(); + }); + $rootScope.$digest(); + }); + + it('should not destroy remote data if local is pre-set', function () { + var origValue = $utils.scopeData(obj); + var $scope = $rootScope.$new(); + $scope.test = {foo: true}; + obj.$bindTo($scope, 'test'); + expect($utils.scopeData(obj)).toEqual(origValue); + }); + + it('should not fail if remote data is null', function (done) { + var $scope = $rootScope.$new(); + var obj = makeObject(FIXTURE_DATA); + obj.$bindTo($scope, 'test'); + obj.$ref().set(null).then(function () { + $rootScope.$digest(); + expect($scope.test).toEqual({$value: null, $id: obj.$id, $priority: obj.$priority}); + done(); + }); + }); + + it('should delete $value if set to an object', function (done) { + var $scope = $rootScope.$new(); + var obj = makeObject(null); + var ready = false; + + obj.$bindTo($scope, 'test') + .then(function () { + expect($scope.test).toEqual({$value: null, $id: obj.$id, $priority: obj.$priority}); + }).then(function () { + $scope.test.text = "hello"; + ready = true; + }) + + $scope.$watch('test.$value', function (val) { + if (val === null) return; + expect(val).toBe(undefined); + done(); + }); + }); + + it('should update $priority if $priority changed in $scope', function (done) { + var $scope = $rootScope.$new(); + var ref = stubRef(); + var obj = $firebaseObject(ref); + var ready = false; + + var spy = spyOn(firebase.database.Reference.prototype, 'set').and.callThrough(); + obj.$value = 'foo'; + obj.$save().then(function () { + return obj.$bindTo($scope, 'test'); + }) + .then(function () { + $scope.test.$priority = 9999; + ready = true; + }); + + obj.$ref().on("value", function (snapshot) { + if (!ready) return + expect(spy).toHaveBeenCalledWith(jasmine.objectContaining({'.priority': 9999}), jasmine.any(Function)); + done(); + }); + }); + + it('should update $value if $value changed in $scope', function () { + var $scope = $rootScope.$new(); + var ref = stubRef(); + var obj = $firebaseObject(ref); + + obj.$$updated(testutils.refSnap(ref, 'foo', null)); + expect(obj.$value).toBe('foo'); + var spy = spyOn(firebase.database.Reference.prototype, 'set'); + obj.$bindTo($scope, 'test') + .then(function () { + $scope.test.$value = 'bar'; + }) + .then(function () { + expect(spy).toHaveBeenCalledWith(jasmine.objectContaining({'.value': 'bar'}), jasmine.any(Function)); + }); + }); + + it('should only call $$scopeUpdated once if both metaVars and properties change in the same $digest', function(done){ + var $scope = $rootScope.$new(); + var ref = stubRef(); + ref.setWithPriority({text:'hello'}, 3); + var obj = $firebaseObject(ref); + + var old$scopeUpdated = obj.$$scopeUpdated; + var callCount = 0; + var ready = false; + + obj.$bindTo($scope, 'test') + .then(function () { + expect($scope.test).toEqual({text:'hello', $id: obj.$id, $priority: 3}); + }) + .then(function () { + obj.$$scopeUpdated = function(){ + callCount++; + done(); + return old$scopeUpdated.apply(this,arguments); + }; + + $scope.test.text='goodbye'; + $scope.test.$priority=4; + ready = true; + }); + + obj.$ref().on("value", function (snapshot) { + if (!ready) return; + expect(callCount).toBe(1); + done(); + }); + }); + + it('should throw error if double bound', function(done) { + var $scope = $rootScope.$new(); + var aSpy = jasmine.createSpy('firstBind'); + var bResolve = jasmine.createSpy('secondBindResolve'); + var bReject = jasmine.createSpy('secondBindReject'); + obj.$bindTo($scope, 'a') + .then(aSpy) + .then(function () { + expect(aSpy).toHaveBeenCalled(); + return obj.$bindTo($scope, 'b').then(bResolve, bReject); + }) + .then(function () { + expect(bResolve).not.toHaveBeenCalled(); + expect(bReject).toHaveBeenCalled(); + done(); + }); + }); + + it('should accept another binding after off is called', function(done) { + var $scope = $rootScope.$new(); + + var bSpy = jasmine.createSpy('secondResolve'); + var bFail = jasmine.createSpy('secondReject'); + obj.$bindTo($scope, 'a') + .then(function (unbind) { + unbind(); + }) + .then(function () { + return obj.$bindTo($scope, 'b'); + }) + .then(bSpy, bFail) + .then(function () { + expect(bSpy).toHaveBeenCalled(); + expect(bFail).not.toHaveBeenCalled(); + done(); + }); + }); + }); + + describe('$watch', function(){ + it('should return a deregistration function',function(done){ + var spy = jasmine.createSpy('$watch'); + var off = obj.$watch(spy); + obj.foo = 'watchtest'; + obj.$save() + .then(function () { + expect(spy).toHaveBeenCalled(); + spy.calls.reset(); + off(); + expect(spy).not.toHaveBeenCalled(); + done(); + }); + }); + + it('additional calls to the deregistration function should be silently ignored',function(done){ + var spy = jasmine.createSpy('$watch'); + var off = obj.$watch(spy); + off(); + off(); + + obj.foo = 'watchtest'; + obj.$save() + .then(function () { + expect(spy).not.toHaveBeenCalled(); + done(); + }); + }); + }); + + describe('$remove', function() { + it('should return a promise', function() { + expect(obj.$remove()).toBeAPromise(); + }); + + it('should set $value to null and remove any local keys', function() { + expect($utils.dataKeys(obj).sort()).toEqual($utils.dataKeys(FIXTURE_DATA).sort()); + obj.$remove(); + expect($utils.dataKeys(obj)).toEqual([]); + }); + + it('should call remove on the Firebase ref', function(done) { + obj.$ref().set("Hello!"); + obj.$remove() + obj.$ref().on("value", function (ss) { + if (ss.val() == null) { + done(); + } + }) + }); + + it('should delete a primitive value', function() { + var snap = fakeSnap('foo'); + obj.$$updated(snap); + + expect(obj.$value).toBe('foo'); + obj.$remove().then(function () { + expect(obj.$value).toBe(null); + }); + }); + + it('should trigger a value event for $watch listeners', function(done) { + var spy = jasmine.createSpy('$watch listener'); + + obj.$watch(spy); + obj.$remove().then(function () { + expect(spy).toHaveBeenCalledWith({ event: 'value', key: obj.$id }); + done(); + }); + }); + + it('should work on a query', function(done) { + var ref = stubRef(); + ref.set({foo: 'bar'}); + var query = ref.limitToLast(3); + var obj = $firebaseObject(query); + + obj.$loaded().then(function () { + expect(obj.foo).toBe('bar'); + }).then(function () { + return obj.$remove(); + }).then(function () { + expect(obj.$value).toBe(null); + done(); + }); + }); + }); + + describe('$destroy', function () { + it('should call off on Firebase ref', function () { + var spy = spyOn(obj.$ref(), 'off'); + obj.$destroy(); + expect(spy).toHaveBeenCalled(); + }); + + it('should dispose of any bound instance', function (done) { + var $scope = $rootScope.$new(); + spyOnWatch($scope); + // now bind to scope and destroy to see what happens + obj.$bindTo($scope, 'foo').then(function () { + expect($scope.$watch).toHaveBeenCalled(); + return obj.$destroy(); + }).then(function () { + expect($scope.$watch.$$$offSpy).toHaveBeenCalled(); + done(); + }); + }); + + it('should unbind if scope is destroyed', function (done) { + var $scope = $rootScope.$new(); + spyOnWatch($scope); + obj.$bindTo($scope, 'foo') + .then(function () { + expect($scope.$watch).toHaveBeenCalled(); + $scope.$emit('$destroy'); + expect($scope.$watch.$$$offSpy).toHaveBeenCalled(); + done(); + }); + }); + }); + + describe('$extend', function () { + it('should preserve child prototype', function () { + function Extend() { + $firebaseObject.apply(this, arguments); + } + Extend.prototype.foo = function () {}; + var ref = stubRef(); + $firebaseObject.$extend(Extend); + var arr = new Extend(ref); + expect(arr.foo).toBeA('function'); + }); + + it('should return child class', function () { + function A() {} + var res = $firebaseObject.$extend(A); + expect(res).toBe(A); + }); + + it('should be instanceof $firebaseObject', function () { + function A() {} + $firebaseObject.$extend(A); + expect(new A(stubRef())).toBeInstanceOf($firebaseObject); + }); + + it('should add on methods passed into function', function () { + function foo() { + return 'foo'; + } + var F = $firebaseObject.$extend({foo: foo}); + var res = F(stubRef()); + expect(res.$$updated).toBeA('function'); + expect(res.foo).toBeA('function'); + expect(res.foo()).toBe('foo'); + }); + + + it('should work with the new keyword', function() { + var fn = function() {}; + var Res = $firebaseObject.$extend({foo: fn}); + expect(new Res(stubRef()).foo).toBeA('function'); + }); + + it('should work without the new keyword', function() { + var fn = function() {}; + var Res = $firebaseObject.$extend({foo: fn}); + expect(Res(stubRef()).foo).toBeA('function'); + }); + }); + + describe('$$updated', function () { + it('should add keys to local data', function () { + obj.$$updated(fakeSnap({'key1': true, 'key2': 5})); + expect(obj.key1).toBe(true); + expect(obj.key2).toBe(5); + }); + + it('should remove old keys', function () { + var keys = ['aString', 'aNumber', 'aBoolean', 'anObject']; + keys.forEach(function(k) { + expect(obj).toHaveKey(k); + }); + obj.$$updated(fakeSnap(null)); + + keys.forEach(function (k) { + expect(obj).not.toHaveKey(k); + }); + }); + + it('should assign null to $value', function() { + obj.$$updated(fakeSnap(null)); + expect(obj.$value).toBe(null); + }); + + it('should assign primitive value to $value', function () { + obj.$$updated(fakeSnap(false)); + expect(obj.$value).toBe(false); + }); + + it('should remove other keys when setting primitive', function() { + var keys = Object.keys(obj); + }); + + it('should preserve the id', function() { + obj.$id = 'change_id_for_test'; + obj.$$updated(fakeSnap(true)); + expect(obj.$id).toBe('change_id_for_test'); + }); + + it('should set the priority', function() { + obj.$priority = false; + obj.$$updated(fakeSnap(null, true)); + expect(obj.$priority).toBe(true); + }); + + it('should apply $$defaults if they exist', function() { + var F = $firebaseObject.$extend({ + $$defaults: {baz: 'baz', aString: 'bravo'} + }); + var obj = new F(stubRef()); + obj.$$updated(fakeSnap(FIXTURE_DATA)); + expect(obj.aString).toBe(FIXTURE_DATA.aString); + expect(obj.baz).toBe('baz'); + }); + }); + + describe('$$error',function(){ + it('will log an error',function(){ + obj.$$error(new Error()); + expect(log.error).toHaveLength(1); + }); + + it('will call $destroy',function(){ + obj.$destroy = jasmine.createSpy('$destroy'); + var error = new Error(); + obj.$$error(error); + expect(obj.$destroy).toHaveBeenCalledWith(error); + }); + }); + + var pushCounter = 1; + + function fakeSnap(data, pri) { + return testutils.refSnap(testutils.ref('data/a'), data, pri); + } + + function stubRef() { + return firebase.database().ref().push(); + } + + function makeObject(initialData, ref) { + if( !ref ) { + ref = stubRef(); + } + var obj = $firebaseObject(ref); + if (angular.isDefined(initialData)) { + ref.ref.set(initialData); + $rootScope.$digest(); + } + return obj; + } + + function spyOnWatch($scope) { + // hack on $scope.$watch to return our spy instead of the usual + // so that we can determine if it gets called + var _watch = $scope.$watch; + spyOn($scope, '$watch').and.callFake(function (varName, callback) { + // call the real watch method and store the real off method + var _off = _watch.call($scope, varName, callback); + // replace it with our 007 + var offSpy = jasmine.createSpy('off method for $watch').and.callFake(function () { + // call the real off method + _off(); + }); + $scope.$watch.$$$offSpy = offSpy; + return offSpy; + }); + } +}); diff --git a/tests/unit/FirebaseStorage.spec.js b/tests/unit/FirebaseStorage.spec.js new file mode 100644 index 00000000..f2a1ef54 --- /dev/null +++ b/tests/unit/FirebaseStorage.spec.js @@ -0,0 +1,327 @@ +'use strict'; +describe('$firebaseStorage', function () { + var $firebaseStorage; + var URL = 'https://oss-test.firebaseio.com'; + + beforeEach(function () { + module('firebase.storage'); + }); + + describe('', function () { + + var $firebaseStorage; + var $q; + var $rootScope; + var $firebaseUtils; + beforeEach(function () { + module('firebase.storage'); + inject(function (_$firebaseStorage_, _$q_, _$rootScope_, _$firebaseUtils_) { + $firebaseStorage = _$firebaseStorage_; + $q = _$q_; + $rootScope = _$rootScope_; + $firebaseUtils = _$firebaseUtils_; + }); + }); + + function setupPutTests(fileOrRawString, mockTask, isPutString) { + var ref = firebase.storage().ref('thing'); + var task = null; + var storage = $firebaseStorage(ref); + var putMethod = isPutString ? 'putString': 'put'; + var metadata = { + contentType: 'image/jpeg' + }; + // If a MockTask is provided use it as the + // return value of the spy on put + if (mockTask) { + spyOn(ref, putMethod).and.returnValue(mockTask); + } else { + spyOn(ref, putMethod); + } + if(isPutString) { + task = storage.$putString(fileOrRawString, 'raw', metadata); + } else { + task = storage.$put(fileOrRawString, metadata); + } + return { + ref: ref, + task: task + }; + } + + function setupPutStringTests(rawString, mockTask) { + return setupPutTests(rawString, mockTask, true); + } + + it('should exist', inject(function () { + expect($firebaseStorage).not.toBe(null); + })); + + it('should create an instance', function () { + var ref = firebase.storage().ref('thing'); + var storage = $firebaseStorage(ref); + expect(storage).not.toBe(null); + }); + + it('should throw error given a non-reference', function() { + function errorWrapper() { + var storage = $firebaseStorage(null); + } + expect(errorWrapper).toThrow(); + }); + + describe('$firebaseStorage.utils', function () { + + describe('_unwrapStorageSnapshot', function () { + + it('should unwrap the snapshot', function () { + var mockSnapshot = { + bytesTransferred: 0, + downloadURL: 'url', + metadata: {}, + ref: {}, + state: {}, + task: {}, + totalBytes: 0, + randomAttr: 'rando', // gets removed + anotherRando: 'woooo' // gets removed + }; + var unwrapped = $firebaseStorage.utils._unwrapStorageSnapshot(mockSnapshot); + expect(unwrapped).toEqual({ + bytesTransferred: 0, + downloadURL: 'url', + metadata: {}, + ref: {}, + state: {}, + task: {}, + totalBytes: 0 + }); + }); + + }); + + describe('_isStorageRef', function () { + + it('should determine a storage ref', function () { + var ref = firebase.storage().ref('thing'); + var isTrue = $firebaseStorage.utils._isStorageRef(ref); + var isFalse = $firebaseStorage.utils._isStorageRef(true); + expect(isTrue).toEqual(true); + expect(isFalse).toEqual(false); + }); + + }); + + describe('_assertStorageRef', function () { + it('should not throw an error if a storage ref is passed', function () { + var ref = firebase.storage().ref('thing'); + function errorWrapper() { + $firebaseStorage.utils._assertStorageRef(ref); + } + expect(errorWrapper).not.toThrow(); + }); + + it('should throw an error if a storage ref is passed', function () { + function errorWrapper() { + $firebaseStorage.utils._assertStorageRef(null); + } + expect(errorWrapper).toThrow(); + }); + }); + + }); + + describe('$firebaseStorage', function() { + + describe('$put', function() { + + it('should call a storage ref put', function () { + var mockTask = new MockTask(); + var setup = setupPutTests('file', mockTask); + var ref = setup.ref; + expect(ref.put).toHaveBeenCalledWith('file', { + contentType: 'image/jpeg' + }); + }); + + it('should return the observer functions', function () { + var mockTask = new MockTask(); + var setup = setupPutTests('file', mockTask); + var task = setup.task; + expect(task.$progress).toEqual(jasmine.any(Function)); + expect(task.$error).toEqual(jasmine.any(Function)); + expect(task.$complete).toEqual(jasmine.any(Function)); + }); + + it('should return a promise with then and catch', function() { + var mockTask = new MockTask(); + var setup = setupPutTests('file', mockTask); + var task = setup.task; + expect(task.then).toEqual(jasmine.any(Function)); + expect(task.catch).toEqual(jasmine.any(Function)); + }); + + it('$cancel', function() { + var mockTask = new MockTask(); + spyOn(mockTask, 'cancel'); + var setup = setupPutTests('file', mockTask); + var task = setup.task; + task.$cancel(); + expect(mockTask.cancel).toHaveBeenCalled(); + }); + + it('$resume', function() { + var mockTask = new MockTask(); + spyOn(mockTask, 'resume'); + var setup = setupPutTests('file', mockTask); + var task = setup.task; + task.$resume(); + expect(mockTask.resume).toHaveBeenCalled(); + }); + + it('$pause', function() { + var mockTask = new MockTask(); + spyOn(mockTask, 'pause') + var setup = setupPutTests('file', mockTask); + var task = setup.task; + task.$pause(); + expect(mockTask.pause).toHaveBeenCalled(); + }); + + it('then', function() { + var mockTask = new MockTask(); + spyOn(mockTask, 'then'); + var setup = setupPutTests('file', mockTask); + var task = setup.task; + task.then(); + expect(mockTask.then).toHaveBeenCalled(); + }); + + it('catch', function() { + var mockTask = new MockTask(); + spyOn(mockTask, 'catch'); + var setup = setupPutTests('file', mockTask); + var task = setup.task; + task.catch(); + expect(mockTask.catch).toHaveBeenCalled(); + }); + + it('$snapshot', function() { + var mockTask = new MockTask(); + var setup = null; + var task = null; + mockTask.on('', null, null, function() {}); + mockTask.complete(); + setup = setupPutTests('file', mockTask); + task = setup.task; + expect(mockTask.snapshot).toEqual(task.$snapshot); + }); + + }); + + describe('$putString', function() { + it('should call a storage ref putString', function () { + var mockTask = new MockTask(); + var setup = setupPutStringTests('string data', mockTask); + var ref = setup.ref; + expect(ref.putString).toHaveBeenCalledWith('string data', 'raw', { + contentType: 'image/jpeg' + }); + }); + }); + + describe('$toString', function() { + it('should call a storage ref to string', function() { + var ref = firebase.storage().ref('myfile'); + var storage = $firebaseStorage(ref); + spyOn(ref, 'toString'); + storage.$toString(); + expect(ref.toString).toHaveBeenCalled(); + }); + + it('should return the proper gs:// URL', function() { + var ref = firebase.storage().ref('myfile'); + var storage = $firebaseStorage(ref); + var stringValue = storage.$toString(); + expect(stringValue).toEqual(ref.toString()); + }); + }); + + describe('$getDownloadURL', function() { + it('should call the ref getDownloadURL method', function() { + var ref = firebase.storage().ref('thing'); + var storage = $firebaseStorage(ref); + spyOn(ref, 'getDownloadURL'); + storage.$getDownloadURL(); + expect(ref.getDownloadURL).toHaveBeenCalled(); + }); + }); + + describe('$delete', function() { + it('should call the storage ref delete method', function() { + var ref = firebase.storage().ref('thing'); + var storage = $firebaseStorage(ref); + spyOn(ref, 'delete'); + storage.$delete(); + expect(ref.delete).toHaveBeenCalled(); + }); + }); + + describe('$getMetadata', function() { + it('should call ref getMetadata', function() { + var ref = firebase.storage().ref('thing'); + var storage = $firebaseStorage(ref); + spyOn(ref, 'getMetadata'); + storage.$getMetadata(); + expect(ref.getMetadata).toHaveBeenCalled(); + }); + }); + + describe('$updateMetadata', function() { + it('should call ref updateMetadata', function() { + var ref = firebase.storage().ref('thing'); + var storage = $firebaseStorage(ref); + spyOn(ref, 'updateMetadata'); + storage.$updateMetadata(); + expect(ref.updateMetadata).toHaveBeenCalled(); + }); + }); + + }); + }); +}); + +/** + * A Mock for Cloud Storage for Firebase tasks. It has the same .on() method signature + * but it simply stores the callbacks without doing anything. To make something + * happen you call the makeProgress(), causeError(), or complete() methods. The + * empty methods are intentional noops. + */ +var MockTask = (function () { + function MockTask() { + this.snapshot = null; + } + MockTask.prototype.on = function (event, successCallback, errorCallback, completionCallback) { + this.event = event; + this.successCallback = successCallback; + this.errorCallback = errorCallback; + this.completionCallback = completionCallback; + }; + MockTask.prototype.makeProgress = function () { + this.snapshot = {}; + this.successCallback(); + }; + MockTask.prototype.causeError = function () { + this.errorCallback(); + }; + MockTask.prototype.complete = function () { + this.snapshot = {}; + this.completionCallback(); + }; + MockTask.prototype.cancel = function () { }; + MockTask.prototype.resume = function () { }; + MockTask.prototype.pause = function () { }; + MockTask.prototype.then = function () { }; + MockTask.prototype.catch = function () { }; + return MockTask; +} ()); diff --git a/tests/unit/firebase.spec.js b/tests/unit/firebase.spec.js new file mode 100644 index 00000000..d693ed60 --- /dev/null +++ b/tests/unit/firebase.spec.js @@ -0,0 +1,21 @@ +'use strict'; +describe('$firebase', function () { + + beforeEach(function () { + module('firebase'); + }); + + describe('', function () { + var $firebase; + beforeEach(function() { + inject(function (_$firebase_) { + $firebase = _$firebase_; + }); + }); + it('throws an error', function() { + expect(function() { + $firebase(new Firebase('Mock://')); + }).toThrow(); + }); + }); +}); diff --git a/tests/unit/firebaseRef.spec.js b/tests/unit/firebaseRef.spec.js new file mode 100644 index 00000000..5c689354 --- /dev/null +++ b/tests/unit/firebaseRef.spec.js @@ -0,0 +1,55 @@ +'use strict'; +describe('firebaseRef', function () { + + var $firebaseRefProvider; + var MOCK_URL = firebase.database().ref().toString(); + + beforeEach(module('firebase.database', function(_$firebaseRefProvider_) { + $firebaseRefProvider = _$firebaseRefProvider_; + })); + + describe('registerUrl', function() { + + it('creates a single reference with a url', inject(function() { + $firebaseRefProvider.registerUrl(MOCK_URL); + expect($firebaseRefProvider.$get().default).toBeAFirebaseRef(); + })); + + it('creates a default reference with a config object', inject(function() { + $firebaseRefProvider.registerUrl({ + default: MOCK_URL + }); + var firebaseRef = $firebaseRefProvider.$get(); + expect(firebaseRef.default).toBeAFirebaseRef(); + })); + + it('creates multiple references with a config object', inject(function() { + $firebaseRefProvider.registerUrl({ + default: MOCK_URL, + messages: MOCK_URL + '/messages' + }); + var firebaseRef = $firebaseRefProvider.$get(); + expect(firebaseRef.default).toBeAFirebaseRef(); + expect(firebaseRef.messages).toBeAFirebaseRef(); + })); + + it('should throw an error when no url is provided', inject(function () { + function errorWrapper() { + $firebaseRefProvider.registerUrl(); + $firebaseRefProvider.$get(); + } + expect(errorWrapper).toThrow(); + })); + + it('should throw an error when no default url is provided', inject(function() { + function errorWrapper() { + $firebaseRefProvider.registerUrl({ messages: MOCK_URL + '/messages' }); + $firebaseRefProvider.$get(); + } + expect(errorWrapper).toThrow(); + })); + + + }); + +}); diff --git a/tests/unit/omnibinder-protocol.spec.js b/tests/unit/omnibinder-protocol.spec.js deleted file mode 100644 index e5dd52a7..00000000 --- a/tests/unit/omnibinder-protocol.spec.js +++ /dev/null @@ -1,157 +0,0 @@ -describe('OmniBinder Protocol', function () { - var firebinder; - - beforeEach(module('omniFire')); - - describe('objectChange', function () { - var objectChange; - - beforeEach(inject(function (_objectChange_) { - objectChange = _objectChange_; - })); - - it('should generate a change object', function () { - expect(objectChange('foo', 'update', 'bar', 'baz')).toEqual({ - name: 'foo', - type: 'update', - value: 'bar', - oldValue: 'baz' - }); - }); - }); - - - describe('arrayChange', function () { - var arrayChange; - - beforeEach(inject(function (_arrayChange_) { - arrayChange = _arrayChange_; - })); - - it('should generate a change object', function () { - expect(arrayChange(1, ['foo'], 1, ['baz'])).toEqual({ - index: 1, - removed: ['foo'], - addedCount: 1, - added: ['baz'] - }); - }); - }); - - - describe('firebinder', function () { - beforeEach(inject(function (_firebinder_) { - firebinder = _firebinder_; - })); - - - it('should have a property called bar', function () { - expect(typeof firebinder.subscribe).toBe('function'); - }); - - describe('subscribe', function () { - it('should create a new Firebase instance for the given location', function () { - var binder = {query: {url: 'foo/bar/'}}; - firebinder.subscribe(binder); - // can't use spyOn(Firbase) here; it breaks the prototype of MockFirebase (makes all methods undefined) - expect(binder.fbRef.toString()).toEqual('foo/bar/'); - }); - - it('should call limit if provided in query', function () { - var binder = {query: {limit: 10, url: 'foo/bar'}}; - firebinder.subscribe(binder); - expect(binder.fbRef.limit).toHaveBeenCalledWith(10); - }); - - - it('should call startAt if provided in query', function () { - var binder = {query: {limit: 20, startAt: 50, url: 'foo/bar'}}; - firebinder.subscribe(binder); - expect(binder.fbRef.limit).toHaveBeenCalledWith(20); - expect(binder.fbRef.startAt).toHaveBeenCalledWith(50); - }); - - describe('child_added', function () { - var binder, snapshot, - value = {foo: 'bar'}; - beforeEach(function (){ - binder = { - query: {url: 'foo/bar'}, - onProtocolChange: angular.noop - }; - - snapshot = { - val: 'foo', - name: function () { - return 'bar'; - }, - val: function () { - return value; - } - } - }); - - - it('should call on child_added on the ref during construction', - function () { - firebinder.subscribe(binder); - expect(binder.fbRef.on.callCount).toBe(1); - }); - - - it('should call onChildAdded on the event of the child being added', - function () { - var spy = spyOn(firebinder, 'onChildAdded'); - - firebinder.subscribe(binder); - binder.fbRef.flush(); - - expect(spy).toHaveBeenCalled(); - }); - - it('should insert the child\'s name at the beginning of the binder index if no prev is provided', - function () { - firebinder.subscribe(binder); - binder.index.push('baz', 'foo'); - firebinder.onChildAdded.call(firebinder, binder, snapshot); - - expect(binder.index.indexOf('bar')).toBe(0); - }); - - it('should insert the child\'s name after the prev in the binder index', - function () { - firebinder.subscribe(binder); - binder.index.push('baz', 'foo'); - firebinder.onChildAdded.call(firebinder, binder, snapshot, 'baz'); - - expect(binder.index.indexOf('bar')).toBe(1); - }); - - it('should call binder.onProtocolChange', function () { - var spy = spyOn(binder, 'onProtocolChange'); - firebinder.subscribe(binder); - - firebinder.onChildAdded(binder, snapshot); - - expect(spy).toHaveBeenCalledWith([{ - addedCount: 1, - added: [value], - index: 0, - removed: [] - }]); - }); - - - it('should not call binder.onProtocolChange if isLocal is true', function () { - var spy = spyOn(binder, 'onProtocolChange'); - firebinder.subscribe(binder); - binder.isLocal = true; - firebinder.onChildAdded(binder, snapshot); - - expect(spy).not.toHaveBeenCalled(); - expect(binder.isLocal).toBe(false); - }); - }); - }); - }); -}); diff --git a/tests/unit/orderbypriority.spec.js b/tests/unit/orderbypriority.spec.js deleted file mode 100644 index 25b2d464..00000000 --- a/tests/unit/orderbypriority.spec.js +++ /dev/null @@ -1,43 +0,0 @@ -describe('OrderByPriority Filter', function () { - var $firebase, $filter, $timeout; - beforeEach(module('firebase')); - beforeEach(inject(function (_$firebase_, _$filter_, _$timeout_) { - $firebase = _$firebase_; - $filter = _$filter_; - $timeout = _$timeout_; - })); - - it('should return a copy if passed an array', function () { - var orig = ['a', 'b', 'c']; - var res = $filter('orderByPriority')(orig); - expect(res).not.toBe(orig); // is a copy - expect(res).toEqual(orig); // is the same - }); - - it('should return an equivalent array if passed an object', function () { - var res = $filter('orderByPriority')({foo: 'bar', fu: 'baz'}); - expect(res).toEqual(['bar', 'baz']); - }); - - it('should return an empty array if passed a non-object', function () { - var res = $filter('orderByPriority')(true); - expect(res).toEqual([]); - }); - - it('should return an array from a $firebase instance', function () { - var loaded = false; - // autoFlush makes all Firebase methods trigger immediately - var fb = new Firebase('Mock//sort').child('data').autoFlush(); - var ref = $firebase(fb); - // $timeout is a mock, so we have to tell the mock when to trigger it - // and fire all the angularFire events - $timeout.flush(); - // now we can actually trigger our filter and pass in the $firebase ref - var res = $filter('orderByPriority')(ref); - // and finally test the results against the original data in Firebase instance - var originalData = _.map(fb.getData(), function(v, k) { - return _.isObject(v)? _.assign({'$id': k}, v) : v; - }); - expect(res).toEqual(originalData); - }); -}); \ No newline at end of file diff --git a/tests/unit/utils.spec.js b/tests/unit/utils.spec.js new file mode 100644 index 00000000..8d8d60ac --- /dev/null +++ b/tests/unit/utils.spec.js @@ -0,0 +1,488 @@ +'use strict'; +describe('$firebaseUtils', function () { + var $utils, $timeout, $rootScope, $q, tick, testutils; + + var MOCK_DATA = { + 'a': { + aString: 'alpha', + aNumber: 1, + aBoolean: false + }, + 'b': { + aString: 'bravo', + aNumber: 2, + aBoolean: true + }, + 'c': { + aString: 'charlie', + aNumber: 3, + aBoolean: true + }, + 'd': { + aString: 'delta', + aNumber: 4, + aBoolean: true + }, + 'e': { + aString: 'echo', + aNumber: 5 + } + }; + + beforeEach(function () { + module('firebase.utils'); + module('testutils'); + inject(function (_$firebaseUtils_, _$timeout_, _$rootScope_, _$q_, _testutils_) { + $utils = _$firebaseUtils_; + $timeout = _$timeout_; + $rootScope = _$rootScope_; + $q = _$q_; + testutils = _testutils_; + + firebase.database.enableLogging(function () {tick()}); + tick = function () { + setTimeout(function() { + $q.defer(); + $rootScope.$digest(); + try { + $timeout.flush(); + } catch (err) { + // This throws an error when there is nothing to flush... + } + }) + }; + }); + }); + + describe('#batch', function() { + it('should return a function', function() { + expect(typeof $utils.batch()).toBe('function'); + }); + + it('should trigger function with arguments', function() { + var spy = jasmine.createSpy(); + var b = $utils.batch(spy); + b('foo', 'bar'); + + $rootScope.$digest(); + + expect(spy).toHaveBeenCalledWith('foo', 'bar'); + }); + + it('should queue up requests until timeout', function() { + var spy = jasmine.createSpy(); + var b = $utils.batch(spy); + for(var i=0; i < 4; i++) { + b(i); + } + + expect(spy).not.toHaveBeenCalled(); + + $rootScope.$digest(); + $timeout.flush(); + expect(spy.calls.count()).toBe(4); + }); + + it('should observe context', function() { + var a = {}, b; + var spy = jasmine.createSpy().and.callFake(function() { + b = this; + }); + $utils.batch(spy, a)(); + $rootScope.$digest(); + expect(spy).toHaveBeenCalled(); + expect(b).toBe(a); + }); + }); + + describe('#debounce', function(){ + it('should trigger function with arguments',function(){ + var spy = jasmine.createSpy(); + $utils.debounce(spy,10)('foo', 'bar'); + + $timeout.flush(); + $rootScope.$digest(); + expect(spy).toHaveBeenCalledWith('foo', 'bar'); + }); + + it('should only trigger once, with most recent arguments',function(){ + var spy = jasmine.createSpy(); + var fn = $utils.debounce(spy,10); + fn('foo', 'bar'); + fn('baz', 'biz'); + + $timeout.flush(); + $rootScope.$digest(); + expect(spy.calls.count()).toBe(1); + expect(spy).toHaveBeenCalledWith('baz', 'biz'); + }); + + it('should only trigger once (timing corner case)',function(){ + var spy = jasmine.createSpy(); + var fn = $utils.debounce(spy, null, 1, 2); + fn('foo', 'bar'); + var start = Date.now(); + + // block for 3ms without releasing + while(Date.now() - start < 3){ } + + fn('bar', 'baz'); + fn('baz', 'biz'); + expect(spy).not.toHaveBeenCalled(); + + $timeout.flush(); + $rootScope.$digest(); + expect(spy.calls.count()).toBe(1); + expect(spy).toHaveBeenCalledWith('baz', 'biz'); + }); + }); + + describe('#deepCopy', function() { + it('should work for empty objects', function() { + var obj = {}; + expect($utils.deepCopy(obj)).toEqual(obj); + }); + it('should work for primitives', function() { + var obj = 'foo'; + expect($utils.deepCopy(obj)).toEqual(obj); + }); + it('should work for dates', function() { + var obj = new Date(); + expect($utils.deepCopy(obj)).toEqual(obj); + }); + it('should work for nested objects', function() { + var d = new Date(); + var obj = { date: {date: [{date: d}, {int: 1}, {str: "foo"}, {}]}}; + expect($utils.deepCopy(obj)).toEqual(obj); + }); + it('should work for functions', function() { + var f = function(){ + var s = 'foo'; + }; + var obj = f; + expect($utils.deepCopy(obj)).toEqual(obj); + }); + }); + + describe('#updateRec', function() { + it('should return true if changes applied', function() { + var rec = {}; + expect($utils.updateRec(rec, testutils.snap('foo'))).toBe(true); + }); + + it('should be false if no changes applied', function() { + var rec = {foo: 'bar', $id: 'foo', $priority: null}; + expect($utils.updateRec(rec, testutils.snap({foo: 'bar'}, 'foo'))).toBe(false); + }); + + it('should apply changes to record', function() { + var rec = {foo: 'bar', bar: 'foo', $id: 'foo', $priority: null}; + $utils.updateRec(rec, testutils.snap({bar: 'baz', baz: 'foo'})); + expect(rec).toEqual({bar: 'baz', baz: 'foo', $id: 'foo', $priority: null}) + }); + + it('should delete $value property if not a primitive',function(){ + var rec = {$id:'foo', $priority:null, $value:null }; + $utils.updateRec(rec, testutils.snap({bar: 'baz', baz:'foo'})); + expect(rec).toEqual({bar: 'baz', baz: 'foo', $id: 'foo', $priority: null}); + }); + }); + + describe('#scopeData',function(){ + it('$id, $priority, and $value are only private properties that get copied',function(){ + var data = {$id:'foo',$priority:'bar',$value:null,$private1:'baz',$private2:'foo'}; + expect($utils.scopeData(data)).toEqual({$id:'foo',$priority:'bar',$value:null}); + }); + + it('all public properties will be copied',function(){ + var data = {$id:'foo',$priority:'bar',public1:'baz',public2:'test'}; + expect($utils.scopeData(data)).toEqual({$id:'foo',$priority:'bar',public1:'baz',public2:'test'}); + }); + + it('$value will not be copied if public properties are present',function(){ + var data = {$id:'foo',$priority:'bar',$value:'noCopy',public1:'baz',public2:'test'}; + expect($utils.scopeData(data)).toEqual({$id:'foo',$priority:'bar',public1:'baz',public2:'test'}); + }); + }); + + describe('#applyDefaults', function() { + it('should return rec', function() { + var rec = {foo: 'bar'}; + expect($utils.applyDefaults(rec), {bar: 'baz'}).toBe(rec); + }); + + it('should do nothing if no defaults exist', function() { + var rec = {foo: 'bar'}; + $utils.applyDefaults(rec, null); + expect(rec).toEqual({foo: 'bar'}); + }); + + it('should add $$defaults if they exist', function() { + var rec = {foo: 'foo', bar: 'bar', $id: 'foo', $priority: null}; + var defaults = { baz: 'baz', bar: 'not_applied' }; + $utils.applyDefaults(rec, defaults); + expect(rec).toEqual({foo: 'foo', bar: 'bar', $id: 'foo', $priority: null, baz: 'baz'}); + }); + }); + + describe('#toJSON', function() { + it('should use toJSON if it exists', function() { + var json = {json: true}; + var spy = jasmine.createSpy('toJSON').and.callFake(function() { + return json; + }); + var F = function() {}; + F.prototype.toJSON = spy; + expect($utils.toJSON(new F())).toEqual(json); + expect(spy).toHaveBeenCalled(); + }); + + it('should use $value if found', function() { + var json = {$value: 'foo'}; + expect($utils.toJSON(json)).toEqual({'.value': json.$value}); + }); + + it('should set $priority if exists', function() { + var json = {$value: 'foo', $priority: 0}; + expect($utils.toJSON(json)).toEqual({'.value': json.$value, '.priority': json.$priority}); + }); + + it('should not set $priority if it is the only key', function() { + var json = {$priority: true}; + expect($utils.toJSON(json)).toEqual({}); + }); + + it('should remove any variables prefixed with $', function() { + var json = {foo: 'bar', $foo: '$bar'}; + expect($utils.toJSON(json)).toEqual({foo: json.foo}); + }); + + it('should be able to handle date objects', function(){ + var d = new Date(); + var json = {date: d}; + + expect($utils.toJSON(json)).toEqual({date: d}); + }); + + it('should remove any deeply nested variables prefixed with $', function() { + var json = { + arr: [[ + {$$hashKey: false, $key: 1, alpha: 'alpha', bravo: {$$private: '$$private', $key: '$key', bravo: 'bravo'}}, + {$$hashKey: false, $key: 1, alpha: 'alpha', bravo: {$$private: '$$private', $key: '$key', bravo: 'bravo'}} + + ], ["a", "b", {$$key: '$$key'}]], + obj: { + nest: {$$hashKey: false, $key: 1, alpha: 'alpha', bravo: {$$private: '$$private', $key: '$key', bravo: 'bravo'} } + } + }; + + expect($utils.toJSON(json)).toEqual({ + arr: [[ + {alpha: 'alpha', bravo: {bravo: 'bravo'}}, + {alpha: 'alpha', bravo: {bravo: 'bravo'}} + + ], ["a", "b", {}]], + obj: { + nest: {alpha: 'alpha', bravo: {bravo: 'bravo'} } + } + }); + }); + + it('should throw error if an invalid character in key', function() { + expect(function() { + $utils.toJSON({'foo.bar': 'foo.bar'}); + }).toThrowError(Error); + expect(function() { + $utils.toJSON({'foo$bar': 'foo.bar'}); + }).toThrowError(Error); + expect(function() { + $utils.toJSON({'foo#bar': 'foo.bar'}); + }).toThrowError(Error); + expect(function() { + $utils.toJSON({'foo[bar': 'foo.bar'}); + }).toThrowError(Error); + expect(function() { + $utils.toJSON({'foo]bar': 'foo.bar'}); + }).toThrowError(Error); + expect(function() { + $utils.toJSON({'foo/bar': 'foo.bar'}); + }).toThrowError(Error); + }); + + it('should throw error if undefined value', function() { + expect(function() { + var undef; + $utils.toJSON({foo: 'bar', baz: undef}); + }).toThrowError(Error); + }); + }); + + describe('#makeNodeResolver', function(){ + var deferred, callback; + beforeEach(function(){ + deferred = jasmine.createSpyObj('promise',['resolve','reject']); + callback = $utils.makeNodeResolver(deferred); + }); + + it('should return a function', function(){ + expect(callback).toBeA('function'); + }); + + it('should reject the promise if the first argument is truthy', function(){ + var error = new Error('blah'); + callback(error); + expect(deferred.reject).toHaveBeenCalledWith(error); + }); + + it('should reject the promise if the first argument is not null', function(){ + callback(false); + expect(deferred.reject).toHaveBeenCalledWith(false); + }); + + it('should resolve the promise if the first argument is null', function(){ + var result = {data:'hello world'}; + callback(null,result); + expect(deferred.resolve).toHaveBeenCalledWith(result); + }); + + it('should aggregate multiple arguments into an array', function(){ + var result1 = {data:'hello world!'}; + var result2 = {data:'howdy!'}; + callback(null,result1,result2); + expect(deferred.resolve).toHaveBeenCalledWith([result1,result2]); + }); + }); + + describe('#doSet', function() { + var ref; + beforeEach(function() { + ref = firebase.database().ref().child("angularfire"); + }); + + it('returns a promise', function() { + expect($utils.doSet(ref, null)).toBeAPromise(); + }); + + it('resolves on success', function(done) { + var whiteSpy = jasmine.createSpy('resolve'); + var blackSpy = jasmine.createSpy('reject'); + $utils.doSet(ref, {foo: 'bar'}) + .then(whiteSpy, blackSpy) + .then(function () { + expect(blackSpy).not.toHaveBeenCalled(); + expect(whiteSpy).toHaveBeenCalled(); + done(); + }); + }); + + it('saves the data', function(done) { + $utils.doSet(ref, true); + ref.once("value", function (ss) { + expect(ss.val()).toBe(true); + done(); + }); + }); + + it('rejects promise when fails', function(done) { + var whiteSpy = jasmine.createSpy('resolve'); + var blackSpy = jasmine.createSpy('reject'); + $utils.doSet(ref, {"zippo/pippo": 'bar'}) + .then(whiteSpy, blackSpy) + .finally(function () { + expect(whiteSpy).not.toHaveBeenCalled(); + expect(blackSpy).toHaveBeenCalled(); + done(); + }); + }); + + it('only affects query keys when using a query', function(done) { + ref.set({fish: true}); + var query = ref.limitToLast(1); + var spy = spyOn(firebase.database.Reference.prototype, 'update').and.callThrough(); + + $utils.doSet(query, {hello: 'world'}) + .then(function () { + var args = spy.calls.mostRecent().args[0]; + expect(Object.keys(args)).toEqual(['hello', 'fish']); + done(); + }); + }); + }); + + describe('#doRemove', function() { + var ref; + beforeEach(function() { + ref = firebase.database().ref().child("angularfire"); + }); + + it('returns a promise', function() { + expect($utils.doRemove(ref)).toBeAPromise(); + }); + + it('resolves if successful', function(done) { + var whiteSpy = jasmine.createSpy('resolve'); + var blackSpy = jasmine.createSpy('reject'); + $utils.doRemove(ref) + .then(whiteSpy, blackSpy) + .then(function () { + expect(blackSpy).not.toHaveBeenCalled(); + expect(whiteSpy).toHaveBeenCalled(); + done(); + }); + }); + + it('removes the data', function(done) { + return ref.set(MOCK_DATA).then(function() { + return $utils.doRemove(ref); + }).then(function () { + return ref.once('value'); + }).then(function(snapshot) { + expect(snapshot.val()).toBe(null); + done(); + }); + }); + + it('rejects promise if write fails', function(done) { + var whiteSpy = jasmine.createSpy('resolve'); + var blackSpy = jasmine.createSpy('reject'); + var err = new Error('test_fail_remove'); + + spyOn(firebase.database.Reference.prototype, "remove").and.callFake(function (cb) { + cb(err); + }); + + $utils.doRemove(ref) + .then(whiteSpy, blackSpy) + .then(function () { + expect(whiteSpy).not.toHaveBeenCalled(); + expect(blackSpy).toHaveBeenCalledWith(err); + done(); + }); + }); + + it('only removes keys in query when query is used', function(done){ + return ref.set(MOCK_DATA).then(function() { + var query = ref.limitToFirst(2); + return $utils.doRemove(query); + }).then(function() { + return ref.once('value'); + }).then(function(snapshot) { + var val = snapshot.val(); + + expect(val.a).not.toBeDefined(); + expect(val.b).not.toBeDefined(); + expect(val.c).toBeDefined(); + expect(val.d).toBeDefined(); + expect(val.e).toBeDefined(); + + done(); + }); + }); + }); + + describe('#VERSION', function() { + it('should return the version number', function() { + expect($utils.VERSION).toEqual('0.0.0'); + }); + }); +});