├── .gitignore ├── .npmignore ├── .travis.yml ├── src ├── effroi.js ├── dsl │ ├── element.js │ └── input.js ├── utils.js ├── devices │ ├── pointers.js │ ├── tactile.js │ ├── mouse.js │ └── keyboard.js └── ui │ └── focus.js ├── LICENCE ├── package.json ├── karma.conf.js ├── test ├── input.js ├── element.js ├── focus.js ├── pointers.js ├── tactile.js ├── keyboard.js └── mouse.js ├── Gruntfile.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | *~ 3 | *.swp 4 | *.swo 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | node_modules 3 | dist 4 | *~ 5 | *.swp 6 | *.swo 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | 5 | before_script: 6 | - export DISPLAY=:99.0 7 | - sh -e /etc/init.d/xvfb start 8 | - npm install -g grunt-cli 9 | - npm install --dev 10 | 11 | script: 12 | - grunt travis -------------------------------------------------------------------------------- /src/effroi.js: -------------------------------------------------------------------------------- 1 | // Devices 2 | module.exports.mouse = require('./devices/mouse.js'); 3 | module.exports.keyboard = require('./devices/keyboard.js'); 4 | module.exports.tactile = require('./devices/tactile.js'); 5 | module.exports.pointers = require('./devices/pointers.js'); 6 | 7 | // UI 8 | module.exports.focus = require('./ui/focus.js'); 9 | 10 | // DSL 11 | module.exports.element = require('./dsl/element.js'); 12 | module.exports.input = require('./dsl/input.js'); 13 | -------------------------------------------------------------------------------- /src/dsl/element.js: -------------------------------------------------------------------------------- 1 | function Element(selector) { 2 | 3 | var mouse = require('../devices/mouse.js'); 4 | 5 | this.selector = selector; 6 | this.element = document.querySelector(selector); 7 | if (!this.element) { 8 | throw new Error("Element not found using selector '" + selector + "'"); 9 | } 10 | 11 | this.isVisible = function isVisible() { 12 | try { 13 | var comp = window.getComputedStyle(this.element, null); 14 | return comp.visibility !== 'hidden' && 15 | comp.display !== 'none' && 16 | this.element.offsetHeight > 0 && 17 | this.element.offsetWidth > 0; 18 | } catch (e) {console.log(e); 19 | return false; 20 | } 21 | }; 22 | 23 | this.click = function click() { 24 | return mouse.click(this.element); 25 | }; 26 | 27 | this.dblclick = function dblclick() { 28 | return mouse.dblclick(this.element); 29 | }; 30 | } 31 | 32 | module.exports = function element(selector) { 33 | return new Element(selector); 34 | }; 35 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2013-2015 Nicolas Froidure, Raphaël Rougeron and contributors 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "effroi", 3 | "version": "0.0.0", 4 | "description": "UI testing library.", 5 | "author": "Raphaël Rougeron, Nicolas Froidure", 6 | "main": "dist/effroi.js", 7 | "scripts": { 8 | "test": "grunt test" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/francejs/effroi.git" 13 | }, 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/francejs/effroi/issues" 17 | }, 18 | "devDependencies": { 19 | "karma": "~0.10.8", 20 | "karma-chrome-launcher": "~0.1.1", 21 | "karma-firefox-launcher": "~0.1.2", 22 | "karma-phantomjs-launcher": "~0.1.1", 23 | "grunt": "~0.4.2", 24 | "grunt-contrib-clean": "~0.5.0", 25 | "grunt-contrib-watch": "~0.5.3", 26 | "grunt-contrib-jshint": "~0.7.2", 27 | "grunt-browserify": "~1.3.0", 28 | "grunt-parallel": "~0.3.1", 29 | "grunt-karma": "~0.6.2", 30 | "karma-mocha": "~0.1.1", 31 | "karma-chai": "0.0.2", 32 | "karma-sinon": "1.0.0", 33 | "matchdep": "~0.3.0" 34 | }, 35 | "keywords": [ 36 | "effroi", 37 | "mouse", 38 | "keyboard", 39 | "touch", 40 | "ui", 41 | "test", 42 | "testing", 43 | "event", 44 | "events", 45 | "simulation" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /src/dsl/input.js: -------------------------------------------------------------------------------- 1 | function Input(elementOrSelector) { 2 | 3 | var mouse = require('../devices/mouse.js'); 4 | 5 | if (typeof elementOrSelector == 'string') { 6 | this.element = document.querySelector(elementOrSelector); 7 | if (!this.element) { 8 | throw new Error("Element not found using selector '" + elementOrSelector + "'"); 9 | } 10 | } else { 11 | if (!(elementOrSelector instanceof HTMLElement)) { 12 | throw new Error("Invalid input() arg: only selector or HTMLElement are supported"); 13 | } 14 | this.element = elementOrSelector; 15 | } 16 | 17 | this.val = function val() { 18 | return this.element.value; 19 | }; 20 | 21 | this.set = function set(value) { 22 | try { 23 | this.element.focus(); 24 | } catch (e) { 25 | throw new Error("Unable to focus() input field " + this.element.getAttribute('name') + ": " + e); 26 | } 27 | 28 | this.element.value = value; 29 | }; 30 | 31 | this.fill = function fill(value, method) { 32 | method = method || 'paste'; 33 | switch(method) { 34 | case 'paste': 35 | mouse.paste(this.element, value); 36 | break; 37 | } 38 | }; 39 | } 40 | 41 | module.exports = function input(elementOrSelector) { 42 | return new Input(elementOrSelector); 43 | }; -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Tue Oct 15 2013 15:27:53 GMT+0200 (CEST) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path, that will be used to resolve files and exclude 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | frameworks: ['mocha','chai','sinon'], 13 | 14 | 15 | // list of files / patterns to load in the browser 16 | files: [ 17 | 'dist/effroi.js', 18 | 'test/**/*.js' 19 | ], 20 | 21 | 22 | // list of files to exclude 23 | exclude: [ 24 | 25 | ], 26 | 27 | 28 | // test results reporter to use 29 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' 30 | reporters: ['progress'], 31 | 32 | 33 | // web server port 34 | port: 9876, 35 | 36 | 37 | // enable / disable colors in the output (reporters and logs) 38 | colors: true, 39 | 40 | 41 | // level of logging 42 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 43 | logLevel: config.LOG_INFO, 44 | 45 | 46 | // enable / disable watching file and executing tests whenever any file changes 47 | autoWatch: true, 48 | 49 | 50 | // Start these browsers, currently available: 51 | // - Chrome 52 | // - ChromeCanary 53 | // - Firefox 54 | // - Opera 55 | // - Safari (only Mac) 56 | // - PhantomJS 57 | // - IE (only Windows) 58 | browsers: ['Chrome', 'Firefox', 'PhantomJS'], 59 | 60 | 61 | // If browser does not capture in given timeout [ms], kill it 62 | captureTimeout: 60000, 63 | 64 | 65 | // Continuous Integration mode 66 | // if true, it capture browsers, run tests and exit 67 | singleRun: false 68 | }); 69 | }; 70 | -------------------------------------------------------------------------------- /test/input.js: -------------------------------------------------------------------------------- 1 | describe("Input DSL", function() { 2 | function createElement(html) { 3 | var elt = document.createElement('div'); 4 | elt.id = 'foo'; 5 | elt.innerHTML = html; 6 | document.body.appendChild(elt); 7 | } 8 | 9 | var assert = chai.assert, 10 | input = effroi.input; 11 | 12 | afterEach(function() { 13 | document.body.removeChild(document.getElementById('foo')); 14 | }); 15 | 16 | var textInputs = [ 17 | { 18 | type: "input[type='text']", 19 | html: '' 20 | }, 21 | { 22 | type: "input[type='password']", 23 | html: '' 24 | }, 25 | { 26 | type: "textarea", 27 | html: '' 28 | } 29 | ]; 30 | 31 | textInputs.forEach(function(textInput) { 32 | describe("input() with an "+textInput.type, function() { 33 | beforeEach(function() { 34 | createElement(textInput.html); 35 | }); 36 | 37 | describe("val()", function() { 38 | it("should return the input value", function() { 39 | assert.equal('hello', input('[name="bar"]').val()); 40 | }); 41 | }); 42 | 43 | describe("set()", function() { 44 | it("should set the input value", function() { 45 | input('[name="bar"]').set('salut'); 46 | assert.equal('salut', document.querySelector('[name="bar"]').value); 47 | }); 48 | }); 49 | 50 | describe("fill()", function() { 51 | it("can paste the input value", function() { 52 | input('[name="bar"]').fill('salut', 'paste'); 53 | assert.equal('salut', document.querySelector('[name="bar"]').value); 54 | }); 55 | }); 56 | }); 57 | }); 58 | }); -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks); 4 | 5 | grunt.initConfig({ 6 | clean: ['dist'], 7 | 8 | jshint: { 9 | all: { 10 | src: ['Gruntfile.js', 'src/**/*.js'], 11 | options: { 12 | '-W014': true 13 | } 14 | } 15 | }, 16 | 17 | browserify: { 18 | lib: { 19 | src: 'src/effroi.js', 20 | dest: 'dist/effroi.js', 21 | options: { 22 | standalone: 'effroi' 23 | } 24 | } 25 | }, 26 | 27 | karma: { 28 | local: { 29 | configFile: 'karma.conf.js' 30 | }, 31 | travis: { 32 | configFile: 'karma.conf.js', 33 | singleRun: true, 34 | browsers: ['Firefox', 'PhantomJS'] 35 | } 36 | }, 37 | 38 | watch: { 39 | code: { 40 | files: ['src/**/*.js'], 41 | tasks: ['jshint', 'dist'] 42 | } 43 | }, 44 | 45 | parallel: { 46 | testing: { 47 | options: { 48 | stream: true 49 | }, 50 | tasks: [{ 51 | grunt: true, 52 | args: ['karma:local'] 53 | },{ 54 | grunt: true, 55 | args: ['watch'] 56 | }] 57 | } 58 | } 59 | }); 60 | 61 | grunt.registerTask('dist', [ 62 | 'clean', 63 | 'browserify' 64 | ]); 65 | 66 | grunt.registerTask('test', [ 67 | 'dist', 68 | 'parallel:testing' 69 | ]); 70 | 71 | grunt.registerTask('hint', [ 72 | 'jshint' 73 | ]); 74 | 75 | grunt.registerTask('default', [ 76 | 'test' 77 | ]); 78 | 79 | grunt.registerTask('travis', [ 80 | 'dist', 81 | 'karma:travis' 82 | ]); 83 | }; 84 | -------------------------------------------------------------------------------- /test/element.js: -------------------------------------------------------------------------------- 1 | describe("Element DSL", function() { 2 | var assert = chai.assert, 3 | element = effroi.element, 4 | mouse = effroi.mouse; 5 | 6 | describe("element()", function() { 7 | beforeEach(function() { 8 | var elt = document.createElement('div'); 9 | elt.id = 'foo'; 10 | elt.innerHTML = 'test'; 11 | document.body.appendChild(elt); 12 | }); 13 | 14 | afterEach(function() { 15 | document.body.removeChild(document.getElementById('foo')); 16 | }); 17 | 18 | it("should return an object", function() { 19 | assert(typeof element('#foo') == 'object'); 20 | }); 21 | 22 | it("should throw if the provided selector doesn't match any element", function() { 23 | assert.throw(function() { element('#bar'); }, Error); 24 | }); 25 | 26 | describe("isVisible()", function() { 27 | it("should return true if the element is visible", function() { 28 | assert.ok(element('#foo').isVisible()); 29 | }); 30 | 31 | it("should return false if the element's visibility is set to hidden", function() { 32 | document.getElementById('foo').setAttribute('style', 'visibility: hidden'); 33 | assert.notOk(element('#foo').isVisible()); 34 | }); 35 | }); 36 | 37 | describe("click()", function() { 38 | before(function() { 39 | sinon.spy(mouse, 'click'); 40 | }); 41 | 42 | after(function() { 43 | mouse.click.restore(); 44 | }); 45 | 46 | it("should call mouse.click() with an element", function() { 47 | element('#foo').click(); 48 | assert(mouse.click.calledOnce); 49 | assert(mouse.click.args[0][0] instanceof Element); 50 | }); 51 | }); 52 | 53 | describe("dblclick()", function() { 54 | before(function() { 55 | sinon.spy(mouse, 'dblclick'); 56 | }); 57 | 58 | after(function() { 59 | mouse.dblclick.restore(); 60 | }); 61 | 62 | it("should call mouse.dblclick() with an element", function() { 63 | element('#foo').dblclick(); 64 | assert(mouse.dblclick.calledOnce); 65 | assert(mouse.dblclick.args[0][0] instanceof Element); 66 | }); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Effroi.js](http://francejs.org/effroi/images/effroi.png) 2 | 3 | [![Build Status](https://travis-ci.org/francejs/effroi.png?branch=master)](https://travis-ci.org/francejs/effroi) 4 | 5 | A JavaScript event-simulation, device oriented library for UI testing. 6 | 7 | ## Device oriented ? 8 | 9 | Effroi try to be the closest possible of the way your users use their input 10 | devices. To achieve this goal, effroi emulate real physical devices and fires 11 | every events a real device would fire for the same action. 12 | 13 | By example, when a user want to click an element the following events sequence 14 | are fired: 15 | - several mousewheel events to scroll to the element (if needed) 16 | - several mouseout/mousemove/mouseover events to go over the element (if needed) 17 | - mousedown/mouseup events 18 | - a click event if none of the 2 previous events have been prevented 19 | 20 | That's typically what effroi emulate for your UI tests. Effroi will also check 21 | for the feasibility of the requested action. If you try to click an element 22 | that is nor visible nor clickable, effroi will throw an error. 23 | 24 | ## How to use 25 | 26 | ### Device API 27 | 28 | The device API goal is to emulate a real device and fire every events usually 29 | fired for each action a device permit. 30 | 31 | #### Mouse 32 | ```js 33 | var mouse = effroi.mouse; 34 | // Scrolling with the mouse 35 | mouse.scroll(x, y); // Returns true if scrolled false otherwise 36 | mouse.scrollTo(element); // Returns true if scrolled 37 | // Moving the cursor 38 | mouse.move(x, y); // Returns true if dispatched 39 | mouse.moveTo(element); // Returns true if dispatched 40 | // Clicking 41 | mouse.click(element); // Returns true if dispatched 42 | // Right-clicking 43 | mouse.rightclick(element); // Returns true if dispatched 44 | // Double-clicking 45 | mouse.dblclick(element); // Returns true if dispatched 46 | // Pasting with the mouse 47 | mouse.paste(inputElement, 'content'); 48 | // Cutting with the mouse 49 | mouse.cut(inputElement); // Returns the cutted content 50 | ``` 51 | 52 | #### Keyboard 53 | ```js 54 | var kbd = effroi.keyboard; 55 | // Tabbing with the keyboard 56 | kbd.tab(); // Returns true if dispatched 57 | // Focusing an element 58 | kbd.focus(element); // Returns true if focus changed 59 | // Hitting a key 60 | kbd.hit('a'); // Returns true if dispatched 61 | kbd.hit('b','c','d'); // Returns true if dispatched 62 | // Combining keys 63 | kbd.combine(this.CTRL, 'c'); // Returns true if dispatched 64 | // Pasting with the keyboard 65 | kbd.paste('content'); 66 | // Cutting with the keyboard 67 | kbd.cut(); // Returns the cutted content 68 | ``` 69 | 70 | #### Tactile 71 | ```js 72 | var tactile = effroi.tactile; 73 | // Scrolling with the tactile display 74 | tactile.scroll(x, y); // Returns true if scrolled false otherwise 75 | tactile.scrollTo(element); // Returns true if scrolled 76 | // Performing a touch 77 | tactile.touch(); // Returns true if dispatched 78 | ``` 79 | 80 | #### Unified pointing device API (PointerEvents) 81 | Under development. 82 | 83 | ## How to contribute 84 | 85 | 1. Clone this repo 86 | 2. npm install 87 | 3. To run the tests: 88 | 89 | ```sh 90 | grunt test 91 | ``` 92 | 4. To build the lib: 93 | 94 | ```sh 95 | grunt dist 96 | ``` 97 | -------------------------------------------------------------------------------- /test/focus.js: -------------------------------------------------------------------------------- 1 | var focus = effroi.focus, 2 | assert = chai.assert; 3 | 4 | // Helper 5 | var elt, evts=[], listeners=[]; 6 | function regEventListener(elt, type, capture, stop, prevent) { 7 | var listener=function(e) { 8 | if(e.type!=type) { 9 | throw 'Event types differs.' 10 | } 11 | evts.push({ 12 | type : e.type, 13 | target : e.target, 14 | currentTarget : e.currentTarget, 15 | clientX : e.clientX, 16 | clientY : e.clientY, 17 | altKey : e.altKey, 18 | ctrlKey : e.ctrlKey, 19 | shiftKey : e.shiftKey, 20 | metaKey : e.metaKey, 21 | button : e.button, 22 | buttons : e.buttons, 23 | pointerType : e.pointerType, 24 | charCode : e.charCode, 25 | char : e.char, 26 | view : e.view, 27 | relatedTarget : e.relatedTarget 28 | }); 29 | if(stop) { 30 | e.stopPropagation(); 31 | } 32 | if(prevent) { 33 | e.preventDefault(); 34 | } 35 | }; 36 | elt.addEventListener(type, listener, capture); 37 | listeners.push({ 38 | elt: elt, 39 | type: type, 40 | listener : listener, 41 | capture : capture 42 | }); 43 | } 44 | 45 | function init(innerHTML) { 46 | elt = document.createElement('div'); 47 | elt.innerHTML = innerHTML || 'foo'; 48 | document.body.appendChild(elt); 49 | } 50 | 51 | function uninit() { 52 | document.body.removeChild(elt); 53 | for(var i=listeners.length-1; i>=0; i--) { 54 | listeners[i].elt.removeEventListener( 55 | listeners[i].type, listeners[i].listener, listeners[i].capture); 56 | } 57 | evts=[]; 58 | elt=null; 59 | } 60 | 61 | describe("UI focus", function() { 62 | 63 | describe("set to an element when another element is focused", function() { 64 | 65 | var previousActiveElement; 66 | 67 | before(function() { 68 | init( 69 | '

' 70 | + '' 71 | + '' 72 | + '

' 73 | ); 74 | elt.firstChild.firstChild.lastChild.focus(); 75 | regEventListener(elt.firstChild.firstChild.lastChild, 'blur'); 76 | regEventListener(elt.firstChild.firstChild.lastChild, 'focusout'); 77 | regEventListener(elt.firstChild.lastChild.lastChild, 'focus'); 78 | regEventListener(elt.firstChild.lastChild.lastChild, 'focusin'); 79 | regEventListener(document.body, 'focusout'); 80 | regEventListener(document.body, 'focusin'); 81 | }); 82 | 83 | after(uninit); 84 | 85 | it("should return true", function() { 86 | assert.equal(focus.focus(elt.firstChild.lastChild.lastChild), true); 87 | }); 88 | 89 | it("should set the element as the active element", function() { 90 | assert.equal(elt.firstChild.lastChild.lastChild, document.activeElement); 91 | }); 92 | 93 | it("should trigger a blur event on the previously focused element", function() { 94 | assert.equal(evts[0].type, 'blur'); 95 | assert.equal(evts[0].target, elt.firstChild.firstChild.lastChild); 96 | }); 97 | 98 | it("should not bubble the blur event on the element parents", function() { 99 | assert.notEqual(evts[1].type, 'blur'); 100 | }); 101 | 102 | it("should trigger a focusout event on the previously focused element", function() { 103 | assert.equal(evts[1].type, 'focusout'); 104 | assert.equal(evts[1].target, elt.firstChild.firstChild.lastChild); 105 | }); 106 | 107 | it("should bubble the focusout event to element parents", function() { 108 | assert.equal(evts[2].type, 'focusout'); 109 | assert.equal(evts[2].target, elt.firstChild.firstChild.lastChild); 110 | }); 111 | 112 | it("should trigger a focus event on the newly focused element", function() { 113 | assert.equal(evts[3].type, 'focus'); 114 | assert.equal(evts[3].target, elt.firstChild.lastChild.lastChild); 115 | }); 116 | 117 | it("should not bubble the focus event on the element parents", function() { 118 | assert.notEqual(evts[4].type, 'focus'); 119 | }); 120 | 121 | it("should trigger a focusin event on the previously focused element", function() { 122 | assert.equal(evts[4].type, 'focusin'); 123 | assert.equal(evts[4].target, elt.firstChild.lastChild.lastChild); 124 | }); 125 | 126 | it("should bubble the focusin event to element parents", function() { 127 | assert.equal(evts[5].type, 'focusin'); 128 | assert.equal(evts[5].target, elt.firstChild.lastChild.lastChild); 129 | }); 130 | 131 | }); 132 | 133 | }); 134 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | module.exports={ 2 | 3 | setEventCoords: function(event, x, y) { 4 | this.setEventProperty(event, 'clientX', x); 5 | this.setEventProperty(event, 'clientY', y); 6 | this.setEventProperty(event, 'pageX', x + window.scrollX); 7 | this.setEventProperty(event, 'pageY', y + window.scrollY); 8 | }, 9 | 10 | // Find the center of an element 11 | getElementCenter: function(element) { 12 | var c={}; 13 | try { 14 | var rect = element.getBoundingClientRect(); 15 | c.x = Math.floor((rect.left + rect.right) / 2); 16 | c.y = Math.floor((rect.top + rect.bottom) / 2); 17 | } catch(e) { 18 | c.x = 1; 19 | c.y = 1; 20 | } 21 | return c; 22 | }, 23 | 24 | // Find a point in the viewport at wich an element can be the root of 25 | // a pointer event (is not under another element) 26 | getPossiblePointerCoords: function(element) { 27 | var comp, rects, coords = null; 28 | if(!(element instanceof HTMLElement)) { 29 | throw new Error('getPossiblePointerCoords needs a valid HTMLElement.'); 30 | } 31 | comp = window.getComputedStyle(element, null); 32 | rects=element.getClientRects(); 33 | if('none' !== comp.pointerEvents && rects.length) { 34 | mainLoop: for(var i=rects.length-1; i>=0; i--) { 35 | for(var x=rects[i].left, mX=rects[i].right; x window.innerWidth ? x - window.innerWidth : 0) 49 | ), 50 | scrollY = (y < 0 ? y : 51 | (y > window.innerHeight ? y - window.innerHeight : 0) 52 | ), 53 | moveX=Math.round(window.innerWidth/2), 54 | moveY=Math.round(window.innerHeight/2); 55 | options = options || {}; 56 | // Put the finger on the middle of the screen 57 | options.type = 'touchstart'; 58 | dispatched = this.dispatch(document.elementFromPoint(moveX, moveY), 59 | options); 60 | // Moving through the x/y axis 61 | while(dispatched && (scrollX !== 0 || scrollY !== 0)) { 62 | // repeat the move if the finger is about to go out of the screen 63 | if(moveX<10||moveY<10 64 | ||moveX>window.innerWidth-10 65 | ||moveY>window.innerHeight-10) { 66 | moveX = Math.round(window.innerWidth/2); 67 | moveY = Math.round(window.innerHeight/2); 68 | // Remove the finger of the screen 69 | options.type = 'touchend'; 70 | dispatched = this.dispatch(document.elementFromPoint(moveX, moveY), 71 | options); 72 | // Re-put the finger on the middle of the screen 73 | options.type = 'touchstart'; 74 | dispatched = this.dispatch(document.elementFromPoint(moveX, moveY), 75 | options); 76 | } 77 | // Move the finger 78 | options.type = 'touchmove'; 79 | dispatched = dispatched && 80 | this.dispatch(document.elementFromPoint(moveX, moveY), options); 81 | if(dispatched) { 82 | moveX += (scrollX < 0 ? 5 : -5); 83 | moveY += (scrollY < 0 ? 5 : -5); 84 | window.scrollTo(window.scrollX 85 | - (scrollX < 0 ? 120 : (scrollX > 0 ? -120 : 0)), 86 | window.scrollY 87 | - (scrollY < 0 ? 120 : (scrollY > 0 ? -120 : 0))); 88 | if(scrollX) { 89 | scrollX = (scrollX < 0 ? 90 | (scrollX + 120 > 0 ? 0 : scrollX + 120) : 91 | (scrollX - 120 < 0 ? 0 : scrollX - 120)); 92 | } 93 | if(scrollY) { 94 | scrollY = (scrollY < 0 ? 95 | (scrollY + 120 > 0 ? 0 : scrollY + 120) : 96 | (scrollY - 120 < 0 ? 0 : scrollY - 120)); 97 | } 98 | } 99 | } 100 | // Remove the finger of the screen 101 | options.type = 'touchend'; 102 | dispatched = dispatched && 103 | this.dispatch(document.elementFromPoint(moveX, moveY), options); 104 | return dispatched; 105 | }; 106 | 107 | /** 108 | * Perform a scroll with the fingers to an element. 109 | * 110 | * @param DOMElement element A DOMElement on wich to scroll to 111 | * @param Object options Touch options 112 | * @return Boolean 113 | */ 114 | this.scrollTo = function scrollTo(element, options) { 115 | // Getting element center 116 | var c = utils.getElementCenter(element); 117 | // Scroll only if the element is not already in the viewport 118 | if(c.x<0 || c.y<0 || c.x > window.innerWidth || c.y > window.innerHeight) { 119 | return this.scroll(c.x, c.y, options); 120 | } 121 | return false; 122 | }; 123 | 124 | /** 125 | * Dispatches a touch event to the given DOM element. 126 | * 127 | * @param DOMElement element A DOMElement on wich to dispatch the event 128 | * @param Object options Event options 129 | * @return Boolean 130 | */ 131 | this.dispatch = function (element,options) { 132 | var event = document.createEvent('UIEvent'), coords; 133 | options = options || {}; 134 | options.canBubble = ('false' === options.canBubble ? false : true); 135 | options.cancelable = ('false' === options.cancelable ? false : true); 136 | options.view = options.view || window; 137 | options.detail = options.detail || 1; 138 | options.altKey = !!options.altKey; 139 | options.ctrlKey = !!options.ctrlKey; 140 | options.shiftKey = !!options.shiftKey; 141 | options.metaKey = !!options.metaKey; 142 | options.changedTouches = options.changedTouches || []; 143 | options.touches = options.touches || []; 144 | options.scale = options.scale || 1.0; 145 | options.rotation = options.rotation || 0.0; 146 | coords = utils.getPossiblePointerCoords(element); 147 | if(null===coords) { 148 | throw Error('Unable to find a point in the viewport at wich the given' 149 | +' element can receive a touch event.'); 150 | } 151 | // Safari, Firefox: must use initTouchEvent. 152 | if ("function" === typeof event.initTouchEvent) { 153 | event.initTouchEvent(options.type, 154 | options.canBubble, options.cancelable, 155 | options.view, options.detail, 156 | // Screen coordinates (relative to the whole user screen) 157 | // FIXME: find a way to get the right screen coordinates 158 | coords.x + window.screenLeft, coords.y + window.screenTop, 159 | // Client coordinates (relative to the viewport) 160 | coords.x, coords.y, 161 | options.ctrlKey, options.altKey, 162 | options.shiftKey, options.metaKey, 163 | options.touches, options.targetTouches, options.changedTouches, 164 | options.scale, options.rotation); 165 | utils.setEventCoords(event, coords.x, coords.y); 166 | } else { 167 | event.initUIEvent(options.type, 168 | options.canBubble, options.cancelable, 169 | options.view, options.detail); 170 | utils.setEventProperty(event, 'altKey', options.altKey); 171 | utils.setEventProperty(event, 'ctrlKey', options.ctrlKey); 172 | utils.setEventProperty(event, 'shiftKey', options.shiftKey); 173 | utils.setEventProperty(event, 'metaKey', options.metaKey); 174 | utils.setEventProperty(event, 'changedTouches', options.changedTouches); 175 | utils.setEventProperty(event, 'touches', options.touches); 176 | utils.setEventProperty(event, 'scale', options.scale); 177 | utils.setEventProperty(event, 'rotation', options.rotation); 178 | utils.setEventCoords(event, coords.x, coords.y); 179 | } 180 | return element.dispatchEvent(event); 181 | }; 182 | 183 | } 184 | 185 | module.exports = new Tactile(); 186 | -------------------------------------------------------------------------------- /src/devices/mouse.js: -------------------------------------------------------------------------------- 1 | function Mouse() { 2 | 3 | var utils = require('../utils.js'); 4 | var uiFocus = require('../ui/focus.js'); 5 | 6 | // Consts 7 | this.LEFT_BUTTON = 1; 8 | this.RIGHT_BUTTON = 2; 9 | this.MIDDLE_BUTTON = 4; 10 | this.BACK_BUTTON = 8; 11 | this.FORWARD_BUTTON = 16; 12 | this.BUTTONS_MASK = this.LEFT_BUTTON | this.RIGHT_BUTTON 13 | | this.MIDDLE_BUTTON | this.BACK_BUTTON | this.FORWARD_BUTTON; 14 | 15 | // Private vars 16 | var _x = 1, _y = 1; 17 | 18 | /** 19 | * Select content like if it were done by a user with his mouse. 20 | * 21 | * @param DOMElement element A DOMElement to dblclick 22 | * @param Number start The selection start 23 | * @param Number end The selection end 24 | * @return Boolean 25 | */ 26 | this.select = function cut(element, start, end) { 27 | if(!utils.isSelectable(element)) { 28 | throw Error('Cannot select the element content.'); 29 | } 30 | if(!start) { 31 | start = 0; 32 | } else if(start < 0 || start > element.value.length) { 33 | throw RangeError('Invalid selection start.'); 34 | } 35 | if(!end) { 36 | end = element.value.length; 37 | } else if(end > element.value.length || end < start) { 38 | throw RangeError('Invalid selection end.'); 39 | } 40 | // We move to the element if not over yet 41 | this.moveTo(element); 42 | // To select, we keep the mousedown over the input 43 | options = {}; 44 | options.type = 'mousedown'; 45 | // if the mousedown event is prevented we can't select content 46 | if(!this.dispatch(element, options)) { 47 | return false; 48 | } 49 | // We move over the selection to perform 50 | // FIXME: This should be done better with real coords 51 | options.type = 'mousemove'; 52 | this.dispatch(element, options); 53 | // if the mouseup event is prevented the whole content is selected 54 | options.type = 'mouseup'; 55 | if(!this.dispatch(element, options)) { 56 | end = element.value.length; 57 | } 58 | // finally selecting the content 59 | element.selectionStart = start; 60 | element.selectionEnd = end; 61 | return true; 62 | }; 63 | 64 | /** 65 | * Cut selected content like if it were done by a user with his mouse. 66 | * 67 | * @param DOMElement element A DOMElement to dblclick 68 | * @param String content The content to paste 69 | * @return Boolean 70 | */ 71 | this.cut = function cut(element) { 72 | var content; 73 | // We move to the element if not over yet 74 | this.moveTo(element); 75 | // To cut, we right-click but only the mousedown is fired due to the 76 | // contextual menu that appears 77 | options = {}; 78 | options.type = 'mousedown'; 79 | // if the mousedown event is prevented we can't cut content 80 | if(!this.dispatch(element, options)) { 81 | return ''; 82 | } 83 | // if content is selectable, we cut only the selected content 84 | if(utils.isSelectable(element)) { 85 | content = element.value.substr(element.selectionStart, element.selectionEnd-1); 86 | element.value = 87 | (element.selectionStart ? 88 | element.value.substr(0, element.selectionStart) : '') 89 | + (element.selectionEnd ? 90 | element.value.substr(element.selectionEnd) : 91 | ''); 92 | // otherwise we cut the full content 93 | } else { 94 | content = element.value; 95 | element.value = null; 96 | } 97 | // finally firing an input event 98 | utils.dispatch(element, {type: 'input'}); 99 | return content; 100 | }; 101 | 102 | /** 103 | * Paste content like if it were done by a user with his mouse. 104 | * 105 | * @param DOMElement element A DOMElement to dblclick 106 | * @param String content The content to paste 107 | * @return Boolean 108 | */ 109 | this.paste = function paste(element, content) { 110 | // The content of a paste is always a string 111 | if('string' !== typeof content) { 112 | throw Error('Can only paste strings (received '+(typeof content)+').'); 113 | } 114 | if(!utils.canAcceptContent(element, content)) { 115 | throw Error('Unable to paste content in the given element.'); 116 | } 117 | // We move to the element if not over yet 118 | this.moveTo(element); 119 | options = {}; 120 | options.type = 'mousedown'; 121 | // if the mousedown event is prevented we can't paste content 122 | if(!this.dispatch(element, options)) { 123 | return false; 124 | } 125 | // if content is selectable, we paste content in the place of the selected content 126 | if(utils.isSelectable(element)) { 127 | element.value = 128 | (element.selectionStart ? 129 | element.value.substr(0, element.selectionStart) : '') 130 | + content 131 | + (element.selectionEnd ? 132 | element.value.substr(element.selectionEnd) : 133 | ''); 134 | // otherwise we just replace the value 135 | } else { 136 | element.value = content; 137 | } 138 | // finally firing an input event 139 | return utils.dispatch(element, {type: 'input'}); 140 | }; 141 | 142 | /** 143 | * Perform a real mouse double click on the given DOM element. 144 | * 145 | * @param DOMElement element A DOMElement to dblclick 146 | * @param Object options Clic options 147 | * @return Boolean 148 | */ 149 | this.dblclick = function dblclick(element, options) { 150 | var dispatched; 151 | // We move to the element if not over yet 152 | this.moveTo(element); 153 | options = options||{}; 154 | dispatched = this.click(element, options); 155 | if(!(this.click(element, options)&&dispatched)) { 156 | return false; 157 | } 158 | options.type = 'dblclick'; 159 | return this.dispatch(element, options); 160 | }; 161 | 162 | /** 163 | * Perform a real mouse rightclick on the given DOM element. 164 | * 165 | * @param DOMElement element A DOMElement to rightclick 166 | * @param Object options Clic options 167 | * @return Boolean 168 | */ 169 | this.rightclick = function rightclick(element, options) { 170 | options = options || {}; 171 | options.buttons = this.RIGHT_BUTTON; 172 | return this.click(element, options); 173 | }; 174 | 175 | /** 176 | * Perform a real mouse click on the given DOM element. 177 | * 178 | * @param DOMElement element A DOMElement to click 179 | * @param Object options Clic options 180 | * @return Boolean 181 | */ 182 | this.click = function click(element, options) { 183 | var dispatched; 184 | // We move to the element if not over yet 185 | this.moveTo(element); 186 | options = options || {}; 187 | options.type = 'mousedown'; 188 | dispatched = this.dispatch(element, options); 189 | options.type = 'mouseup'; 190 | if(!(this.dispatch(element, options)&&dispatched)) { 191 | return false; 192 | } 193 | options.type = 'click'; 194 | return this.dispatch(element, options); 195 | }; 196 | 197 | /** 198 | * Focus a DOM element with the mouse. 199 | * 200 | * @param DOMElement element A DOMElement to focus 201 | * @param Object options Event options 202 | * @return Boolean 203 | */ 204 | this.focus = function focus(element, options) { 205 | var dispatched; 206 | // We move to the element if not over yet 207 | this.moveTo(element); 208 | options = options || {}; 209 | options.type = 'mousedown'; 210 | dispatched=this.dispatch(element, options); 211 | // Here, maybe find the first parent element having greater bound rect 212 | // and move on it's focusable zone or fail if none available 213 | if(dispatched) { 214 | uiFocus.focus(element); 215 | } 216 | this.moveTo(element.parentNode); 217 | options.type = 'mouseup'; 218 | this.dispatch(element.parentNode, options); 219 | return dispatched; 220 | }; 221 | 222 | /** 223 | * Perform a real mouse move to the given coordinates. 224 | * 225 | * @param int x The x position to go 226 | * @param int y The y position to go 227 | * @param Object options Clic options 228 | * @return Boolean 229 | */ 230 | this.move = function move(x, y, options) { 231 | var curElement = document.elementFromPoint(_x, _y), 232 | targetElement, 233 | oldScrollX = window.scrollX, 234 | oldScrollY = window.scrollY, 235 | dispatched; 236 | // Could move the cursor of %n px and repeat mouseover/out events 237 | // killer feature or overkill ? 238 | options = options || {}; 239 | options.type = 'mouseout'; 240 | dispatched = this.dispatch(curElement, options); 241 | this.scroll(x, y, options); 242 | _x = x + oldScrollX - window.scrollX; 243 | _y = y + oldScrollY - window.scrollY; 244 | if(_x < 0 || _y < 0) { 245 | throw new Error('The mouse pointer coordinates can\'t be negative.'); 246 | } 247 | if(_x >= window.innerWidth || _y >= window.innerHeight) { 248 | throw new Error('The mouse pointer coordinates can\'t be greater than the' 249 | +' viewport size.'); 250 | } 251 | targetElement = document.elementFromPoint(_x, _y); 252 | if(!targetElement) { 253 | throw Error('Couldn\'t perform the move. Coordinnates seems invalid.'); 254 | } 255 | if(curElement===targetElement) { 256 | return false; 257 | } 258 | options.type = 'mouseover'; 259 | options.relatedTarget = curElement; 260 | dispatched = this.dispatch(targetElement, options); 261 | return true; 262 | }; 263 | 264 | /** 265 | * Perform a real mouse move to an element. 266 | * 267 | * @param DOMElement element A DOMElement on wich to move the cursor 268 | * @param Object options Clic options 269 | * @return Boolean 270 | */ 271 | this.moveTo = function moveTo(element, options) { 272 | var c = utils.getElementCenter(element); 273 | // We are giving the related target to avoid calculating it later 274 | options = options || {}; 275 | options.relatedTarget = element; 276 | return this.move(c.x, c.y, options); 277 | }; 278 | 279 | /** 280 | * Perform a scroll with the mouse wheel. 281 | * 282 | * @param int x The x delta to scroll to 283 | * @param int y The y delta to scroll to 284 | * @param Object options Clic options 285 | * @return Boolean 286 | */ 287 | this.scroll = function scroll(x, y, options) { 288 | var dispatched=true, scrollX = 0, scrollY = 0; 289 | options = options || {}; 290 | options.type = ('onwheel' in document ? 'wheel' : 291 | ('onmousewheel' in document ? 'mousewheel' : '') 292 | ); 293 | if(options.type) { 294 | // Moving through the x axis 295 | options.shiftKey = true; 296 | options.wheelDelta = 120; 297 | options.wheelDeltaX = (x < 0 ? 120 : -120); 298 | options.wheelDeltaY = 0; 299 | while(dispatched && (x+scrollX<0 || x+scrollX > window.innerWidth)) { 300 | dispatched=this.dispatch(document.elementFromPoint(_x, _y), options); 301 | if(dispatched) { 302 | scrollX += options.wheelDeltaX; 303 | window.scrollTo(window.scrollX - options.wheelDeltaX, window.scrollY); 304 | } 305 | } 306 | // Then moving through the y axis 307 | options.wheelDelta = 120; 308 | options.wheelDeltaX = 0; 309 | options.wheelDeltaY = (y < 0 ? 120 : -120); 310 | while(dispatched && (y+scrollY<0 || y+scrollY > window.innerHeight)) { 311 | dispatched=this.dispatch(document.elementFromPoint(_x, _y), options); 312 | if(dispatched) { 313 | scrollY += options.wheelDeltaY; 314 | window.scrollTo(window.scrollX, window.scrollY - options.wheelDeltaY); 315 | } 316 | } 317 | } 318 | return dispatched; 319 | }; 320 | 321 | /** 322 | * Perform a scroll with the mouse wheel. 323 | * 324 | * @param DOMElement element A DOMElement on wich to scroll to 325 | * @param Object options Clic options 326 | * @return Boolean 327 | */ 328 | this.scrollTo = function scrollTo(element, options) { 329 | // Getting element center 330 | var c = utils.getElementCenter(element); 331 | // Scroll only if the element is not already in the viewport 332 | if(c.x<0 || c.y<0 || c.x > window.innerWidth || c.y > window.innerHeight) { 333 | return this.scroll(c.x, c.y, options); 334 | } 335 | return false; 336 | }; 337 | 338 | /** 339 | * Dispatches a mouse event to the given DOM element. 340 | * 341 | * @param DOMElement element A DOMElement on wich to dispatch the event 342 | * @param Object options Event options 343 | * @return Boolean 344 | */ 345 | this.dispatch = function dispatch(element, options) { 346 | var event, button, coords; 347 | options = options || {}; 348 | options.type = options.type || 'click'; 349 | if(options.buttons !== options.buttons&this.BUTTONS_MASK) { 350 | throw Error('Bad value for the "buttons" property.'); 351 | } 352 | options.buttons = options.buttons || this.LEFT_BUTTON; 353 | if(options.button) { 354 | throw Error('Please use the "buttons" property.'); 355 | } 356 | button=( options.buttons&this.RIGHT_BUTTON ? 2 : 357 | ( options.buttons&this.MIDDLE_BUTTON? 1 : 0 ) 358 | ); 359 | coords = utils.getPossiblePointerCoords(element); 360 | if(null===coords) { 361 | throw Error('Unable to find a point in the viewport at wich the given' 362 | +' element can receive a mouse event.'); 363 | } 364 | options.canBubble = ('false' === options.canBubble ? false : true); 365 | options.cancelable = ('false' === options.cancelable ? false : true); 366 | options.view = options.view || window; 367 | options.detail = options.detail || 1; 368 | options.altKey = !!options.altKey; 369 | options.ctrlKey = !!options.ctrlKey; 370 | options.shiftKey = !!options.shiftKey; 371 | options.metaKey = !!options.metaKey; 372 | options.relatedTarget = options.relatedTarget||null; 373 | // try to use the constructor (recommended with DOM level 3) 374 | // http://www.w3.org/TR/DOM-Level-3-Events/#new-event-interface-initializers 375 | try { 376 | event = new MouseEvent(options.type, { 377 | bubbles: options.canBubble, 378 | cancelable: options.cancelable, 379 | view: options.view, 380 | // FIXME: find a way to get the right screen coordinates 381 | screenX: coords.x + window.screenLeft, 382 | screenY: coords.y + window.screenTop, 383 | clientX: coords.x, 384 | clientY: coords.y, 385 | altKey: options.altKey, 386 | ctrlKey: options.ctrlKey, 387 | shiftKey: options.shiftKey, 388 | metaKey: options.metaKey, 389 | button: button, 390 | buttons: options.buttons, 391 | relatedTarget: options.relatedTarget 392 | }); 393 | // Chrome seems to not set the buttons property properly 394 | utils.setEventProperty(event, 'buttons', options.buttons); 395 | return element.dispatchEvent(event); 396 | } catch(e) { 397 | // old fashined event intializer 398 | if(document.createEvent) { 399 | event = document.createEvent('MouseEvent'); 400 | event.initMouseEvent(options.type, 401 | options.canBubble, options.cancelable, 402 | options.view, options.detail, 403 | // Screen coordinates (relative to the whole user screen) 404 | // FIXME: find a way to get the right screen coordinates 405 | coords.x + window.screenLeft, coords.y + window.screenTop, 406 | // Client coordinates (relative to the viewport) 407 | coords.x, coords.y, 408 | options.ctrlKey, options.altKey, 409 | options.shiftKey, options.metaKey, 410 | button, options.relatedTarget); 411 | utils.setEventCoords(event, coords.x, coords.y); 412 | utils.setEventProperty(event, 'buttons', options.buttons); 413 | return element.dispatchEvent(event); 414 | // old IE event initializer 415 | } else if(document.createEventObject) { 416 | event = document.createEventObject(); 417 | event.eventType = options.type; 418 | event.button = button; 419 | event.buttons = option.buttons; 420 | return element.fireEvent('on'+options.type, event); 421 | } 422 | } 423 | }; 424 | 425 | } 426 | 427 | module.exports = new Mouse(); 428 | -------------------------------------------------------------------------------- /test/pointers.js: -------------------------------------------------------------------------------- 1 | var pointers = effroi.pointers, 2 | assert = chai.assert; 3 | 4 | // Helper 5 | var elt, evts=[], listeners=[]; 6 | function regEventListener(elt, type, capture, stop, prevent) { 7 | var listener=function(e) { 8 | if(e.type!=type) { 9 | throw 'Event types differs.' 10 | } 11 | evts.push({ 12 | type : e.type, 13 | target : e.target, 14 | currentTarget : e.currentTarget, 15 | clientX : e.clientX, 16 | clientY : e.clientY, 17 | altKey : e.altKey, 18 | ctrlKey : e.ctrlKey, 19 | shiftKey : e.shiftKey, 20 | metaKey : e.metaKey, 21 | button : e.button, 22 | buttons : e.buttons, 23 | charCode : e.charCode, 24 | char : e.char, 25 | pointerType : e.pointerType, 26 | view : e.view, 27 | relatedTarget : e.relatedTarget 28 | }); 29 | if(stop) { 30 | e.stopPropagation(); 31 | } 32 | if(prevent) { 33 | e.preventDefault(); 34 | } 35 | }; 36 | elt.addEventListener(type, listener, capture); 37 | listeners.push({ 38 | elt: elt, 39 | type: type, 40 | listener : listener, 41 | capture : capture 42 | }); 43 | } 44 | 45 | function init(innerHTML) { 46 | elt = document.createElement('div'); 47 | elt.innerHTML = innerHTML || 'foo'; 48 | document.body.appendChild(elt); 49 | } 50 | 51 | function uninit() { 52 | document.body.removeChild(elt); 53 | for(var i=listeners.length-1; i>=0; i--) { 54 | listeners[i].elt.removeEventListener( 55 | listeners[i].type, listeners[i].listener, listeners[i].capture); 56 | } 57 | evts=[]; 58 | elt=null; 59 | } 60 | 61 | // Test tactile only if it's available 62 | if(pointers.isConnected()) { 63 | 64 | describe("Pointing device", function() { 65 | 66 | var prefix = (function () { 67 | var _prefixed = !!window.navigator.msPointerEnabled; 68 | return function (eventType) { 69 | return (_prefixed ? 'MSPointer' + eventType[7].toUpperCase() 70 | + eventType.substring(8) : eventType); 71 | }; 72 | })(); 73 | 74 | describe("pointing the screen", function() { 75 | 76 | before(function() { 77 | init(); 78 | regEventListener(elt, prefix('pointerdown')); 79 | regEventListener(elt, prefix('pointerup')); 80 | regEventListener(elt, 'click'); 81 | regEventListener(document.body, prefix('pointerdown')); 82 | regEventListener(document.body, prefix('pointerup')); 83 | regEventListener(document.body, 'click'); 84 | }); 85 | 86 | after(uninit); 87 | 88 | it("should return true", function() { 89 | assert.equal(pointers.point(elt), true); 90 | }); 91 | 92 | it("should trigger a pointerdown event on the element", function() { 93 | assert.equal(evts[0].type, prefix('pointerdown')); 94 | assert.equal(evts[0].target, elt); 95 | assert.equal(evts[0].currentTarget, elt); 96 | }); 97 | 98 | it("should bubble the pointerdown event on the parent", function() { 99 | assert.equal(evts[1].type, prefix('pointerdown')); 100 | assert.equal(evts[1].target, elt); 101 | assert.equal(evts[1].currentTarget, document.body); 102 | }); 103 | 104 | it("should trigger a pointerup event on the element", function() { 105 | assert.equal(evts[2].type, prefix('pointerup')); 106 | assert.equal(evts[2].target, elt); 107 | assert.equal(evts[2].currentTarget, elt); 108 | }); 109 | 110 | it("should bubble the pointerup event on the parent", function() { 111 | assert.equal(evts[3].type, prefix('pointerup')); 112 | assert.equal(evts[3].target, elt); 113 | assert.equal(evts[3].currentTarget, document.body); 114 | }); 115 | 116 | it("should trigger a click event on the element", function() { 117 | assert.equal(evts[4].type, 'click'); 118 | assert.equal(evts[4].target, elt); 119 | assert.equal(evts[4].currentTarget, elt); 120 | }); 121 | 122 | it("should bubble the click event on the parent", function() { 123 | assert.equal(evts[5].type, 'click'); 124 | assert.equal(evts[5].target, elt); 125 | assert.equal(evts[5].currentTarget, document.body); 126 | }); 127 | 128 | }); 129 | 130 | describe("pointing the screen and stop events", function() { 131 | 132 | before(function() { 133 | init(); 134 | regEventListener(elt, prefix('pointerdown'), false, true); 135 | regEventListener(elt, prefix('pointerup'), false, true); 136 | regEventListener(elt, 'click', false, true); 137 | regEventListener(document.body, prefix('pointerdown')); 138 | regEventListener(document.body, prefix('pointerup')); 139 | regEventListener(document.body, 'click'); 140 | }); 141 | 142 | after(uninit); 143 | 144 | it("should return true", function() { 145 | assert.equal(pointers.point(elt), true); 146 | }); 147 | 148 | it("should trigger a pointerdown event on the element", function() { 149 | assert.equal(evts[0].type, prefix('pointerdown')); 150 | assert.equal(evts[0].target, elt); 151 | assert.equal(evts[0].currentTarget, elt); 152 | }); 153 | 154 | it("should not bubble the pointerdown event on the parent", function() { 155 | assert.notEqual(evts[1].type, prefix('pointerdown')); 156 | }); 157 | 158 | it("should trigger a pointerup event on the element", function() { 159 | assert.equal(evts[1].type, prefix('pointerup')); 160 | assert.equal(evts[1].target, elt); 161 | assert.equal(evts[1].currentTarget, elt); 162 | }); 163 | 164 | it("should not bubble the pointerup event on the parent", function() { 165 | assert.notEqual(evts[2].type, prefix('pointerup')); 166 | }); 167 | 168 | it("should trigger a click event on the element", function() { 169 | assert.equal(evts[2].type, 'click'); 170 | assert.equal(evts[2].target, elt); 171 | assert.equal(evts[2].currentTarget, elt); 172 | }); 173 | 174 | it("should not bubble the click event on the parent", function() { 175 | assert.equal(evts[3], null); 176 | }); 177 | 178 | }); 179 | 180 | describe("pointing the screen and cancelling pointerup", function() { 181 | 182 | before(function() { 183 | init(); 184 | regEventListener(elt, prefix('pointerdown')); 185 | regEventListener(elt, prefix('pointerup'), false, false, true); 186 | regEventListener(elt, 'click'); 187 | regEventListener(document.body, prefix('pointerdown')); 188 | regEventListener(document.body, prefix('pointerup')); 189 | regEventListener(document.body, 'click'); 190 | }); 191 | 192 | after(uninit); 193 | 194 | it("should return false for IE11, true otherwise", function() { 195 | assert.equal(pointers.point(elt), 196 | window.navigator.msPointerEnabled ? true : false); 197 | }); 198 | 199 | it("should trigger a pointerdown event on the element", function() { 200 | assert.equal(evts[0].type, prefix('pointerdown')); 201 | assert.equal(evts[0].target, elt); 202 | assert.equal(evts[0].currentTarget, elt); 203 | }); 204 | 205 | it("should bubble the pointerdown event on the parent", function() { 206 | assert.equal(evts[1].type, prefix('pointerdown')); 207 | assert.equal(evts[1].target, elt); 208 | assert.equal(evts[1].currentTarget, document.body); 209 | }); 210 | 211 | it("should trigger a pointerup event on the element", function() { 212 | assert.equal(evts[2].type, prefix('pointerup')); 213 | assert.equal(evts[2].target, elt); 214 | assert.equal(evts[2].currentTarget, elt); 215 | }); 216 | 217 | it("should bubble the pointerup event on the parent", function() { 218 | assert.equal(evts[3].type, prefix('pointerup')); 219 | assert.equal(evts[3].target, elt); 220 | assert.equal(evts[3].currentTarget, document.body); 221 | }); 222 | 223 | it("should not trigger a click event on the element for IE11", function() { 224 | if(window.navigator.pointerEnabled) { 225 | assert.equal(evts[4], null); 226 | } 227 | }); 228 | 229 | }); 230 | 231 | describe("pointing the screen and cancelling pointerdown", function() { 232 | 233 | before(function() { 234 | init(); 235 | regEventListener(elt, prefix('pointerdown'), false, false, true); 236 | regEventListener(elt, prefix('pointerup')); 237 | regEventListener(elt, 'click'); 238 | regEventListener(document.body, prefix('pointerdown')); 239 | regEventListener(document.body, prefix('pointerup')); 240 | regEventListener(document.body, 'click'); 241 | }); 242 | 243 | after(uninit); 244 | 245 | it("should return false", function() { 246 | assert.equal(pointers.point(elt), 247 | window.navigator.msPointerEnabled ? true : false); 248 | }); 249 | 250 | it("should trigger a pointerdown event on the element", function() { 251 | assert.equal(evts[0].type, prefix('pointerdown')); 252 | assert.equal(evts[0].target, elt); 253 | assert.equal(evts[0].currentTarget, elt); 254 | }); 255 | 256 | it("should bubble the pointerdown event on the parent", function() { 257 | assert.equal(evts[1].type, prefix('pointerdown')); 258 | assert.equal(evts[1].target, elt); 259 | assert.equal(evts[1].currentTarget, document.body); 260 | }); 261 | 262 | it("should trigger a pointerup event on the element", function() { 263 | assert.equal(evts[2].type, prefix('pointerup')); 264 | assert.equal(evts[2].target, elt); 265 | assert.equal(evts[2].currentTarget, elt); 266 | }); 267 | 268 | it("should bubble the pointerup event on the parent", function() { 269 | assert.equal(evts[3].type, prefix('pointerup')); 270 | assert.equal(evts[3].target, elt); 271 | assert.equal(evts[3].currentTarget, document.body); 272 | }); 273 | 274 | it("should not trigger a click event on the element for IE11", function() { 275 | if(window.navigator.pointerEnabled) { 276 | assert.equal(evts[4], null); 277 | } 278 | }); 279 | 280 | }); 281 | 282 | describe("clicking the screen", function() { 283 | 284 | before(function() { 285 | init(); 286 | regEventListener(elt, prefix('pointerdown')); 287 | regEventListener(elt, prefix('pointerup')); 288 | regEventListener(elt, 'click'); 289 | }); 290 | 291 | after(uninit); 292 | 293 | it("should return true", function() { 294 | assert.equal(pointers.click(elt), true); 295 | }); 296 | 297 | if(window.navigator.pointerEnabled) { 298 | it("should set pointerType to 'mouse' on IE11+", function() { 299 | assert.equal(evts[0].pointerType, 'mouse'); 300 | assert.equal(evts[1].pointerType, 'mouse'); 301 | assert.equal(evts[2].pointerType, 'mouse'); 302 | }); 303 | } else { 304 | it("should set pointerType to 4 on IE10", function() { 305 | assert.equal(evts[0].pointerType, 4); 306 | assert.equal(evts[1].pointerType, 4); 307 | assert.equal(evts[2].pointerType, undefined); // IE10 click is a MouseEvent 308 | }); 309 | } 310 | 311 | it("should set button to 0", function() { 312 | assert.equal(evts[0].button, 0); 313 | assert.equal(evts[1].button, 0); 314 | assert.equal(evts[1].button, 0); 315 | }); 316 | 317 | }); 318 | 319 | describe("touching the screen", function() { 320 | 321 | before(function() { 322 | init(); 323 | regEventListener(elt, prefix('pointerdown')); 324 | regEventListener(elt, prefix('pointerup')); 325 | regEventListener(elt, 'click'); 326 | }); 327 | 328 | after(uninit); 329 | 330 | it("should return true", function() { 331 | assert.equal(pointers.touch(elt), true); 332 | }); 333 | 334 | if(window.navigator.pointerEnabled) { 335 | it("should set pointerType to 'touch' on IE11+", function() { 336 | assert.equal(evts[0].pointerType, 'touch'); 337 | assert.equal(evts[1].pointerType, 'touch'); 338 | assert.equal(evts[2].pointerType, 'touch'); 339 | }); 340 | } else { 341 | it("should set pointerType to 2 on IE10", function() { 342 | assert.equal(evts[0].pointerType, 2); 343 | assert.equal(evts[1].pointerType, 2); 344 | assert.equal(evts[2].pointerType, undefined); // IE10 click is a MouseEvent 345 | }); 346 | } 347 | 348 | it("should set button to 0", function() { 349 | assert.equal(evts[0].button, 0); 350 | assert.equal(evts[1].button, 0); 351 | assert.equal(evts[1].button, 0); 352 | }); 353 | 354 | }); 355 | 356 | describe("pointing the screen with a pen", function() { 357 | 358 | before(function() { 359 | init(); 360 | regEventListener(elt, prefix('pointerdown')); 361 | regEventListener(elt, prefix('pointerup')); 362 | regEventListener(elt, 'click'); 363 | }); 364 | 365 | after(uninit); 366 | 367 | it("should return true", function() { 368 | assert.equal(pointers.pen(elt), true); 369 | }); 370 | 371 | if(window.navigator.pointerEnabled) { 372 | it("should set pointerType to 'touch' on IE11+", function() { 373 | assert.equal(evts[0].pointerType, 'pen'); 374 | assert.equal(evts[1].pointerType, 'pen'); 375 | assert.equal(evts[2].pointerType, 'pen'); 376 | }); 377 | } else { 378 | it("should set pointerType to 2 on IE10", function() { 379 | assert.equal(evts[0].pointerType, 3); 380 | assert.equal(evts[1].pointerType, 3); 381 | assert.equal(evts[2].pointerType, undefined); // IE10 click is a MouseEvent 382 | }); 383 | } 384 | 385 | it("should set button to 0", function() { 386 | assert.equal(evts[0].button, 0); 387 | assert.equal(evts[1].button, 0); 388 | assert.equal(evts[1].button, 0); 389 | }); 390 | 391 | }); 392 | 393 | describe("clicking an undisplayed element", function() { 394 | 395 | before(function() { 396 | init('

Text

A link

'); 397 | }); 398 | 399 | after(uninit); 400 | 401 | it("should throw an exception", function() { 402 | assert.throw(function() { 403 | pointers.dispatch(elt.lastChild.firstChild, {type: 'pointerdown'}); 404 | }, Error, 'Unable to find a point in the viewport at wich the' 405 | +' given element can receive a pointer event.'); 406 | }); 407 | 408 | }); 409 | 410 | describe("clicking an hidden element", function() { 411 | 412 | before(function() { 413 | init('

Text

A link

'); 414 | }); 415 | 416 | after(uninit); 417 | 418 | it("should throw an exception", function() { 419 | assert.throw(function() { 420 | pointers.dispatch(elt.lastChild.firstChild, {type: 'pointerdown'}); 421 | }, Error, 'Unable to find a point in the viewport at wich the' 422 | +' given element can receive a pointer event.'); 423 | }); 424 | 425 | }); 426 | 427 | describe("clicking a pointer disabled element", function() { 428 | 429 | before(function() { 430 | init('

Text

A link

'); 431 | }); 432 | 433 | after(uninit); 434 | 435 | it("should throw an exception", function() { 436 | assert.throw(function() { 437 | pointers.dispatch(elt.lastChild.firstChild, {type: 'pointerdown'}); 438 | }, Error, 'Unable to find a point in the viewport at wich the' 439 | +' given element can receive a pointer event.'); 440 | }); 441 | 442 | }); 443 | 444 | describe("clicking an unclickable element", function() { 445 | 446 | before(function() { 447 | init('

Text

A link

'); 448 | }); 449 | 450 | after(uninit); 451 | 452 | it("should throw an exception", function() { 453 | assert.throw(function() { 454 | pointers.dispatch(elt.lastChild.firstChild, {type: 'pointerdown'}); 455 | }, Error, 'Unable to find a point in the viewport at wich the' 456 | +' given element can receive a pointer event.'); 457 | }); 458 | 459 | }); 460 | 461 | }); 462 | 463 | } 464 | -------------------------------------------------------------------------------- /test/tactile.js: -------------------------------------------------------------------------------- 1 | var tactile = effroi.tactile, 2 | assert = chai.assert; 3 | 4 | // Helper 5 | var elt, evts=[], listeners=[]; 6 | function regEventListener(elt, type, capture, stop, prevent) { 7 | var listener=function(e) { 8 | if(e.type!=type) { 9 | throw 'Event types differs.' 10 | } 11 | evts.push({ 12 | type : e.type, 13 | target : e.target, 14 | currentTarget : e.currentTarget, 15 | clientX : e.clientX, 16 | clientY : e.clientY, 17 | altKey : e.altKey, 18 | ctrlKey : e.ctrlKey, 19 | shiftKey : e.shiftKey, 20 | metaKey : e.metaKey, 21 | button : e.button, 22 | buttons : e.buttons, 23 | charCode : e.charCode, 24 | char : e.char, 25 | pointerType : e.pointerType, 26 | view : e.view, 27 | relatedTarget : e.relatedTarget 28 | }); 29 | if(stop) { 30 | e.stopPropagation(); 31 | } 32 | if(prevent) { 33 | e.preventDefault(); 34 | } 35 | }; 36 | elt.addEventListener(type, listener, capture); 37 | listeners.push({ 38 | elt: elt, 39 | type: type, 40 | listener : listener, 41 | capture : capture 42 | }); 43 | } 44 | 45 | function init(innerHTML) { 46 | elt = document.createElement('div'); 47 | elt.innerHTML = innerHTML || 'foo'; 48 | document.body.appendChild(elt); 49 | } 50 | 51 | function uninit() { 52 | document.body.removeChild(elt); 53 | for(var i=listeners.length-1; i>=0; i--) { 54 | listeners[i].elt.removeEventListener( 55 | listeners[i].type, listeners[i].listener, listeners[i].capture); 56 | } 57 | evts=[]; 58 | elt=null; 59 | } 60 | 61 | // Test tactile only if it's available 62 | if(tactile.isConnected()) { 63 | 64 | describe("A tactile device", function() { 65 | 66 | beforeEach(function() { 67 | // Would like to put init here but seems impossible 68 | }); 69 | 70 | afterEach(function() { 71 | // Would like to put uninit here but seems impossible 72 | }); 73 | 74 | describe("touching the screen", function() { 75 | 76 | before(function() { 77 | init(); 78 | regEventListener(elt, 'touchstart'); 79 | regEventListener(elt, 'touchend'); 80 | regEventListener(elt, 'click'); 81 | regEventListener(document.body, 'touchstart'); 82 | regEventListener(document.body, 'touchend'); 83 | regEventListener(document.body, 'click'); 84 | }); 85 | 86 | after(uninit); 87 | 88 | it("should return true", function() { 89 | assert.equal(tactile.touch(elt),true); 90 | }); 91 | 92 | if(document.elementFromPoint&&document.body.getBoundingClientRect) { 93 | it("should return right coords", function() { 94 | assert.equal( 95 | document.elementFromPoint(evts[0].clientX, evts[0].clientY), 96 | elt 97 | ); 98 | }); 99 | } 100 | 101 | it("should trigger a touchstart event on the element", function() { 102 | assert.equal(evts[0].type, 'touchstart'); 103 | assert.equal(evts[0].target, elt); 104 | assert.equal(evts[0].currentTarget, elt); 105 | }); 106 | 107 | it("should bubble the touchstart event on the parent", function() { 108 | assert.equal(evts[1].type, 'touchstart'); 109 | assert.equal(evts[1].target, elt); 110 | assert.equal(evts[1].currentTarget, document.body); 111 | }); 112 | 113 | it("should trigger a touchend event on the element", function() { 114 | assert.equal(evts[2].type, 'touchend'); 115 | assert.equal(evts[2].target, elt); 116 | assert.equal(evts[2].currentTarget, elt); 117 | }); 118 | 119 | it("should bubble the touchend event on the parent", function() { 120 | assert.equal(evts[3].type, 'touchend'); 121 | assert.equal(evts[3].target, elt); 122 | assert.equal(evts[3].currentTarget, document.body); 123 | }); 124 | 125 | it("should trigger a click event on the element", function() { 126 | assert.equal(evts[4].type, 'click'); 127 | assert.equal(evts[4].target, elt); 128 | assert.equal(evts[4].currentTarget, elt); 129 | }); 130 | 131 | it("should bubble the click event on the parent", function() { 132 | assert.equal(evts[5].type, 'click'); 133 | assert.equal(evts[5].target, elt); 134 | assert.equal(evts[5].currentTarget, document.body); 135 | }); 136 | 137 | }); 138 | 139 | describe("touching the screen and stop events", function() { 140 | 141 | before(function() { 142 | init(); 143 | regEventListener(elt, 'touchstart', false, true); 144 | regEventListener(elt, 'touchend', false, true); 145 | regEventListener(elt, 'click', false, true); 146 | regEventListener(document.body, 'touchstart'); 147 | regEventListener(document.body, 'touchend'); 148 | regEventListener(document.body, 'click'); 149 | }); 150 | 151 | after(uninit); 152 | 153 | it("should return true", function() { 154 | assert.equal(tactile.touch(elt),true); 155 | }); 156 | 157 | it("should trigger a touchstart event on the element", function() { 158 | assert.equal(evts[0].type, 'touchstart'); 159 | assert.equal(evts[0].target, elt); 160 | assert.equal(evts[0].currentTarget, elt); 161 | }); 162 | 163 | it("should not bubble the touchstart event on the parent", function() { 164 | assert.notEqual(evts[1].type, 'touchstart'); 165 | }); 166 | 167 | it("should trigger a touchend event on the element", function() { 168 | assert.equal(evts[1].type, 'touchend'); 169 | assert.equal(evts[1].target, elt); 170 | assert.equal(evts[1].currentTarget, elt); 171 | }); 172 | 173 | it("should not bubble the touchend event on the parent", function() { 174 | assert.notEqual(evts[2].type, 'touchend'); 175 | }); 176 | 177 | it("should trigger a click event on the element", function() { 178 | assert.equal(evts[2].type, 'click'); 179 | assert.equal(evts[2].target, elt); 180 | assert.equal(evts[2].currentTarget, elt); 181 | }); 182 | 183 | it("should not bubble the click event on the parent", function() { 184 | assert.equal(evts[3], null); 185 | }); 186 | 187 | }); 188 | 189 | describe("touching the screen while the touchend prevented", function() { 190 | 191 | before(function() { 192 | init(); 193 | regEventListener(elt, 'touchstart'); 194 | regEventListener(elt, 'touchend', false, false, true); 195 | regEventListener(elt, 'click'); 196 | regEventListener(document.body, 'touchstart'); 197 | regEventListener(document.body, 'touchend'); 198 | regEventListener(document.body, 'click'); 199 | }); 200 | 201 | after(uninit); 202 | 203 | it("should return false", function() { 204 | assert.equal(tactile.touch(elt),false); 205 | }); 206 | 207 | it("should trigger a touchstart event on the element", function() { 208 | assert.equal(evts[0].type, 'touchstart'); 209 | assert.equal(evts[0].target, elt); 210 | assert.equal(evts[0].currentTarget, elt); 211 | }); 212 | 213 | it("should bubble the touchstart event on the parent", function() { 214 | assert.equal(evts[1].type, 'touchstart'); 215 | assert.equal(evts[1].target, elt); 216 | assert.equal(evts[1].currentTarget, document.body); 217 | }); 218 | 219 | it("should trigger a touchend event on the element", function() { 220 | assert.equal(evts[2].type, 'touchend'); 221 | assert.equal(evts[2].target, elt); 222 | assert.equal(evts[2].currentTarget, elt); 223 | }); 224 | 225 | it("should bubble the touchend event on the parent", function() { 226 | assert.equal(evts[3].type, 'touchend'); 227 | assert.equal(evts[3].target, elt); 228 | assert.equal(evts[3].currentTarget, document.body); 229 | }); 230 | 231 | it("should not trigger a click event on the element", function() { 232 | assert.equal(evts[4], null); 233 | }); 234 | 235 | }); 236 | 237 | describe("touching the screen while the touchstart prevented", function() { 238 | 239 | before(function() { 240 | init(); 241 | regEventListener(elt, 'touchstart', false, false, true); 242 | regEventListener(elt, 'touchend'); 243 | regEventListener(elt, 'click'); 244 | regEventListener(document.body, 'touchstart'); 245 | regEventListener(document.body, 'touchend'); 246 | regEventListener(document.body, 'click'); 247 | }); 248 | 249 | after(uninit); 250 | 251 | it("should return false", function() { 252 | assert.equal(tactile.touch(elt),false); 253 | }); 254 | 255 | it("should trigger a touchstart event on the element", function() { 256 | assert.equal(evts[0].type, 'touchstart'); 257 | assert.equal(evts[0].target, elt); 258 | assert.equal(evts[0].currentTarget, elt); 259 | }); 260 | 261 | it("should bubble the touchstart event on the parent", function() { 262 | assert.equal(evts[1].type, 'touchstart'); 263 | assert.equal(evts[1].target, elt); 264 | assert.equal(evts[1].currentTarget, document.body); 265 | }); 266 | 267 | it("should trigger a touchend event on the element", function() { 268 | assert.equal(evts[2].type, 'touchend'); 269 | assert.equal(evts[2].target, elt); 270 | assert.equal(evts[2].currentTarget, elt); 271 | }); 272 | 273 | it("should bubble the touchend event on the parent", function() { 274 | assert.equal(evts[3].type, 'touchend'); 275 | assert.equal(evts[3].target, elt); 276 | assert.equal(evts[3].currentTarget, document.body); 277 | }); 278 | 279 | it("should not trigger a click event on the element", function() { 280 | assert.equal(evts[4], null); 281 | }); 282 | 283 | }); 284 | 285 | 286 | 287 | describe("touching the screen + pushing the ctrlKey", function() { 288 | 289 | before(function() { 290 | init(); 291 | regEventListener(elt, 'touchstart'); 292 | regEventListener(elt, 'touchend'); 293 | regEventListener(elt, 'click'); 294 | regEventListener(document.body, 'touchstart'); 295 | regEventListener(document.body, 'touchend'); 296 | regEventListener(document.body, 'click'); 297 | }); 298 | 299 | after(uninit); 300 | 301 | it("should return true", function() { 302 | assert.equal(tactile.touch(elt),true); 303 | }); 304 | 305 | it("should trigger a touchstart event on the element", function() { 306 | assert.equal(evts[0].type, 'touchstart'); 307 | assert.equal(evts[0].target, elt); 308 | assert.equal(evts[0].currentTarget, elt); 309 | }); 310 | 311 | it("should bubble the touchstart event on the parent", function() { 312 | assert.equal(evts[1].type, 'touchstart'); 313 | assert.equal(evts[1].target, elt); 314 | assert.equal(evts[1].currentTarget, document.body); 315 | }); 316 | 317 | it("should trigger a touchend event on the element", function() { 318 | assert.equal(evts[2].type, 'touchend'); 319 | assert.equal(evts[2].target, elt); 320 | assert.equal(evts[2].currentTarget, elt); 321 | }); 322 | 323 | it("should bubble the touchend event on the parent", function() { 324 | assert.equal(evts[3].type, 'touchend'); 325 | assert.equal(evts[3].target, elt); 326 | assert.equal(evts[3].currentTarget, document.body); 327 | }); 328 | 329 | it("should trigger a click event on the element", function() { 330 | assert.equal(evts[4].type, 'click'); 331 | assert.equal(evts[4].target, elt); 332 | assert.equal(evts[4].currentTarget, elt); 333 | }); 334 | 335 | it("should bubble the click event on the parent", function() { 336 | assert.equal(evts[5].type, 'click'); 337 | assert.equal(evts[5].target, elt); 338 | assert.equal(evts[5].currentTarget, document.body); 339 | }); 340 | 341 | }); 342 | 343 | describe("scrolling to an element", function() { 344 | 345 | before(function() { 346 | init('

Block 1

' 347 | +'

Block 2

' 348 | +'

Block 3

'); 349 | regEventListener(elt, 'touchstart'); 350 | regEventListener(elt, 'touchmove'); 351 | regEventListener(elt, 'touchend'); 352 | }); 353 | 354 | after(uninit); 355 | 356 | it("should return true if scrolled, false otherwise", function() { 357 | assert.equal(tactile.scrollTo(elt), 358 | (25048>window.innerHeight ? true : false)); 359 | }); 360 | 361 | it("should trigger a touchstart event", function() { 362 | if((25048>window.innerHeight)) { 363 | assert.equal(evts[0].type, 'touchstart'); 364 | } else { 365 | assert.equal(evts[0], null); 366 | } 367 | }); 368 | 369 | it("should trigger a touchend event", function() { 370 | if((25048>window.innerHeight)) { 371 | assert.equal(evts[0].type, 'touchstart'); 372 | } 373 | }); 374 | 375 | }); 376 | 377 | describe("touching an undisplayed element", function() { 378 | 379 | before(function() { 380 | init('

Text

A link

'); 381 | }); 382 | 383 | after(uninit); 384 | 385 | it("should throw an exception", function() { 386 | assert.throw(function() { 387 | tactile.dispatch(elt.lastChild.firstChild, {type: 'touchstart'}); 388 | }, Error, 'Unable to find a point in the viewport at wich the' 389 | +' given element can receive a touch event.'); 390 | }); 391 | 392 | }); 393 | 394 | describe("touching an hidden element", function() { 395 | 396 | before(function() { 397 | init('

Text

A link

'); 398 | }); 399 | 400 | after(uninit); 401 | 402 | it("should throw an exception", function() { 403 | assert.throw(function() { 404 | tactile.dispatch(elt.lastChild.firstChild, {type: 'touchstart'}); 405 | }, Error, 'Unable to find a point in the viewport at wich the' 406 | +' given element can receive a touch event.'); 407 | }); 408 | 409 | }); 410 | 411 | describe("touching a pointer disabled element", function() { 412 | 413 | before(function() { 414 | init('

Text

A link

'); 415 | }); 416 | 417 | after(uninit); 418 | 419 | it("should throw an exception", function() { 420 | assert.throw(function() { 421 | tactile.dispatch(elt.lastChild.firstChild, {type: 'touchstart'}); 422 | }, Error, 'Unable to find a point in the viewport at wich the' 423 | +' given element can receive a touch event.'); 424 | }); 425 | 426 | }); 427 | 428 | describe("touching an unclickable element", function() { 429 | 430 | before(function() { 431 | init('

Text

A link

'); 432 | }); 433 | 434 | after(uninit); 435 | 436 | it("should throw an exception", function() { 437 | assert.throw(function() { 438 | tactile.dispatch(elt.lastChild.firstChild, {type: 'touchstart'}); 439 | }, Error, 'Unable to find a point in the viewport at wich the' 440 | +' given element can receive a touch event.'); 441 | }); 442 | 443 | }); 444 | 445 | }); 446 | 447 | } 448 | -------------------------------------------------------------------------------- /test/keyboard.js: -------------------------------------------------------------------------------- 1 | var keyboard = effroi.keyboard, 2 | assert = chai.assert; 3 | 4 | // Helper 5 | var elt, evts=[], listeners=[]; 6 | function regEventListener(elt, type, capture, stop, prevent) { 7 | var listener=function(e) { 8 | if(e.type!=type) { 9 | throw 'Event types differs.' 10 | } 11 | evts.push({ 12 | type : e.type, 13 | target : e.target, 14 | currentTarget : e.currentTarget, 15 | clientX : e.clientX, 16 | clientY : e.clientY, 17 | altKey : e.altKey, 18 | ctrlKey : e.ctrlKey, 19 | shiftKey : e.shiftKey, 20 | metaKey : e.metaKey, 21 | button : e.button, 22 | buttons : e.buttons, 23 | pointerType : e.pointerType, 24 | charCode : e.charCode, 25 | char : e.char, 26 | view : e.view, 27 | relatedTarget : e.relatedTarget 28 | }); 29 | if(stop) { 30 | e.stopPropagation(); 31 | } 32 | if(prevent) { 33 | e.preventDefault(); 34 | } 35 | }; 36 | elt.addEventListener(type, listener, capture); 37 | listeners.push({ 38 | elt: elt, 39 | type: type, 40 | listener : listener, 41 | capture : capture 42 | }); 43 | } 44 | 45 | function init(innerHTML) { 46 | elt = document.createElement('div'); 47 | elt.innerHTML = innerHTML || 'foo'; 48 | document.body.appendChild(elt); 49 | } 50 | 51 | function uninit() { 52 | document.body.removeChild(elt); 53 | for(var i=listeners.length-1; i>=0; i--) { 54 | listeners[i].elt.removeEventListener( 55 | listeners[i].type, listeners[i].listener, listeners[i].capture); 56 | } 57 | evts=[]; 58 | elt=null; 59 | } 60 | 61 | describe("Keyboard device", function() { 62 | 63 | describe("hit a key when on an element", function() { 64 | 65 | before(function() { 66 | init(); 67 | regEventListener(document.activeElement, 'keydown'); 68 | regEventListener(document.activeElement, 'keypress'); 69 | regEventListener(document.activeElement, 'keyup'); 70 | 71 | }); 72 | 73 | after(uninit); 74 | 75 | it("should return true", function() { 76 | assert.equal(keyboard.hit('a'), true); 77 | }); 78 | 79 | it("should set the char and charcode property", function() { 80 | assert.equal(evts[0].char, 'a'); 81 | // Seems impossible to change charCode prop with phantom 82 | if(!navigator.userAgent.match(/phantom/i)) { 83 | assert.equal(evts[0].charCode, 'a'.charCodeAt(0)); 84 | } 85 | }); 86 | 87 | it("should set the view property to window", function() { 88 | assert.equal(evts[0].view, window); 89 | }); 90 | 91 | it("should set the special key property to false", function() { 92 | assert.equal(evts[0].ctrlKey, false); 93 | assert.equal(evts[0].altKey, false); 94 | assert.equal(evts[0].shiftKey, false); 95 | assert.equal(evts[0].metaKey, false); 96 | }); 97 | 98 | it("should trigger a keydown event on the activeElement", function() { 99 | assert.equal(evts[0].type, 'keydown'); 100 | assert.equal(evts[0].target, document.activeElement); 101 | }); 102 | 103 | it("should trigger a keypress event on the element", function() { 104 | assert.equal(evts[1].type, 'keypress'); 105 | assert.equal(evts[1].target, document.activeElement); 106 | }); 107 | 108 | it("should trigger a keyup event on the element", function() { 109 | assert.equal(evts[2].type, 'keyup'); 110 | assert.equal(evts[2].target, document.activeElement); 111 | }); 112 | 113 | }); 114 | 115 | describe("hit a key when on an input element", function() { 116 | 117 | before(function() { 118 | init('

'); 119 | elt.firstChild.firstChild.lastChild.focus(); 120 | regEventListener(elt.firstChild.firstChild.lastChild, 'keydown'); 121 | regEventListener(elt.firstChild.firstChild.lastChild, 'keypress'); 122 | regEventListener(elt.firstChild.firstChild.lastChild, 'keyup'); 123 | regEventListener(document.body, 'keydown'); 124 | regEventListener(document.body, 'keypress'); 125 | regEventListener(document.body, 'keyup'); 126 | 127 | }); 128 | 129 | after(uninit); 130 | 131 | it("should return true", function() { 132 | assert.equal(keyboard.hit('a'), true); 133 | }); 134 | 135 | it("should set the char property correctly", function() { 136 | assert.equal(evts[0].char, 'a'); 137 | }); 138 | 139 | it("should set the view property to window", function() { 140 | assert.equal(evts[0].view, window); 141 | }); 142 | 143 | it("should change its value", function() { 144 | assert.equal(elt.firstChild.firstChild.lastChild.value, 'boooooooba'); 145 | }); 146 | 147 | it("should set the special key property to false", function() { 148 | assert.equal(evts[0].ctrlKey, false); 149 | assert.equal(evts[0].altKey, false); 150 | assert.equal(evts[0].shiftKey, false); 151 | assert.equal(evts[0].metaKey, false); 152 | }); 153 | 154 | it("should trigger a keydown event on the activeElement", function() { 155 | assert.equal(evts[0].type, 'keydown'); 156 | assert.equal(evts[0].target, elt.firstChild.firstChild.lastChild); 157 | assert.equal(evts[0].currentTarget, elt.firstChild.firstChild.lastChild); 158 | }); 159 | 160 | it("should bubble the keydown event on the parent", function() { 161 | assert.equal(evts[1].type, 'keydown'); 162 | assert.equal(evts[1].target, elt.firstChild.firstChild.lastChild); 163 | assert.equal(evts[1].currentTarget, document.body); 164 | }); 165 | 166 | it("should trigger a keypress event on the element", function() { 167 | assert.equal(evts[2].type, 'keypress'); 168 | assert.equal(evts[2].target, elt.firstChild.firstChild.lastChild); 169 | assert.equal(evts[2].currentTarget, elt.firstChild.firstChild.lastChild); 170 | }); 171 | 172 | it("should bubble the keypress event on the parent", function() { 173 | assert.equal(evts[3].type, 'keypress'); 174 | assert.equal(evts[3].target, elt.firstChild.firstChild.lastChild); 175 | assert.equal(evts[3].currentTarget, document.body); 176 | }); 177 | 178 | it("should trigger a keyup event on the element", function() { 179 | assert.equal(evts[4].type, 'keyup'); 180 | assert.equal(evts[4].target, elt.firstChild.firstChild.lastChild); 181 | assert.equal(evts[4].currentTarget, elt.firstChild.firstChild.lastChild); 182 | }); 183 | 184 | it("should bubble the keyup event on the parent", function() { 185 | assert.equal(evts[5].type, 'keyup'); 186 | assert.equal(evts[5].target, elt.firstChild.firstChild.lastChild); 187 | assert.equal(evts[5].currentTarget, document.body); 188 | }); 189 | 190 | }); 191 | 192 | describe("combining ctrl + c", function() { 193 | 194 | before(function() { 195 | init(); 196 | regEventListener(document.body, 'keydown'); 197 | regEventListener(document.body, 'keypress'); 198 | regEventListener(document.body, 'keyup'); 199 | 200 | }); 201 | 202 | after(uninit); 203 | 204 | it("should return true", function() { 205 | assert.equal(keyboard.combine(keyboard.CTRL, 'c'), true); 206 | }); 207 | 208 | it("should set an empty char property for the CTRL keydown event", function() { 209 | assert.equal(evts[0].type, 'keydown'); 210 | assert.equal(evts[0].char, ''); 211 | }); 212 | 213 | it("should set the ctrlKey property for the CTRL keydown event", function() { 214 | (!/phantom/i.test(navigator.userAgent)) && 215 | assert.equal(evts[0].ctrlKey, true); 216 | }); 217 | 218 | it("should set an empty char property for the c keydown event", function() { 219 | assert.equal(evts[1].type, 'keydown'); 220 | assert.equal(evts[1].char, 'c'); 221 | }); 222 | 223 | it("should set the ctrlKey property for the c keydown event", function() { 224 | (!/phantom/i.test(navigator.userAgent)) && 225 | assert.equal(evts[1].ctrlKey, true); 226 | }); 227 | 228 | it("should set an empty char property for the CTRL keyup event", function() { 229 | assert.equal(evts[2].type, 'keyup'); 230 | assert.equal(evts[2].char, ''); 231 | }); 232 | 233 | it("should set the ctrlKey property for the CTRL keyup event", function() { 234 | (!/phantom/i.test(navigator.userAgent)) && 235 | assert.equal(evts[2].ctrlKey, false); 236 | }); 237 | 238 | it("should set the char property to 'c' for the c keyup event", function() { 239 | assert.equal(evts[3].type, 'keyup'); 240 | assert.equal(evts[3].char, 'c'); 241 | }) 242 | 243 | it("should set the ctrlKey property for the c keyup event", function() { 244 | (!/phantom/i.test(navigator.userAgent)) && 245 | assert.equal(evts[3].ctrlKey, false); 246 | }); 247 | 248 | }); 249 | 250 | describe("pasting inside an input[type=text] element where some text is selected", function() { 251 | 252 | before(function() { 253 | init('

'); 254 | elt.firstChild.firstChild.lastChild.focus(); 255 | elt.firstChild.firstChild.lastChild.selectionStart=1; 256 | elt.firstChild.firstChild.lastChild.selectionEnd=8; 257 | }); 258 | 259 | after(uninit); 260 | 261 | it("should return true", function() { 262 | assert.equal(keyboard.paste('00'), true); 263 | }); 264 | 265 | it("should change it's value", function() { 266 | assert.equal(elt.firstChild.firstChild.lastChild.value,'b00b'); 267 | }); 268 | 269 | }); 270 | 271 | describe("pasting inside an input[type=text] element where some text is selected and the keydown event prevented", function() { 272 | 273 | before(function() { 274 | init('

'); 275 | elt.firstChild.firstChild.lastChild.focus(); 276 | elt.firstChild.firstChild.lastChild.selectionStart=1; 277 | elt.firstChild.firstChild.lastChild.selectionEnd=8; 278 | regEventListener(elt.firstChild.firstChild.lastChild, 279 | 'keydown', false, false, true); 280 | }); 281 | 282 | after(uninit); 283 | 284 | it("should return false", function() { 285 | assert.equal( 286 | keyboard.paste('00'), 287 | false); 288 | }); 289 | 290 | it("should not change it's value", function() { 291 | assert.equal(elt.firstChild.firstChild.lastChild.value,'booooooob'); 292 | }); 293 | 294 | }); 295 | 296 | describe("cutting the selected text inside an input[type=text]", function() { 297 | 298 | before(function() { 299 | init('

'); 300 | elt.firstChild.firstChild.lastChild.focus(); 301 | elt.firstChild.firstChild.lastChild.selectionStart=1; 302 | elt.firstChild.firstChild.lastChild.selectionEnd=8; 303 | }); 304 | 305 | after(uninit); 306 | 307 | it("should return the cutted content", function() { 308 | assert.equal(keyboard.cut(),'ooooooo'); 309 | }); 310 | 311 | it("should change it's value", function() { 312 | assert.equal(elt.firstChild.firstChild.lastChild.value,'bb'); 313 | }); 314 | 315 | }); 316 | 317 | describe("cutting the selected text inside an input[type=text] with a keydown event prevented", function() { 318 | 319 | before(function() { 320 | init('

'); 321 | elt.firstChild.firstChild.lastChild.focus(); 322 | elt.firstChild.firstChild.lastChild.selectionStart=1; 323 | elt.firstChild.firstChild.lastChild.selectionEnd=8; 324 | regEventListener(elt.firstChild.firstChild.lastChild, 325 | 'keydown', false, false, true); 326 | }); 327 | 328 | after(uninit); 329 | 330 | it("should return no content", function() { 331 | assert.equal(keyboard.cut(),''); 332 | }); 333 | 334 | it("should not change it's value", function() { 335 | assert.equal(elt.firstChild.firstChild.lastChild.value,'booooooob'); 336 | }); 337 | 338 | }); 339 | 340 | describe("focusing elements with the keyboard", function() { 341 | 342 | var previousActiveElement; 343 | 344 | before(function() { 345 | init( 346 | '

' 347 | + '' 348 | + '' 349 | + '