├── .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 [](https://travis-ci.org/redgeoff/paste-image) [](https://coveralls.io/github/redgeoff/paste-image?branch=master) [](https://david-dm.org/redgeoff/paste-image)
2 | ===
3 | [](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 |
--------------------------------------------------------------------------------