├── .bowerrc ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jshintignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── beautify.json ├── bin └── publish.sh ├── bower.json ├── examples └── index.html ├── index.js ├── package.json ├── scripts └── index.js └── test ├── browser-coverage ├── index.html ├── phantom-hooks.js ├── server.js └── test.js ├── browser ├── google.png ├── index.html ├── sauce-results-updater.js ├── server.js ├── test.js └── webrunner.js ├── index.js └── spec └── index.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | test/browser/bundle.js 5 | test/browser-coverage/bundle.js 6 | .DS_Store 7 | *~ 8 | npm-debug.log 9 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | test/browser/bundle.js 2 | test/browser-coverage/bundle.js 3 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "newcap": true, 6 | "noarg": true, 7 | "sub": true, 8 | "undef": true, 9 | "unused": true, 10 | "eqnull": true, 11 | "browser": true, 12 | "node": true, 13 | "strict": true, 14 | "globalstrict": true, 15 | "white": true, 16 | "indent": 2, 17 | "maxlen": 100, 18 | "globals": { 19 | "process": false, 20 | "global": false, 21 | "require": false, 22 | "console": false, 23 | "describe": false, 24 | "before": false, 25 | "beforeEach": false, 26 | "after": false, 27 | "afterEach": false, 28 | "it": false, 29 | "emit": false 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git* 2 | coverage 3 | dist 4 | node_modules 5 | .DS_Store 6 | *~ 7 | npm-debug.log 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | script: npm run $COMMAND 4 | 5 | after_script: cat ./coverage/node/lcov.info ./coverage/browser/lcov.info | ./node_modules/coveralls/bin/coveralls.js 6 | 7 | after_failure: 8 | - cat /home/travis/build/redgeoff/paste-image/npm-debug.log 9 | 10 | env: 11 | matrix: 12 | - COMMAND=assert-beautified 13 | - COMMAND=browser-coverage-full-test 14 | 15 | # Saucelabs tests 16 | - CLIENT="saucelabs:firefox" COMMAND=browser-test 17 | - CLIENT="saucelabs:firefox:34" COMMAND=browser-test 18 | - CLIENT="saucelabs:chrome" COMMAND=browser-test 19 | - CLIENT="saucelabs:internet explorer" COMMAND=browser-test 20 | - CLIENT="saucelabs:internet explorer:10" COMMAND=browser-test 21 | - CLIENT="saucelabs:microsoftedge" COMMAND=browser-test 22 | - CLIENT="saucelabs:safari:9" COMMAND=browser-test 23 | - CLIENT="saucelabs:safari:8" COMMAND=browser-test 24 | - CLIENT="saucelabs:safari:7" COMMAND=browser-test 25 | - CLIENT="saucelabs:iphone:7.1" COMMAND=browser-test 26 | - CLIENT="saucelabs:iphone:8.4" COMMAND=browser-test 27 | - CLIENT="saucelabs:android:4.4" COMMAND=browser-test 28 | - CLIENT="saucelabs:android:5.1" COMMAND=browser-test 29 | 30 | # NOTE: there is currently no construct that allows us to encrypt the SAUCE_USERNAME and 31 | # SAUCE_ACCESS_KEY while also allowing saucelabs testing in forked projects. See 32 | # https://github.com/travis-ci/travis-ci/issues/1946 and 33 | # https://github.com/angular/angular.js/issues/5596 for more information. 34 | global: 35 | - SAUCE_USERNAME=paste-image-user 36 | - SAUCE_ACCESS_KEY=323f6977-df33-4713-8967-511394cb07e5 37 | # Prevent issues with bitbucket rate limiting 38 | - PHANTOMJS_CDNURL=http://cnpmjs.org/downloads 39 | 40 | branches: 41 | only: 42 | - master 43 | - /^pull*$/ 44 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ==== 3 | 4 | Beginning Work on an Issue 5 | --- 6 | Create branch 7 | git clone branch-url 8 | 9 | 10 | Committing Changes 11 | --- 12 | [Commit Message Format](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#commit) 13 | 14 | npm run coverage 15 | npm run beautify 16 | git add -A 17 | git commit -m msg 18 | git push 19 | 20 | 21 | Building 22 | --- 23 | 24 | npm run build 25 | 26 | 27 | Publishing to npm/bower 28 | --- 29 | 30 | First, make sure that you have previously issued `npm adduser`. Also make sure that you have tin installed, e.g. `npm install -g tin`. Then: 31 | 32 | git checkout master 33 | git pull origin master 34 | tin -v VERSION 35 | git diff # check that only version changed 36 | npm run build-and-publish 37 | 38 | 39 | Updating Dependencies 40 | --- 41 | This requires having david installed globally. 42 | 43 | david update 44 | 45 | 46 | Run all local tests 47 | --- 48 | 49 | npm run test 50 | 51 | 52 | Run single node test 53 | --- 54 | 55 | node_modules/mocha/bin/mocha -g regex test 56 | 57 | 58 | Run subset of tests and analyze coverage 59 | --- 60 | 61 | node_modules/istanbul/lib/cli.js cover _mocha -- -g regex test 62 | 63 | 64 | Debugging Tests Using Node Inspector 65 | --- 66 | 67 | $ node-inspector # leave this running in this window 68 | Use *Chrome* to visit http://127.0.0.1:8080/?ws=127.0.0.1:8080&port=5858 69 | $ mocha -g regex test/index.js --debug-brk 70 | 71 | 72 | Running Browser Tests With Coverage 73 | --- 74 | 75 | $ npm run browser-coverage-full-test 76 | 77 | You can filter full browser tests using the GREP env variable, e.g. 78 | 79 | $ GREP='e2e basic' npm run browser-coverage-full-test 80 | 81 | 82 | Running Tests in PhantomJS 83 | --- 84 | 85 | $ npm run browser-test-phantomjs 86 | 87 | 88 | You can filter the PhantomJS tests using the GREP env variable, e.g. 89 | 90 | $ GREP='e2e basic' npm run browser-test-phantomjs 91 | 92 | 93 | Running Tests in Chrome and Firefox Automatically 94 | --- 95 | 96 | Currently, this cannot be done in the VM as this project has not been configured to run Chrome and Firefox via Selenium headlessly. You can however use 97 | 98 | $ npm run test-firefox 99 | $ npm run test-chrome 100 | 101 | to test outside the VM, assuming you have Firefox and Chrome installed. 102 | 103 | Run tests in a browser 104 | --- 105 | 106 | $ npm run browser-server 107 | Use browser to visit http://127.0.0.1:8001/test/browser/index.html 108 | 109 | 110 | Run Saucelabs Tests In a Specific Browser 111 | --- 112 | 113 | $ CLIENT="saucelabs:internet explorer:9" SAUCE_USERNAME=paste-image-user 114 | SAUCE_ACCESS_KEY=323f6977-df33-4713-8967-511394cb07e5 npm run browser-test 115 | 116 | 117 | Updating gh-pages 118 | --- 119 | 120 | git checkout gh-pages 121 | git merge master 122 | git push origin gh-pages 123 | git checkout master 124 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | paste-image [![Build Status](https://travis-ci.org/redgeoff/paste-image.svg)](https://travis-ci.org/redgeoff/paste-image) [![Coverage Status](https://coveralls.io/repos/redgeoff/paste-image/badge.svg?branch=master&service=github)](https://coveralls.io/github/redgeoff/paste-image?branch=master) [![Dependency Status](https://david-dm.org/redgeoff/paste-image.svg)](https://david-dm.org/redgeoff/paste-image) 2 | === 3 | [![Selenium Test Status](https://saucelabs.com/browser-matrix/paste-image-user.svg)](https://saucelabs.com/u/paste-image-user) 4 | 5 | Cross-browser pasting of images 6 | 7 | 8 | [Live Demo](http://redgeoff.github.io/paste-image/examples) 9 | --- 10 | A [simple example](http://redgeoff.github.io/paste-image/examples) that works in all major browsers. 11 | 12 | 13 | Example 14 | --- 15 | 16 | ```js 17 | // Listen for all image paste events on a page 18 | pasteImage.on('paste-image', function (image) { 19 | 20 | // Display the image by appending it to the end of the body 21 | document.body.appendChild(image); 22 | 23 | }); 24 | ``` 25 | 26 | 27 | Install via NPM 28 | --- 29 | 30 | npm install paste-image 31 | 32 | 33 | Why? 34 | --- 35 | 36 | It's 2016 and Chrome is the only browser to properly implement the [Clipboard API](https://www.w3.org/TR/clipboard-apis). Let's wrap up some workarounds and provide an easy way to provide cross-browser image pasting. 37 | 38 | 39 | [Contributing](CONTRIBUTING.md) 40 | --- 41 | -------------------------------------------------------------------------------- /beautify.json: -------------------------------------------------------------------------------- 1 | { 2 | "indent_size": 2, 3 | "jslint_happy": true, 4 | "wrap_line_length": 100, 5 | "end_with_newline": true 6 | } 7 | -------------------------------------------------------------------------------- /bin/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Make sure deps are up to date 4 | # rm -r node_modules 5 | # npm install 6 | 7 | # get current version 8 | VERSION=$(node --eval "console.log(require('./package.json').version);") 9 | 10 | # Increment the version in master 11 | git add -A 12 | git commit -m "$VERSION" 13 | git push origin master 14 | 15 | # Build 16 | git checkout -b build 17 | 18 | npm run build 19 | 20 | # Publish npm release 21 | npm publish 22 | 23 | # Create git tag, which is also the Bower/Github release 24 | git add dist -f 25 | # git add bower.json component.json package.json lib/version-browser.js 26 | git rm -r bin scripts test 27 | 28 | git commit -m "build $VERSION" 29 | 30 | # Tag and push 31 | git tag $VERSION 32 | # TODO: can the following line be changed to git push origin master --tags $VERSION ?? 33 | git push --tags https://github.com/redgeoff/paste-image.git $VERSION 34 | 35 | # Cleanup 36 | git checkout master 37 | git branch -D build 38 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "paste-image", 3 | "version": "0.0.3", 4 | "description": "Cross-browser pasting of images", 5 | "main": "dist/paste-image.min.js", 6 | "homepage": "https://github.com/redgeoff/paste-image", 7 | "authors": [ 8 | "Geoffrey Cox" 9 | ], 10 | "keywords": [ 11 | "paste", 12 | "image", 13 | "browser" 14 | ], 15 | "license": "MIT", 16 | "ignore": [ 17 | "**/.*", 18 | "node_modules", 19 | "bower_components", 20 | "test", 21 | "npm-debug.log" 22 | ], 23 | "devDependencies": {}, 24 | "appPath": "app", 25 | "dependencies": {} 26 | } 27 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Paste Image Example 6 | 7 | 8 | 9 | 10 | 11 | 37 | 38 |

39 | Copy an image and then press Command+V (Mac) or Ctrl+V (Windows) anywhere in this window. 40 |

41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./scripts'); 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "paste-image", 3 | "version": "0.0.3", 4 | "description": "Cross-browser pasting of images", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/redgeoff/paste-image" 9 | }, 10 | "keywords": [ 11 | "paste", 12 | "image", 13 | "browser" 14 | ], 15 | "author": "Geoffrey Cox", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/redgeoff/paste-image/issues" 19 | }, 20 | "scripts": { 21 | "assert-beautified": "beautify-proj -i test -c beautify.json -e bundle.js && beautify-proj -i scripts -c beautify.json", 22 | "beautify": "beautify-proj -i test -o . -c beautify.json -e bundle.js && beautify-proj -i scripts -o . -c beautify.json", 23 | "jshint": "jshint -c .jshintrc *.js test scripts", 24 | "browser-server": "./test/browser/server.js", 25 | "browser-test": "./test/browser/test.js", 26 | "browser-test-firefox": "npm run jshint && CLIENT=selenium:firefox npm run browser-test", 27 | "browser-test-chrome": "npm run jshint && CLIENT=selenium:chrome npm run browser-test", 28 | "browser-test-phantomjs": "npm run jshint && CLIENT=selenium:phantomjs npm run browser-test", 29 | "browser-coverage-build": "browserify -t [ browserify-istanbul --ignore **/node_modules/** ] ./test/index.js -o test/browser-coverage/bundle.js -d", 30 | "browser-coverage-server": "./test/browser-coverage/server.js", 31 | "browser-coverage-test": "./test/browser-coverage/test.js", 32 | "browser-coverage-report": "istanbul report --dir coverage/browser --root coverage/browser lcov", 33 | "browser-coverage-check": "istanbul check-coverage --lines 100 --function 100 --statements 100 --branches 100 coverage/browser/coverage.json", 34 | "browser-coverage-full-test": "npm run jshint && npm run browser-coverage-build && npm run browser-coverage-test && npm run browser-coverage-report && npm run browser-coverage-check", 35 | "test": "npm run assert-beautified && npm run browser-coverage-full-test", 36 | "min": "uglifyjs dist/paste-image.js -mc > dist/paste-image.min.js", 37 | "build": "mkdir -p dist && browserify index.js -s pasteImage -o dist/paste-image.js && npm run min", 38 | "build-and-publish": "./bin/publish.sh" 39 | }, 40 | "dependencies": { 41 | "events": "^1.1.1", 42 | "inherits": "^2.0.3" 43 | }, 44 | "devDependencies": { 45 | "beautify-proj": "0.0.4", 46 | "blob-polyfill": "^1.0.20150320", 47 | "bluebird": "^3.4.6", 48 | "browserify": "^13.1.0", 49 | "browserify-istanbul": "^2.0.0", 50 | "chai": "^3.5.0", 51 | "chai-as-promised": "^6.0.0", 52 | "coveralls": "^2.11.12", 53 | "es5-shim": "^4.5.9", 54 | "http-server": "^0.9.0", 55 | "istanbul": "^0.4.5", 56 | "jshint": "^2.9.3", 57 | "mocha": "^3.0.2", 58 | "mocha-phantomjs": "^4.1.0", 59 | "request": "^2.74.0", 60 | "sauce-connect-launcher": "^0.16.0", 61 | "saucelabs": "^1.3.0", 62 | "selenium-standalone": "^5.7.0", 63 | "uglifyjs": "^2.4.10", 64 | "watchify": "^3.7.0", 65 | "wd": "^0.4.0" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /scripts/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This code is heavily based on Joel Basada's great work at 4 | // http://joelb.me/blog/2011/code-snippet-accessing-clipboard-images-with-javascript/ 5 | 6 | var inherits = require('inherits'), 7 | events = require('events'); 8 | 9 | var PasteImage = function () { 10 | this._initialized = false; 11 | this._wrapEmitterFns(); 12 | }; 13 | 14 | inherits(PasteImage, events.EventEmitter); 15 | 16 | // We want to wrap emitter functions so that we can ensure that we have initialized the document 17 | // listeners before listening to any paste events 18 | PasteImage.prototype._wrapEmitterFns = function () { 19 | var self = this, 20 | fns = ['on', 'once']; 21 | 22 | fns.forEach(function (fn) { 23 | PasteImage.prototype[fn] = function () { 24 | if (!self._initialized) { 25 | self._init(); 26 | } 27 | 28 | return events.EventEmitter.prototype[fn].apply(self, arguments); 29 | }; 30 | }); 31 | }; 32 | 33 | PasteImage.prototype._clipboardSupported = function () { 34 | return window.Clipboard; 35 | }; 36 | 37 | PasteImage.prototype._pasteCatcherFocus = function () { 38 | this._pasteCatcher.focus(); 39 | }; 40 | 41 | PasteImage.prototype._listenForClick = function () { 42 | var self = this; 43 | 44 | // Make sure it is always in focus. We ignore code coverage for this area as there does not appear 45 | // to be an easy cross-browser way of triggering a click event on the document 46 | // 47 | /* istanbul ignore next */ 48 | document.addEventListener('click', function () { 49 | self._pasteCatcherFocus(); 50 | }); 51 | }; 52 | 53 | PasteImage.prototype._createPasteCatcherIfNeeded = function () { 54 | // We start by checking if the browser supports the Clipboard object. If not, we need to create a 55 | // contenteditable element that catches all pasted data 56 | if (!this._clipboardSupported()) { 57 | this._pasteCatcher = document.createElement('div'); 58 | 59 | // Firefox allows images to be pasted into contenteditable elements 60 | this._pasteCatcher.setAttribute('contenteditable', ''); 61 | 62 | // We can hide the element and append it to the body, 63 | this._pasteCatcher.style.opacity = 0; 64 | 65 | // Use absolute positioning so that the paste catcher doesn't take up extra space. Note: we 66 | // cannot set style.display='none' as this will disable the functionality. 67 | this._pasteCatcher.style.position = 'absolute'; 68 | 69 | document.body.appendChild(this._pasteCatcher); 70 | 71 | this._pasteCatcher.focus(); 72 | this._listenForClick(); 73 | } 74 | }; 75 | 76 | PasteImage.prototype._listenForPaste = function () { 77 | var self = this; 78 | 79 | // Add the paste event listener. We ignore code coverage for this area as there does not appear to 80 | // be a cross-browser way of triggering a pase event 81 | // 82 | /* istanbul ignore next */ 83 | window.addEventListener('paste', function (e) { 84 | self._pasteHandler(e); 85 | }); 86 | }; 87 | 88 | PasteImage.prototype._init = function () { 89 | this._createPasteCatcherIfNeeded(); 90 | this._listenForPaste(); 91 | this._initialized = true; 92 | }; 93 | 94 | PasteImage.prototype._checkInputOnNextTick = function () { 95 | var self = this; 96 | // This is a cheap trick to make sure we read the data AFTER it has been inserted. 97 | setTimeout(function () { 98 | self._checkInput(); 99 | }, 1); 100 | }; 101 | 102 | PasteImage.prototype._pasteHandler = function (e) { 103 | // Starting to paste image 104 | this.emit('pasting-image', e); 105 | 106 | // We need to check if event.clipboardData is supported (Chrome) 107 | if (e.clipboardData && e.clipboardData.items) { 108 | // Get the items from the clipboard 109 | var items = e.clipboardData.items; 110 | 111 | // Loop through all items, looking for any kind of image 112 | for (var i = 0; i < items.length; i++) { 113 | if (items[i].type.indexOf('image') !== -1) { 114 | // We need to represent the image as a file 115 | var blob = items[i].getAsFile(); 116 | 117 | // Use a URL or webkitURL (whichever is available to the browser) to create a temporary URL 118 | // to the object 119 | var URLObj = this._getURLObj(); 120 | var source = URLObj.createObjectURL(blob); 121 | 122 | // The URL can then be used as the source of an image 123 | this._createImage(source); 124 | } 125 | } 126 | // If we can't handle clipboard data directly (Firefox), we need to read what was pasted from 127 | // the contenteditable element 128 | } else { 129 | this._checkInputOnNextTick(); 130 | } 131 | }; 132 | 133 | PasteImage.prototype._getURLObj = function () { 134 | return window.URL || window.webkitURL; 135 | }; 136 | 137 | // Parse the input in the paste catcher element 138 | PasteImage.prototype._checkInput = function () { 139 | // Store the pasted content in a variable 140 | var child = this._pasteCatcher.childNodes[0]; 141 | 142 | // Clear the inner html to make sure we're always getting the latest inserted content 143 | this._pasteCatcher.innerHTML = ''; 144 | 145 | if (child) { 146 | // If the user pastes an image, the src attribute will represent the image as a base64 encoded 147 | // string. 148 | if (child.tagName === 'IMG') { 149 | this._createImage(child.src); 150 | } 151 | } 152 | }; 153 | 154 | // Creates a new image from a given source 155 | PasteImage.prototype._createImage = function (source) { 156 | var self = this, 157 | pastedImage = new Image(); 158 | 159 | pastedImage.onload = function () { 160 | // You now have the image! 161 | self.emit('paste-image', pastedImage); 162 | }; 163 | pastedImage.src = source; 164 | }; 165 | 166 | module.exports = new PasteImage(); 167 | -------------------------------------------------------------------------------- /test/browser-coverage/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Tests Reporter Output to File 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 17 | 18 | 26 | 27 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /test/browser-coverage/phantom-hooks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | afterEnd: function (runner) { 5 | var fs = require('fs'); 6 | var coverage = runner.page.evaluate(function () { 7 | return window.__coverage__; 8 | }); 9 | 10 | if (coverage) { 11 | console.log('Writing coverage to coverage/browser/coverage.json'); 12 | fs.write('coverage/browser/coverage.json', JSON.stringify(coverage), 'w'); 13 | } else { 14 | console.log('No coverage data generated'); 15 | } 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /test/browser-coverage/server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var HTTP_PORT = 8001; 6 | var http_server = require("http-server"); 7 | 8 | http_server.createServer().listen(HTTP_PORT); 9 | console.log('Tests: http://127.0.0.1:' + HTTP_PORT + '/test/browser-coverage/index.html'); 10 | -------------------------------------------------------------------------------- /test/browser-coverage/test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | require('./server'); 6 | 7 | // Uncomment for debugging 8 | // (function() { 9 | // var childProcess = require("child_process"); 10 | // var oldSpawn = childProcess.spawn; 11 | // function mySpawn() { 12 | // console.log('spawn called'); 13 | // console.log(arguments); 14 | // var result = oldSpawn.apply(this, arguments); 15 | // return result; 16 | // } 17 | // childProcess.spawn = mySpawn; 18 | // })(); 19 | 20 | var spawn = require('child_process').spawn; 21 | 22 | var options = [ 23 | 'http://127.0.0.1:8001/test/browser-coverage/index.html', 24 | '--timeout', '25000', 25 | '--hooks', 'test/browser-coverage/phantom-hooks.js' 26 | ]; 27 | 28 | if (process.env.GREP) { 29 | options.push('-g'); 30 | options.push(process.env.GREP); 31 | } 32 | 33 | // Unless we have mocha-phantomjs installed globally we have to specify the full path 34 | // var child = spawn('mocha-phantomjs', options); 35 | var child = spawn('./node_modules/mocha-phantomjs/bin/mocha-phantomjs', options); 36 | 37 | child.stdout.on('data', function (data) { 38 | console.log(data.toString()); // echo output, including what could be errors 39 | }); 40 | 41 | child.stderr.on('data', function (data) { 42 | console.error(data.toString()); 43 | }); 44 | 45 | child.on('error', function (err) { 46 | console.error(err); 47 | }); 48 | 49 | child.on('close', function (code) { 50 | console.log('Mocha process exited with code ' + code); 51 | if (code > 0) { 52 | process.exit(1); 53 | } else { 54 | process.exit(0); 55 | } 56 | }); 57 | -------------------------------------------------------------------------------- /test/browser/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redgeoff/paste-image/672c0143edcc5d9c84ad352adc327dea232986a0/test/browser/google.png -------------------------------------------------------------------------------- /test/browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Mocha Tests 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 |
18 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /test/browser/sauce-results-updater.js: -------------------------------------------------------------------------------- 1 | // TODO: Is this really the best way to update the sauce lab job with the build and test status? If 2 | // so, then this should be made into a separate GH repo. 3 | 4 | 'use strict'; 5 | 6 | var request = require('request'), 7 | SauceLabs = require('saucelabs'), 8 | Promise = require('bluebird'); 9 | 10 | var Sauce = function (username, accessKey) { 11 | this._username = username; 12 | this._password = accessKey; 13 | this._sauceLabs = new SauceLabs({ 14 | username: username, 15 | password: accessKey 16 | }); 17 | }; 18 | 19 | Sauce.prototype.findJob = function (jobName) { 20 | // NOTE: it appears that there is no way to retrieve the job id when launching a test via the 21 | // sauce-connect-launcher package. Therefore, we will use the sauce API to look up the job id. The 22 | // saucelabs package doesn't support the full option for getJobs and we don't want to have to make 23 | // an API call for each job to determine whether the job name matches so we will execute this GET 24 | // request manually. 25 | var self = this; 26 | return new Promise(function (resolve, reject) { 27 | var opts = { 28 | url: 'https://saucelabs.com/rest/v1/' + self._username + '/jobs?full=true', 29 | auth: { 30 | user: self._username, 31 | password: self._password 32 | } 33 | }; 34 | request(opts, function (err, res, body) { 35 | if (err) { 36 | reject(err); 37 | } else { 38 | var jobs = JSON.parse(body); 39 | jobs.forEach(function (job) { 40 | if (job.name === jobName) { // matching job name? 41 | resolve(job); 42 | } 43 | }); 44 | } 45 | }); 46 | }); 47 | }; 48 | 49 | Sauce.prototype.updateJob = function (id, data) { 50 | var self = this; 51 | return new Promise(function (resolve, reject) { 52 | self._sauceLabs.updateJob(id, data, function (err, res) { 53 | if (err) { 54 | reject(err); 55 | } else { 56 | resolve(res); 57 | } 58 | }); 59 | }); 60 | }; 61 | 62 | Sauce.prototype.setPassed = function (jobName, build, passed) { 63 | var self = this; 64 | return self.findJob(jobName).then(function (job) { 65 | return self.updateJob(job.id, { 66 | build: build, 67 | passed: passed 68 | }); 69 | }); 70 | }; 71 | 72 | module.exports = Sauce; 73 | -------------------------------------------------------------------------------- /test/browser/server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var HTTP_PORT = 8001; 6 | 7 | var http_server = require('http-server'); 8 | var fs = require('fs'); 9 | var indexfile = './test/index.js'; 10 | var dotfile = './test/browser/.bundle.js'; 11 | var outfile = './test/browser/bundle.js'; 12 | var watchify = require('watchify'); 13 | var browserify = require('browserify'); 14 | var w = watchify(browserify(indexfile, { 15 | cache: {}, 16 | packageCache: {}, 17 | fullPaths: true, 18 | debug: true 19 | })); 20 | 21 | w.on('update', bundle); 22 | bundle(); 23 | 24 | var filesWritten = false; 25 | var serverStarted = false; 26 | var readyCallback; 27 | 28 | function bundle() { 29 | var wb = w.bundle(); 30 | wb.on('error', function (err) { 31 | console.error(String(err)); 32 | }); 33 | wb.on('end', end); 34 | wb.pipe(fs.createWriteStream(dotfile)); 35 | 36 | function end() { 37 | fs.rename(dotfile, outfile, function (err) { 38 | if (err) { 39 | return console.error(err); 40 | } 41 | console.log('Updated:', outfile); 42 | filesWritten = true; 43 | checkReady(); 44 | }); 45 | } 46 | } 47 | 48 | function startServers(callback) { 49 | readyCallback = callback; 50 | http_server.createServer().listen(HTTP_PORT); 51 | console.log('Tests: http://127.0.0.1:' + HTTP_PORT + '/test/browser/index.html'); 52 | serverStarted = true; 53 | checkReady(); 54 | } 55 | 56 | function checkReady() { 57 | if (filesWritten && serverStarted && readyCallback) { 58 | readyCallback(); 59 | } 60 | } 61 | 62 | if (require.main === module) { 63 | startServers(); 64 | } else { 65 | module.exports.start = startServers; 66 | } 67 | -------------------------------------------------------------------------------- /test/browser/test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var wd = require('wd'); 6 | var sauceConnectLauncher = require('sauce-connect-launcher'); 7 | var selenium = require('selenium-standalone'); 8 | var querystring = require('querystring'); 9 | var SauceResultsUpdater = require('./sauce-results-updater'); 10 | 11 | var server = require('./server.js'); 12 | 13 | var testTimeout = 30 * 60 * 1000; 14 | 15 | var retries = 0; 16 | var MAX_RETRIES = 10; 17 | var MS_BEFORE_RETRY = 60000; 18 | 19 | var username = process.env.SAUCE_USERNAME; 20 | var accessKey = process.env.SAUCE_ACCESS_KEY; 21 | 22 | var sauceResultsUpdater = new SauceResultsUpdater(username, accessKey); 23 | 24 | // process.env.CLIENT is a colon seperated list of 25 | // (saucelabs|selenium):browserName:browserVerion:platform 26 | var clientStr = process.env.CLIENT || 'selenium:phantomjs'; 27 | var tmp = clientStr.split(':'); 28 | var client = { 29 | runner: tmp[0] || 'selenium', 30 | browser: tmp[1] || 'phantomjs', 31 | version: tmp[2] || null, // Latest 32 | platform: tmp[3] || null 33 | }; 34 | 35 | var testUrl = 'http://127.0.0.1:8001/test/browser/index.html'; 36 | var qs = {}; 37 | 38 | var sauceClient; 39 | var sauceConnectProcess; 40 | var tunnelId = process.env.TRAVIS_JOB_NUMBER || 'tunnel-' + Date.now(); 41 | 42 | var jobName = tunnelId + '-' + clientStr; 43 | 44 | var build = (process.env.TRAVIS_COMMIT ? process.env.TRAVIS_COMMIT : Date.now()); 45 | 46 | if (client.runner === 'saucelabs') { 47 | qs.saucelabs = true; 48 | } 49 | if (process.env.GREP) { 50 | qs.grep = process.env.GREP; 51 | } 52 | testUrl += '?'; 53 | testUrl += querystring.stringify(qs); 54 | 55 | function testError(e) { 56 | console.error(e); 57 | console.error('Doh, tests failed'); 58 | sauceClient.quit(); 59 | process.exit(3); 60 | } 61 | 62 | function postResult(result) { 63 | var failed = !process.env.PERF && result.failed; 64 | if (client.runner === 'saucelabs') { 65 | sauceResultsUpdater.setPassed(jobName, build, !failed).then(function () { 66 | process.exit(failed ? 1 : 0); 67 | }); 68 | } else { 69 | process.exit(failed ? 1 : 0); 70 | } 71 | } 72 | 73 | function testComplete(result) { 74 | sauceClient.quit().then(function () { 75 | if (sauceConnectProcess) { 76 | sauceConnectProcess.close(function () { 77 | postResult(result); 78 | }); 79 | } else { 80 | postResult(result); 81 | } 82 | }); 83 | } 84 | 85 | function startSelenium(callback) { 86 | // Start selenium 87 | var opts = { 88 | version: '2.45.0' 89 | }; 90 | selenium.install(opts, function (err) { 91 | if (err) { 92 | console.error('Failed to install selenium'); 93 | process.exit(1); 94 | } 95 | selenium.start(opts, function ( /* err, server */ ) { 96 | sauceClient = wd.promiseChainRemote(); 97 | callback(); 98 | }); 99 | }); 100 | } 101 | 102 | function startSauceConnect(callback) { 103 | 104 | var options = { 105 | username: username, 106 | accessKey: accessKey, 107 | tunnelIdentifier: tunnelId 108 | }; 109 | 110 | sauceConnectLauncher(options, function (err, _sauceConnectProcess) { 111 | if (err) { 112 | console.error('Failed to connect to saucelabs, err=', err); 113 | 114 | if (++retries > MAX_RETRIES) { 115 | console.log('Max retries reached, exiting'); 116 | process.exit(1); 117 | } else { 118 | console.log('Retry', retries, '...'); 119 | setTimeout(function () { 120 | startSauceConnect(callback); 121 | }, MS_BEFORE_RETRY); 122 | } 123 | 124 | } else { 125 | sauceConnectProcess = _sauceConnectProcess; 126 | sauceClient = wd.promiseChainRemote('localhost', 4445, username, accessKey); 127 | callback(); 128 | } 129 | }); 130 | } 131 | 132 | function startTest() { 133 | 134 | console.log('Starting', client); 135 | 136 | var opts = { 137 | browserName: client.browser, 138 | version: client.version, 139 | platform: client.platform, 140 | tunnelTimeout: testTimeout, 141 | name: jobName, 142 | 'max-duration': 60 * 30, 143 | 'command-timeout': 599, 144 | 'idle-timeout': 599, 145 | 'tunnel-identifier': tunnelId 146 | }; 147 | 148 | sauceClient.init(opts).get(testUrl, function () { 149 | 150 | /* jshint evil: true */ 151 | var interval = setInterval(function () { 152 | 153 | sauceClient.eval('window.results', function (err, results) { 154 | 155 | console.log('=> ', results); 156 | 157 | if (err) { 158 | clearInterval(interval); 159 | testError(err); 160 | } else if (results.completed || results.failures.length) { 161 | clearInterval(interval); 162 | testComplete(results); 163 | } 164 | 165 | }); 166 | }, 10 * 1000); 167 | }); 168 | } 169 | 170 | server.start(function () { 171 | if (client.runner === 'saucelabs') { 172 | startSauceConnect(startTest); 173 | } else { 174 | startSelenium(startTest); 175 | } 176 | }); 177 | -------------------------------------------------------------------------------- /test/browser/webrunner.js: -------------------------------------------------------------------------------- 1 | /* global mocha */ 2 | 3 | (function () { 4 | 'use strict'; 5 | var runner = mocha.run(); 6 | window.results = { 7 | lastPassed: '', 8 | passed: 0, 9 | failed: 0, 10 | failures: [] 11 | }; 12 | 13 | runner.on('pass', function (e) { 14 | window.results.lastPassed = e.title; 15 | window.results.passed++; 16 | }); 17 | 18 | runner.on('fail', function (e) { 19 | window.results.failed++; 20 | window.results.failures.push({ 21 | title: e.title, 22 | message: e.err.message, 23 | stack: e.err.stack 24 | }); 25 | }); 26 | 27 | runner.on('end', function () { 28 | window.results.completed = true; 29 | window.results.passed++; 30 | }); 31 | })(); 32 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | chai.use(require('chai-as-promised')); 5 | chai.should(); 6 | 7 | require('./spec'); 8 | -------------------------------------------------------------------------------- /test/spec/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // NOTE: there doesn't appear to be a way to actually initiate a paste during automated browser 4 | // testing as browsers require the user to initiate a paste. As such, we'll fake the paste and will 5 | // have to be very careful when making changes to the code as the unit tests will not be able to 6 | // catch all the possible problems. 7 | 8 | var pasteImage = require('../../scripts'), 9 | Promise = require('bluebird'); 10 | 11 | describe('paste-image', function () { 12 | 13 | // // TODO: needed for saucelabs? 14 | // // The default of 2s is too low for IE 9 15 | // this.timeout(4000); 16 | 17 | var clipboardSupported = null, 18 | imgURL = '../browser/google.png'; 19 | 20 | before(function () { 21 | // Clear any previously set listeners 22 | pasteImage.removeAllListeners(); 23 | 24 | // Save so that we can fake 25 | clipboardSupported = pasteImage._clipboardSupported; 26 | 27 | // Fake 28 | pasteImage._clipboardSupported = function () { 29 | return false; 30 | }; 31 | }); 32 | 33 | after(function () { 34 | // Restore after any faking 35 | pasteImage._clipboardSupported = clipboardSupported; 36 | }); 37 | 38 | // A low performance polyfill based on toDataURL as Safari and IE don't yet support canvas.toBlob 39 | if (!HTMLCanvasElement.prototype.toBlob) { 40 | Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', { 41 | value: function (callback, type, quality) { 42 | 43 | var binStr = atob(this.toDataURL(type, quality).split(',')[1]), 44 | len = binStr.length, 45 | arr = new Uint8Array(len); 46 | 47 | for (var i = 0; i < len; i++) { 48 | arr[i] = binStr.charCodeAt(i); 49 | } 50 | 51 | callback(new Blob([arr], { 52 | type: type || 'image/png' 53 | })); 54 | } 55 | }); 56 | } 57 | 58 | var once = function (emitter, evnt) { 59 | return new Promise(function (resolve) { 60 | emitter.once(evnt, function () { 61 | resolve(arguments); 62 | }); 63 | }); 64 | }; 65 | 66 | var imageURLToBlob = function (url) { 67 | return new Promise(function (resolve) { 68 | var canvas = document.createElement('canvas'), 69 | context = canvas.getContext('2d'), 70 | img = new Image(); 71 | 72 | img.onload = function () { 73 | context.drawImage(img, img.width, img.height); 74 | canvas.toBlob(function (blob) { 75 | resolve(blob); 76 | }); 77 | }; 78 | img.src = url; 79 | }); 80 | }; 81 | 82 | var imageURLToImage = function (url) { 83 | return new Promise(function (resolve) { 84 | var img = new Image(); 85 | 86 | img.onload = function () { 87 | resolve(img); 88 | }; 89 | img.src = url; 90 | }); 91 | }; 92 | 93 | // To ensure that the image is being pasted properly, we'll compare dataURLs, i.e. we are 94 | // comparing the actual image data. 95 | var imageURLToDataURL = function (url) { 96 | return new Promise(function (resolve) { 97 | var canvas = document.createElement('canvas'), 98 | context = canvas.getContext('2d'), 99 | img = new Image(); 100 | 101 | img.onload = function () { 102 | context.drawImage(img, img.width, img.height); 103 | resolve(canvas.toDataURL('image/png')); 104 | }; 105 | img.src = url; 106 | }); 107 | }; 108 | 109 | var imageToDataURL = function (img) { 110 | var canvas = document.createElement('canvas'), 111 | context = canvas.getContext('2d'); 112 | 113 | context.drawImage(img, img.width, img.height); 114 | return canvas.toDataURL('image/png'); 115 | }; 116 | 117 | var imagesShouldEql = function (img1URL, img2) { 118 | var img2DataURL = imageToDataURL(img2); 119 | return imageURLToDataURL(img1URL).then(function (img1DataURL) { 120 | img1DataURL.should.eql(img2DataURL); 121 | }); 122 | }; 123 | 124 | // As implemented by Chrome now and hopefully Firefox, Safari and IE in the future 125 | it('should paste image via clipboardData', function () { 126 | var imagePasted = once(pasteImage, 'paste-image'), 127 | blob = null; 128 | 129 | return imageURLToBlob(imgURL).then(function (_blob) { 130 | 131 | blob = _blob; 132 | 133 | // Fake 134 | pasteImage._pasteHandler({ 135 | clipboardData: { 136 | items: [{ 137 | type: 'image', 138 | getAsFile: function () { 139 | return blob; 140 | } 141 | }, { 142 | type: 'not-img' 143 | }] 144 | } 145 | }); 146 | 147 | }).then(function () { 148 | return imagePasted; 149 | }).then(function (args) { 150 | return imagesShouldEql(imgURL, args[0]); 151 | }); 152 | }); 153 | 154 | // As implemented by Firefox, Safari and IE 155 | it('should paste image via pasteCatcher', function () { 156 | 157 | var imagePasted = once(pasteImage, 'paste-image'); 158 | 159 | return imageURLToImage(imgURL).then(function (img) { 160 | 161 | // Fake paste to pasteCatcher 162 | pasteImage._pasteCatcher.appendChild(img); 163 | 164 | // Fake unsupported clipboardData 165 | pasteImage._pasteHandler({ 166 | clipboardData: { 167 | items: undefined 168 | } 169 | }); 170 | 171 | }).then(function () { 172 | return imagePasted; 173 | }).then(function (args) { 174 | return imagesShouldEql(imgURL, args[0]); 175 | }); 176 | }); 177 | 178 | it('should check if clipboard supported', function () { 179 | // Mostly for test coverage 180 | pasteImage._clipboardSupported = clipboardSupported; 181 | (pasteImage._clipboardSupported() === window.Clipboard).should.eql(true); 182 | }); 183 | 184 | it('should not create paste catcher if clipboard supported', function () { 185 | pasteImage._clipboardSupported = function () { 186 | return true; 187 | }; 188 | pasteImage._createPasteCatcherIfNeeded(); 189 | }); 190 | 191 | it('should handle missing pasteCatcher children', function (done) { 192 | 193 | // Just to trigger init 194 | pasteImage.on('paste-image', function () {}); 195 | 196 | // Fake unsupported clipboardData 197 | pasteImage._pasteHandler({ 198 | clipboardData: { 199 | items: undefined 200 | } 201 | }); 202 | 203 | // We have to wait a bit for the _checkInput to run 204 | setTimeout(done, 10); 205 | 206 | }); 207 | 208 | it('should handle non image pasteCatcher children', function (done) { 209 | 210 | // Just to trigger init 211 | pasteImage.on('paste-image', function () {}); 212 | 213 | var div = document.createElement('div'); 214 | 215 | // Fake paste to pasteCatcher 216 | pasteImage._pasteCatcher.appendChild(div); 217 | 218 | // Fake unsupported clipboardData 219 | pasteImage._pasteHandler({ 220 | clipboardData: { 221 | items: undefined 222 | } 223 | }); 224 | 225 | // We have to wait a bit for the _checkInput to run 226 | setTimeout(done, 10); 227 | }); 228 | 229 | it('should paste catcher focus', function () { 230 | // Just to trigger init 231 | pasteImage.on('paste-image', function () {}); 232 | 233 | pasteImage._pasteCatcherFocus(); 234 | }); 235 | 236 | it('should also use wekitURL', function () { 237 | // Save for later 238 | var windowURL = window.URL; 239 | 240 | // Define webkitURL 241 | window.webkitURL = window.URL; 242 | 243 | // Remove window.URL 244 | window.URL = null; 245 | 246 | pasteImage._getURLObj().should.eql(window.webkitURL); 247 | 248 | // Restore 249 | window.URL = windowURL; 250 | }); 251 | 252 | }); 253 | --------------------------------------------------------------------------------