commit 4ff09939b0426b4ea1138ea91d00a4ca53bc3503 Author: Nolan Lawson Date: Sat Oct 4 22:40:02 2014 -0400 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..318b19e --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules +.DS_Store +*~ +coverage +test/test-bundle.js +npm-debug.log +dist diff --git a/.jshintrc b/.jshintrc new file mode 100755 index 0000000..8b98e1a --- /dev/null +++ b/.jshintrc @@ -0,0 +1,30 @@ +{ + "curly": true, + "eqeqeq": true, + "immed": true, + "newcap": true, + "noarg": true, + "sub": true, + "undef": true, + "unused": true, + "eqnull": true, + "browser": true, + "node": true, + "strict": true, + "globalstrict": true, + "globals": { "Pouch": true}, + "white": true, + "indent": 2, + "maxlen": 100, + "predef": [ + "process", + "global", + "require", + "console", + "describe", + "beforeEach", + "afterEach", + "it", + "emit" + ] +} \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..e6a3bd8 --- /dev/null +++ b/.npmignore @@ -0,0 +1,7 @@ +.git* +node_modules +.DS_Store +*~ +coverage +npm-debug.log +vendor/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..677a401 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,35 @@ +language: node_js + +services: + - couchdb + +node_js: + - "0.10" +script: npm run $COMMAND +before_script: + - "export DISPLAY=:99.0" + - "sh -e /etc/init.d/xvfb start" + - "sleep 5" + + # Workaround for Selenium #3280 issue + - "sudo sed -i 's/^127\\.0\\.0\\.1.*$/127.0.0.1 localhost/' /etc/hosts" + + # Lets know what CouchDB we are playing with + # and make sure its logging debug + - "curl -X GET http://127.0.0.1:5984/" + - "curl -X PUT http://127.0.0.1:5984/_config/log/level -d '\"debug\"'" + +after_failure: + - "curl -X GET http://127.0.0.1:5984/_log?bytes=1000000" + +env: + matrix: + - COMMAND=test + - CLIENT=selenium:firefox COMMAND=test + - CLIENT=selenium:phantomjs COMMAND=test + - COMMAND=coverage + +branches: + only: + - master + - /^pull*$/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..b0c4b37 --- /dev/null +++ b/README.md @@ -0,0 +1,103 @@ +PouchDB Plugin Seed +===== + +[![Build Status](https://travis-ci.org/pouchdb/plugin-seed.svg)](https://travis-ci.org/pouchdb/plugin-seed) + +Fork this project to build your first PouchDB plugin. It contains everything you need to test in Node, WebSQL, and IndexedDB. It also includes a Travis config file so you +can automatically run the tests in Travis. + +Building +---- + npm install + npm run build + +Your plugin is now located at `dist/pouchdb.mypluginname.js` and `dist/pouchdb.mypluginname.min.js` and is ready for distribution. + +Getting Started +------- + +**First**, change the `name` in `package.json` to whatever you want to call your plugin. Change the `build` script so that it writes to the desired filename (e.g. `pouchdb.mypluginname.js`). Also, change the authors, description, git repo, etc. + +**Next**, modify the `index.js` to do whatever you want your plugin to do. Right now it just adds a `pouch.sayHello()` function that says hello: + +```js +exports.sayHello = utils.toPromise(function (callback) { + callback(null, 'hello'); +}); +``` + +**Optionally**, you can add some tests in `tests/test.js`. These tests will be run both in the local database and a remote CouchDB, which is expected to be running at localhost:5984 in "Admin party" mode. + +The sample test is: + +```js + +it('should say hello', function () { + return db.sayHello().then(function (response) { + response.should.equal('hello'); + }); +}); +``` + +Testing +---- + +### In Node + +This will run the tests in Node using LevelDB: + + npm test + +You can also check for 100% code coverage using: + + npm run coverage + +If you don't like the coverage results, change the values from 100 to something else in `package.json`, or add `/*istanbul ignore */` comments. + + +If you have mocha installed globally you can run single test with: +``` +TEST_DB=local mocha --reporter spec --grep search_phrase +``` + +The `TEST_DB` environment variable specifies the database that PouchDB should use (see `package.json`). + +### In the browser + +Run `npm run dev` and then point your favorite browser to [http://127.0.0.1:8001/test/index.html](http://127.0.0.1:8001/test/index.html). + +The query param `?grep=mysearch` will search for tests matching `mysearch`. + +### Automated browser tests + +You can run e.g. + + CLIENT=selenium:firefox npm test + CLIENT=selenium:phantomjs npm test + +This will run the tests automatically and the process will exit with a 0 or a 1 when it's done. Firefox uses IndexedDB, and PhantomJS uses WebSQL. + +What to tell your users +-------- + +Below is some boilerplate you can use for when you want a real README for your users. + +To use this plugin, include it after `pouchdb.js` in your HTML page: + +```html + + +``` + +Or to use it in Node.js, just npm install it: + +``` +npm install pouchdb-myplugin +``` + +And then attach it to the `PouchDB` object: + +```js +var PouchDB = require('pouchdb'); +PouchDB.plugin(require('pouchdb-myplugin')); +``` diff --git a/bin/dev-server.js b/bin/dev-server.js new file mode 100755 index 0000000..2713194 --- /dev/null +++ b/bin/dev-server.js @@ -0,0 +1,94 @@ +#!/usr/bin/env node + +'use strict'; + +var PouchDB = require('pouchdb'); +var COUCH_HOST = process.env.COUCH_HOST || 'http://127.0.0.1:5984'; +var HTTP_PORT = 8001; + +var Promise = require('bluebird'); +var request = require('request'); +var http_server = require("http-server"); +var fs = require('fs'); +var indexfile = "./test/test.js"; +var dotfile = "./test/.test-bundle.js"; +var outfile = "./test/test-bundle.js"; +var watchify = require("watchify"); +var w = watchify(indexfile); + +w.on('update', bundle); +bundle(); + +var filesWritten = false; +var serverStarted = false; +var readyCallback; + +function bundle() { + var wb = w.bundle(); + wb.on('error', function (err) { + console.error(String(err)); + }); + wb.on("end", end); + wb.pipe(fs.createWriteStream(dotfile)); + + function end() { + fs.rename(dotfile, outfile, function (err) { + if (err) { return console.error(err); } + console.log('Updated:', outfile); + filesWritten = true; + checkReady(); + }); + } +} + +function startServers(callback) { + readyCallback = callback; + // enable CORS globally, because it's easier this way + + var corsValues = { + '/_config/httpd/enable_cors': 'true', + '/_config/cors/origins': '*', + '/_config/cors/credentials': 'true', + '/_config/cors/methods': 'PROPFIND, PROPPATCH, COPY, MOVE, DELETE, ' + + 'MKCOL, LOCK, UNLOCK, PUT, GETLIB, VERSION-CONTROL, CHECKIN, ' + + 'CHECKOUT, UNCHECKOUT, REPORT, UPDATE, CANCELUPLOAD, HEAD, ' + + 'OPTIONS, GET, POST', + '/_config/cors/headers': + 'Cache-Control, Content-Type, Depth, Destination, ' + + 'If-Modified-Since, Overwrite, User-Agent, X-File-Name, ' + + 'X-File-Size, X-Requested-With, accept, accept-encoding, ' + + 'accept-language, authorization, content-type, origin, referer' + }; + + Promise.all(Object.keys(corsValues).map(function (key) { + var value = corsValues[key]; + return request({ + method: 'put', + url: COUCH_HOST + key, + body: JSON.stringify(value) + }); + })).then(function () { + return http_server.createServer().listen(HTTP_PORT); + }).then(function () { + console.log('Tests: http://127.0.0.1:' + HTTP_PORT + '/test/index.html'); + serverStarted = true; + checkReady(); + }).catch(function (err) { + if (err) { + console.log(err); + process.exit(1); + } + }); +} + +function checkReady() { + if (filesWritten && serverStarted && readyCallback) { + readyCallback(); + } +} + +if (require.main === module) { + startServers(); +} else { + module.exports.start = startServers; +} diff --git a/bin/run-test.sh b/bin/run-test.sh new file mode 100755 index 0000000..2a59ba8 --- /dev/null +++ b/bin/run-test.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +: ${CLIENT:="node"} + +if [ "$CLIENT" == "node" ]; then + npm run test-node +else + npm run test-browser +fi diff --git a/bin/test-browser.js b/bin/test-browser.js new file mode 100755 index 0000000..fc6d5a8 --- /dev/null +++ b/bin/test-browser.js @@ -0,0 +1,176 @@ +#!/usr/bin/env node +'use strict'; + +var path = require('path'); +var spawn = require('child_process').spawn; + +var wd = require('wd'); +var sauceConnectLauncher = require('sauce-connect-launcher'); +var querystring = require("querystring"); +var request = require('request').defaults({json: true}); + +var devserver = require('./dev-server.js'); + +var SELENIUM_PATH = '../vendor/selenium-server-standalone-2.38.0.jar'; +var SELENIUM_HUB = 'http://localhost:4444/wd/hub/status'; + +var testTimeout = 30 * 60 * 1000; + +var username = process.env.SAUCE_USERNAME; +var accessKey = process.env.SAUCE_ACCESS_KEY; + +// process.env.CLIENT is a colon seperated list of +// (saucelabs|selenium):browserName:browserVerion:platform +var tmp = (process.env.CLIENT || 'selenium:firefox').split(':'); +var client = { + runner: tmp[0] || 'selenium', + browser: tmp[1] || 'firefox', + version: tmp[2] || null, // Latest + platform: tmp[3] || null +}; + +var testUrl = 'http://127.0.0.1:8001/test/index.html'; +var qs = {}; + +var sauceClient; +var sauceConnectProcess; +var tunnelId = process.env.TRAVIS_JOB_NUMBER || 'tunnel-' + Date.now(); + +if (client.runner === 'saucelabs') { + qs.saucelabs = true; +} +if (process.env.GREP) { + qs.grep = process.env.GREP; +} +if (process.env.ADAPTERS) { + qs.adapters = process.env.ADAPTERS; +} +if (process.env.ES5_SHIM || process.env.ES5_SHIMS) { + qs.es5shim = true; +} +testUrl += '?'; +testUrl += querystring.stringify(qs); + +if (process.env.TRAVIS && + client.browser !== 'firefox' && + process.env.TRAVIS_SECURE_ENV_VARS === 'false') { + console.error('Not running test, cannot connect to saucelabs'); + process.exit(0); + return; +} + +function testError(e) { + console.error(e); + console.error('Doh, tests failed'); + sauceClient.quit(); + process.exit(3); +} + +function postResult(result) { + process.exit(!process.env.PERF && result.failed ? 1 : 0); +} + +function testComplete(result) { + console.log(result); + + sauceClient.quit().then(function () { + if (sauceConnectProcess) { + sauceConnectProcess.close(function () { + postResult(result); + }); + } else { + postResult(result); + } + }); +} + +function startSelenium(callback) { + + // Start selenium + spawn('java', ['-jar', path.resolve(__dirname, SELENIUM_PATH)], {}); + + var retries = 0; + var started = function () { + + if (++retries > 30) { + console.error('Unable to connect to selenium'); + process.exit(1); + return; + } + + request(SELENIUM_HUB, function (err, resp) { + if (resp && resp.statusCode === 200) { + sauceClient = wd.promiseChainRemote(); + callback(); + } else { + setTimeout(started, 1000); + } + }); + }; + + started(); + +} + +function startSauceConnect(callback) { + + var options = { + username: username, + accessKey: accessKey, + tunnelIdentifier: tunnelId + }; + + sauceConnectLauncher(options, function (err, process) { + if (err) { + console.error('Failed to connect to saucelabs'); + console.error(err); + return process.exit(1); + } + sauceConnectProcess = process; + sauceClient = wd.promiseChainRemote("localhost", 4445, username, accessKey); + callback(); + }); +} + +function startTest() { + + console.log('Starting', client); + + var opts = { + browserName: client.browser, + version: client.version, + platform: client.platform, + tunnelTimeout: testTimeout, + name: client.browser + ' - ' + tunnelId, + 'max-duration': 60 * 30, + 'command-timeout': 599, + 'idle-timeout': 599, + 'tunnel-identifier': tunnelId + }; + + sauceClient.init(opts).get(testUrl, function () { + + /* jshint evil: true */ + var interval = setInterval(function () { + sauceClient.eval('window.results', function (err, results) { + if (err) { + clearInterval(interval); + testError(err); + } else if (results.completed || results.failures.length) { + clearInterval(interval); + testComplete(results); + } else { + console.log('=> ', results); + } + }); + }, 10 * 1000); + }); +} + +devserver.start(function () { + if (client.runner === 'saucelabs') { + startSauceConnect(startTest); + } else { + startSelenium(startTest); + } +}); diff --git a/index.js b/index.js new file mode 100644 index 0000000..9dfbcfa --- /dev/null +++ b/index.js @@ -0,0 +1,19 @@ +'use strict'; + +var utils = require('./pouch-utils'); + +exports.sayHello = utils.toPromise(function (callback) { + // + // You can use the following code to + // get the pouch or PouchDB objects + // + // var pouch = this; + // var PouchDB = pouch.constructor; + + callback(null, 'hello'); +}); + +/* istanbul ignore next */ +if (typeof window !== 'undefined' && window.PouchDB) { + window.PouchDB.plugin(exports); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..db3c083 --- /dev/null +++ b/package.json @@ -0,0 +1,65 @@ +{ + "name": "pouchdb-plugin-seed", + "version": "0.1.0", + "description": "PouchDB Plugin Seed project", + "main": "index.js", + "repository": { + "type": "git", + "url": "git://github.com/pouchdb/plugin-seed.git" + }, + "keywords": [ + "pouch", + "pouchdb", + "plugin", + "seed", + "couch", + "couchdb" + ], + "author": "", + "license": "Apache", + "bugs": { + "url": "https://github.com/pouchdb/plugin-seed/issues" + }, + "scripts": { + "test-node": "TEST_DB=testdb,http://localhost:5984/testdb istanbul test ./node_modules/mocha/bin/_mocha test/test.js", + "test-browser": "./bin/test-browser.js", + "jshint": "jshint -c .jshintrc *.js test/test.js", + "test": "npm run jshint && ./bin/run-test.sh", + "build": "mkdir -p dist && browserify index.js -o dist/pouchdb.mypluginname.js && npm run min", + "min": "uglifyjs dist/pouchdb.mypluginname.js -mc > dist/pouchdb.mypluginname.min.js", + "dev": "browserify test/test.js > test/test-bundle.js && npm run dev-server", + "dev-server": "./bin/dev-server.js", + "coverage": "npm test --coverage && istanbul check-coverage --lines 100 --function 100 --statements 100 --branches 100" + }, + "dependencies": { + "lie": "^2.6.0", + "inherits": "~2.0.1", + "argsarray": "0.0.1", + "es3ify": "^0.1.3" + }, + "devDependencies": { + "bluebird": "^1.0.7", + "browserify": "~2.36.0", + "chai": "~1.8.1", + "chai-as-promised": "~4.1.0", + "http-server": "~0.5.5", + "istanbul": "^0.2.7", + "jshint": "~2.3.0", + "mocha": "~1.18", + "phantomjs": "^1.9.7-5", + "pouchdb": "pouchdb/pouchdb", + "request": "^2.36.0", + "sauce-connect-launcher": "^0.4.2", + "uglify-js": "^2.4.13", + "watchify": "~0.4.1", + "wd": "^0.2.21" + }, + "browser": { + "crypto": false + }, + "browserify": { + "transform": [ + "es3ify" + ] + } +} diff --git a/pouch-utils.js b/pouch-utils.js new file mode 100644 index 0000000..5b3def6 --- /dev/null +++ b/pouch-utils.js @@ -0,0 +1,83 @@ +'use strict'; + +var Promise; +/* istanbul ignore next */ +if (typeof window !== 'undefined' && window.PouchDB) { + Promise = window.PouchDB.utils.Promise; +} else { + Promise = typeof global.Promise === 'function' ? global.Promise : require('lie'); +} +/* istanbul ignore next */ +exports.once = function (fun) { + var called = false; + return exports.getArguments(function (args) { + if (called) { + console.trace(); + throw new Error('once called more than once'); + } else { + called = true; + fun.apply(this, args); + } + }); +}; +/* istanbul ignore next */ +exports.getArguments = function (fun) { + return function () { + var len = arguments.length; + var args = new Array(len); + var i = -1; + while (++i < len) { + args[i] = arguments[i]; + } + return fun.call(this, args); + }; +}; +/* istanbul ignore next */ +exports.toPromise = function (func) { + //create the function we will be returning + return exports.getArguments(function (args) { + var self = this; + var tempCB = (typeof args[args.length - 1] === 'function') ? args.pop() : false; + // if the last argument is a function, assume its a callback + var usedCB; + if (tempCB) { + // if it was a callback, create a new callback which calls it, + // but do so async so we don't trap any errors + usedCB = function (err, resp) { + process.nextTick(function () { + tempCB(err, resp); + }); + }; + } + var promise = new Promise(function (fulfill, reject) { + try { + var callback = exports.once(function (err, mesg) { + if (err) { + reject(err); + } else { + fulfill(mesg); + } + }); + // create a callback for this invocation + // apply the function in the orig context + args.push(callback); + func.apply(self, args); + } catch (e) { + reject(e); + } + }); + // if there is a callback, call it back + if (usedCB) { + promise.then(function (result) { + usedCB(null, result); + }, usedCB); + } + promise.cancel = function () { + return this; + }; + return promise; + }); +}; + +exports.inherits = require('inherits'); +exports.Promise = Promise; diff --git a/test/bind-polyfill.js b/test/bind-polyfill.js new file mode 100644 index 0000000..6274eb6 --- /dev/null +++ b/test/bind-polyfill.js @@ -0,0 +1,28 @@ +(function () { + 'use strict'; + // minimal polyfill for phantomjs; in the future, we should do ES5_SHIM=true like pouchdb + 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; + }; + } +})(); diff --git a/test/index.html b/test/index.html new file mode 100644 index 0000000..e5adff1 --- /dev/null +++ b/test/index.html @@ -0,0 +1,18 @@ + + + + + Mocha Tests + + + + + + + +
+ + + diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..7e3d487 --- /dev/null +++ b/test/test.js @@ -0,0 +1,52 @@ +/*jshint expr:true */ +'use strict'; + +var Pouch = require('pouchdb'); + +// +// your plugin goes here +// +var helloPlugin = require('../'); +Pouch.plugin(helloPlugin); + +var chai = require('chai'); +chai.use(require("chai-as-promised")); + +// +// more variables you might want +// +chai.should(); // var should = chai.should(); +require('bluebird'); // var Promise = require('bluebird'); + +var dbs; +if (process.browser) { + dbs = 'testdb' + Math.random() + + ',http://localhost:5984/testdb' + Math.round(Math.random() * 100000); +} else { + dbs = process.env.TEST_DB; +} + +dbs.split(',').forEach(function (db) { + var dbType = /^http/.test(db) ? 'http' : 'local'; + tests(db, dbType); +}); + +function tests(dbName, dbType) { + + var db; + + beforeEach(function () { + db = new Pouch(dbName); + return db; + }); + afterEach(function () { + return Pouch.destroy(dbName); + }); + describe(dbType + ': hello test suite', function () { + it('should say hello', function () { + return db.sayHello().then(function (response) { + response.should.equal('hello'); + }); + }); + }); +} diff --git a/test/webrunner.js b/test/webrunner.js new file mode 100644 index 0000000..76608f3 --- /dev/null +++ b/test/webrunner.js @@ -0,0 +1,33 @@ +/* global mocha: true */ + +(function () { + 'use strict'; + var runner = mocha.run(); + window.results = { + lastPassed: '', + passed: 0, + failed: 0, + failures: [] + }; + + runner.on('pass', function (e) { + window.results.lastPassed = e.title; + window.results.passed++; + }); + + runner.on('fail', function (e) { + window.results.failed++; + window.results.failures.push({ + title: e.title, + message: e.err.message, + stack: e.err.stack + }); + }); + + runner.on('end', function () { + window.results.completed = true; + window.results.passed++; + }); +})(); + + diff --git a/vendor/selenium-server-standalone-2.38.0.jar b/vendor/selenium-server-standalone-2.38.0.jar new file mode 100644 index 0000000..15048af Binary files /dev/null and b/vendor/selenium-server-standalone-2.38.0.jar differ