├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── bower.json ├── package.json ├── tests ├── manual │ ├── events.html │ ├── googlemaps.html │ ├── hammer.html │ ├── img.html │ ├── leaflet.html │ └── modernizr.html └── web-platform-tests │ ├── resources │ ├── testharness.css │ ├── testharness.js │ └── testharnessreport.js │ └── touch-events │ ├── create-touch-touchlist.html │ ├── multi-touch-interactions-manual.html │ ├── multi-touch-interactions.js │ ├── multi-touch-interfaces-manual.html │ └── single-touch-manual.html └── touch-emulator.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Deployed apps should consider commenting this line out: 24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | node_modules 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Hammer.js 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. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | server: 2 | python -m SimpleHTTPServer 3 | 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Touch Emulator 2 | ======== 3 | 4 | Emulate multi-touch input on your desktop. Triggers touch events as 5 | [specified by W3C](http://www.w3.org/TR/touch-events). Press the `shift` key to pinch and rotate! 6 | 7 | - [Example with Hammer.js](http://rawgit.com/hammerjs/touchemulator/master/tests/manual/hammer.html) 8 | - [Example with Leaflet Maps](http://rawgit.com/hammerjs/touchemulator/master/tests/manual/leaflet.html) 9 | - [View webpage](http://hammerjs.github.io/touch-emulator) 10 | 11 | ## Install 12 | Download the script from this repo, via Bower: 13 | 14 | ```bash 15 | bower install hammer-touchemulator 16 | ``` 17 | 18 | or NPM: 19 | 20 | ```bash 21 | npm install hammer-touchemulator 22 | ``` 23 | 24 | ## How to use 25 | Include the javascript file, and call the `Emulator()` function before any other libraries that do something with the 26 | touch input. It will set some fake properties to spoof the touch detection of some libraries, and triggers `touchstart`, `touchmove` and `touchend` events on the mouse target. 27 | 28 | ````html 29 | 30 | 31 | ```` 32 | 33 | ````js 34 | function log(ev) { 35 | console.log(ev); 36 | } 37 | 38 | document.body.addEventListener('touchstart', log, false); 39 | document.body.addEventListener('touchmove', log, false); 40 | document.body.addEventListener('touchend', log, false); 41 | ```` 42 | 43 | Also, the script includes polyfills for `document.createTouch` and `document.createTouchList`. 44 | 45 | ## How it works 46 | It listens to the `mousedown`, `mousemove` and `mouseup` events, and translates them to touch events. If the mouseevent 47 | has the `shiftKey` property to `true`, it enables multi-touch. 48 | 49 | The script also prevents the following mouse events on the page: 50 | `mousedown`, `mouseenter`, `mouseleave`, `mousemove`, `mouseout`, `mouseover` and `mouseup`. 51 | 52 | ## Web platform tests 53 | The script has been tested with the [w3c web platform tests](/tests/web-platform-tests/touch-events) and passes all tests, except these; 54 | - *assert_true: event is a TouchEvent event expected true got false* 55 | - We trigger an event of the type `Event` 56 | - *assert_equals: touch list is of type TouchList expected "[object TouchList]" but got "[object Array]"* 57 | - *assert_equals: touch is of type Touch expected "[object Touch]" but got "[object Object]"* 58 | 59 | ## Bookmarklet 60 | ````js 61 | javascript:!function(a){var b=a.createElement("script");b.onload=function(){TouchEmulator()},b.src="//cdn.rawgit.com/hammerjs/touchemulator/0.0.2/touch-emulator.js",a.body.appendChild(b)}(document); 62 | ```` 63 | 64 | ## Options 65 | #### TouchEmulator.template = Function(touch) 66 | Change the css properties of the rendered touches. 67 | 68 | #### TouchEmulator.multiTouchOffset = 75 69 | The distance between the two touch points when entering the *multi-touch zone*. 70 | 71 | #### TouchEmulator.ignoreTags = [...] 72 | The names of HTML tags that shouldn't swallow mouse events (default: `['TEXTAREA', 'INPUT']`). 73 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TouchEmulator", 3 | "version": "0.0.2", 4 | "homepage": "https://github.com/hammerjs/Emulator", 5 | "authors": [ 6 | "Jorik Tangelder " 7 | ], 8 | "description": "Emulate touch input", 9 | "main": "touch-emulator.js", 10 | "keywords": [ 11 | "touch", 12 | "hammerjs", 13 | "input", 14 | "emulator", 15 | "simulator" 16 | ], 17 | "license": "MIT", 18 | "ignore": [ 19 | "**/.*", 20 | "node_modules", 21 | "bower_components", 22 | "test", 23 | "tests" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hammer-touchemulator", 3 | "version": "0.0.2", 4 | "description": "Emulate touch input on your desktop.", 5 | "main": "touch-emulator.js", 6 | "directories": { 7 | "test": "tests" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/hammerjs/touchemulator" 15 | }, 16 | "keywords": [ 17 | "touch", 18 | "hammerjs", 19 | "input", 20 | "emulator", 21 | "simulator" 22 | ], 23 | "author": "Jorik Tangelder ", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/hammerjs/touchemulator/issues" 27 | }, 28 | "homepage": "https://github.com/hammerjs/touchemulator" 29 | } 30 | -------------------------------------------------------------------------------- /tests/manual/events.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | Check the console. No mouseevents should be fired, only touchevents. Click events are allowed. 14 |
15 | 16 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/manual/googlemaps.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |

To test it on the Google Maps view, you should open your 10 | 11 | Inspector and emulate a touch-device (by spoofing the user agent)..

12 | 13 |

This is because the userAgent can't be overwritten and Google uses this to identify if there's any touch 14 | support.

15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/manual/hammer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Hammer.js 7 | 8 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 |
39 | 40 | 41 | 42 | 207 | 208 | 209 | -------------------------------------------------------------------------------- /tests/manual/img.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | no dragging of images;
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/manual/leaflet.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /tests/manual/modernizr.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

Can we trick Modernizr?

9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/web-platform-tests/resources/testharness.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family:DejaVu Sans, Bitstream Vera Sans, Arial, Sans; 3 | } 4 | 5 | #log .warning, 6 | #log .warning a { 7 | color: black; 8 | background: yellow; 9 | } 10 | 11 | #log .error, 12 | #log .error a { 13 | color: white; 14 | background: red; 15 | } 16 | 17 | #log pre { 18 | border: 1px solid black; 19 | padding: 1em; 20 | } 21 | 22 | section#summary { 23 | margin-bottom:1em; 24 | } 25 | 26 | table#results { 27 | border-collapse:collapse; 28 | table-layout:fixed; 29 | width:100%; 30 | } 31 | 32 | table#results th:first-child, 33 | table#results td:first-child { 34 | width:4em; 35 | } 36 | 37 | table#results th:last-child, 38 | table#results td:last-child { 39 | width:50%; 40 | } 41 | 42 | table#results.assertions th:last-child, 43 | table#results.assertions td:last-child { 44 | width:35%; 45 | } 46 | 47 | table#results th { 48 | padding:0; 49 | padding-bottom:0.5em; 50 | border-bottom:medium solid black; 51 | } 52 | 53 | table#results td { 54 | padding:1em; 55 | padding-bottom:0.5em; 56 | border-bottom:thin solid black; 57 | } 58 | 59 | tr.pass > td:first-child { 60 | color:green; 61 | } 62 | 63 | tr.fail > td:first-child { 64 | color:red; 65 | } 66 | 67 | tr.timeout > td:first-child { 68 | color:red; 69 | } 70 | 71 | tr.notrun > td:first-child { 72 | color:blue; 73 | } 74 | 75 | .pass > td:first-child, .fail > td:first-child, .timeout > td:first-child, .notrun > td:first-child { 76 | font-variant:small-caps; 77 | } 78 | 79 | table#results span { 80 | display:block; 81 | } 82 | 83 | table#results span.expected { 84 | font-family:DejaVu Sans Mono, Bitstream Vera Sans Mono, Monospace; 85 | white-space:pre; 86 | } 87 | 88 | table#results span.actual { 89 | font-family:DejaVu Sans Mono, Bitstream Vera Sans Mono, Monospace; 90 | white-space:pre; 91 | } 92 | 93 | span.ok { 94 | color:green; 95 | } 96 | 97 | tr.error { 98 | color:red; 99 | } 100 | 101 | span.timeout { 102 | color:red; 103 | } 104 | 105 | span.ok, span.timeout, span.error { 106 | font-variant:small-caps; 107 | } -------------------------------------------------------------------------------- /tests/web-platform-tests/resources/testharness.js: -------------------------------------------------------------------------------- 1 | /*global self*/ 2 | /*jshint latedef: nofunc*/ 3 | /* 4 | Distributed under both the W3C Test Suite License [1] and the W3C 5 | 3-clause BSD License [2]. To contribute to a W3C Test Suite, see the 6 | policies and contribution forms [3]. 7 | 8 | [1] http://www.w3.org/Consortium/Legal/2008/04-testsuite-license 9 | [2] http://www.w3.org/Consortium/Legal/2008/03-bsd-license 10 | [3] http://www.w3.org/2004/10/27-testcases 11 | */ 12 | 13 | /* Documentation is in docs/testharness.md */ 14 | 15 | (function () 16 | { 17 | var debug = false; 18 | // default timeout is 10 seconds, test can override if needed 19 | var settings = { 20 | output:true, 21 | harness_timeout:{ 22 | "normal":10000, 23 | "long":60000 24 | }, 25 | test_timeout:null 26 | }; 27 | 28 | var xhtml_ns = "http://www.w3.org/1999/xhtml"; 29 | 30 | // script_prefix is used by Output.prototype.show_results() to figure out 31 | // where to get testharness.css from. It's enclosed in an extra closure to 32 | // not pollute the library's namespace with variables like "src". 33 | var script_prefix = null; 34 | (function () 35 | { 36 | var scripts = document.getElementsByTagName("script"); 37 | for (var i = 0; i < scripts.length; i++) { 38 | var src; 39 | if (scripts[i].src) { 40 | src = scripts[i].src; 41 | } else if (scripts[i].href) { 42 | //SVG case 43 | src = scripts[i].href.baseVal; 44 | } 45 | 46 | if (src && src.slice(src.length - "testharness.js".length) === "testharness.js") { 47 | script_prefix = src.slice(0, src.length - "testharness.js".length); 48 | break; 49 | } 50 | } 51 | })(); 52 | 53 | /* 54 | * API functions 55 | */ 56 | 57 | var name_counter = 0; 58 | function next_default_name() 59 | { 60 | //Don't use document.title to work around an Opera bug in XHTML documents 61 | var title = document.getElementsByTagName("title")[0]; 62 | var prefix = (title && title.firstChild && title.firstChild.data) || "Untitled"; 63 | var suffix = name_counter > 0 ? " " + name_counter : ""; 64 | name_counter++; 65 | return prefix + suffix; 66 | } 67 | 68 | function test(func, name, properties) 69 | { 70 | var test_name = name ? name : next_default_name(); 71 | properties = properties ? properties : {}; 72 | var test_obj = new Test(test_name, properties); 73 | test_obj.step(func, test_obj, test_obj); 74 | if (test_obj.phase === test_obj.phases.STARTED) { 75 | test_obj.done(); 76 | } 77 | } 78 | 79 | function async_test(func, name, properties) 80 | { 81 | if (typeof func !== "function") { 82 | properties = name; 83 | name = func; 84 | func = null; 85 | } 86 | var test_name = name ? name : next_default_name(); 87 | properties = properties ? properties : {}; 88 | var test_obj = new Test(test_name, properties); 89 | if (func) { 90 | test_obj.step(func, test_obj, test_obj); 91 | } 92 | return test_obj; 93 | } 94 | 95 | function setup(func_or_properties, maybe_properties) 96 | { 97 | var func = null; 98 | var properties = {}; 99 | if (arguments.length === 2) { 100 | func = func_or_properties; 101 | properties = maybe_properties; 102 | } else if (func_or_properties instanceof Function) { 103 | func = func_or_properties; 104 | } else { 105 | properties = func_or_properties; 106 | } 107 | tests.setup(func, properties); 108 | output.setup(properties); 109 | } 110 | 111 | function done() { 112 | if (tests.tests.length === 0) { 113 | tests.set_file_is_test(); 114 | } 115 | if (tests.file_is_test) { 116 | tests.tests[0].done(); 117 | } 118 | tests.end_wait(); 119 | } 120 | 121 | function generate_tests(func, args, properties) { 122 | forEach(args, function(x, i) 123 | { 124 | var name = x[0]; 125 | test(function() 126 | { 127 | func.apply(this, x.slice(1)); 128 | }, 129 | name, 130 | Array.isArray(properties) ? properties[i] : properties); 131 | }); 132 | } 133 | 134 | function on_event(object, event, callback) 135 | { 136 | object.addEventListener(event, callback, false); 137 | } 138 | 139 | expose(test, 'test'); 140 | expose(async_test, 'async_test'); 141 | expose(generate_tests, 'generate_tests'); 142 | expose(setup, 'setup'); 143 | expose(done, 'done'); 144 | expose(on_event, 'on_event'); 145 | 146 | /* 147 | * Return a string truncated to the given length, with ... added at the end 148 | * if it was longer. 149 | */ 150 | function truncate(s, len) 151 | { 152 | if (s.length > len) { 153 | return s.substring(0, len - 3) + "..."; 154 | } 155 | return s; 156 | } 157 | 158 | /* 159 | * Return true if object is probably a Node object. 160 | */ 161 | function is_node(object) 162 | { 163 | // I use duck-typing instead of instanceof, because 164 | // instanceof doesn't work if the node is from another window (like an 165 | // iframe's contentWindow): 166 | // http://www.w3.org/Bugs/Public/show_bug.cgi?id=12295 167 | if ("nodeType" in object && 168 | "nodeName" in object && 169 | "nodeValue" in object && 170 | "childNodes" in object) { 171 | try { 172 | object.nodeType; 173 | } catch (e) { 174 | // The object is probably Node.prototype or another prototype 175 | // object that inherits from it, and not a Node instance. 176 | return false; 177 | } 178 | return true; 179 | } 180 | return false; 181 | } 182 | 183 | /* 184 | * Convert a value to a nice, human-readable string 185 | */ 186 | function format_value(val, seen) 187 | { 188 | if (!seen) { 189 | seen = []; 190 | } 191 | if (typeof val === "object" && val !== null) { 192 | if (seen.indexOf(val) >= 0) { 193 | return "[...]"; 194 | } 195 | seen.push(val); 196 | } 197 | if (Array.isArray(val)) { 198 | return "[" + val.map(function(x) {return format_value(x, seen);}).join(", ") + "]"; 199 | } 200 | 201 | switch (typeof val) { 202 | case "string": 203 | val = val.replace("\\", "\\\\"); 204 | for (var i = 0; i < 32; i++) { 205 | var replace = "\\"; 206 | switch (i) { 207 | case 0: replace += "0"; break; 208 | case 1: replace += "x01"; break; 209 | case 2: replace += "x02"; break; 210 | case 3: replace += "x03"; break; 211 | case 4: replace += "x04"; break; 212 | case 5: replace += "x05"; break; 213 | case 6: replace += "x06"; break; 214 | case 7: replace += "x07"; break; 215 | case 8: replace += "b"; break; 216 | case 9: replace += "t"; break; 217 | case 10: replace += "n"; break; 218 | case 11: replace += "v"; break; 219 | case 12: replace += "f"; break; 220 | case 13: replace += "r"; break; 221 | case 14: replace += "x0e"; break; 222 | case 15: replace += "x0f"; break; 223 | case 16: replace += "x10"; break; 224 | case 17: replace += "x11"; break; 225 | case 18: replace += "x12"; break; 226 | case 19: replace += "x13"; break; 227 | case 20: replace += "x14"; break; 228 | case 21: replace += "x15"; break; 229 | case 22: replace += "x16"; break; 230 | case 23: replace += "x17"; break; 231 | case 24: replace += "x18"; break; 232 | case 25: replace += "x19"; break; 233 | case 26: replace += "x1a"; break; 234 | case 27: replace += "x1b"; break; 235 | case 28: replace += "x1c"; break; 236 | case 29: replace += "x1d"; break; 237 | case 30: replace += "x1e"; break; 238 | case 31: replace += "x1f"; break; 239 | } 240 | val = val.replace(RegExp(String.fromCharCode(i), "g"), replace); 241 | } 242 | return '"' + val.replace(/"/g, '\\"') + '"'; 243 | case "boolean": 244 | case "undefined": 245 | return String(val); 246 | case "number": 247 | // In JavaScript, -0 === 0 and String(-0) == "0", so we have to 248 | // special-case. 249 | if (val === -0 && 1/val === -Infinity) { 250 | return "-0"; 251 | } 252 | return String(val); 253 | case "object": 254 | if (val === null) { 255 | return "null"; 256 | } 257 | 258 | // Special-case Node objects, since those come up a lot in my tests. I 259 | // ignore namespaces. 260 | if (is_node(val)) { 261 | switch (val.nodeType) { 262 | case Node.ELEMENT_NODE: 263 | var ret = "<" + val.localName; 264 | for (var i = 0; i < val.attributes.length; i++) { 265 | ret += " " + val.attributes[i].name + '="' + val.attributes[i].value + '"'; 266 | } 267 | ret += ">" + val.innerHTML + ""; 268 | return "Element node " + truncate(ret, 60); 269 | case Node.TEXT_NODE: 270 | return 'Text node "' + truncate(val.data, 60) + '"'; 271 | case Node.PROCESSING_INSTRUCTION_NODE: 272 | return "ProcessingInstruction node with target " + format_value(truncate(val.target, 60)) + " and data " + format_value(truncate(val.data, 60)); 273 | case Node.COMMENT_NODE: 274 | return "Comment node "; 275 | case Node.DOCUMENT_NODE: 276 | return "Document node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children"); 277 | case Node.DOCUMENT_TYPE_NODE: 278 | return "DocumentType node"; 279 | case Node.DOCUMENT_FRAGMENT_NODE: 280 | return "DocumentFragment node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children"); 281 | default: 282 | return "Node object of unknown type"; 283 | } 284 | } 285 | 286 | /* falls through */ 287 | default: 288 | return typeof val + ' "' + truncate(String(val), 60) + '"'; 289 | } 290 | } 291 | expose(format_value, "format_value"); 292 | 293 | /* 294 | * Assertions 295 | */ 296 | 297 | function assert_true(actual, description) 298 | { 299 | assert(actual === true, "assert_true", description, 300 | "expected true got ${actual}", {actual:actual}); 301 | } 302 | expose(assert_true, "assert_true"); 303 | 304 | function assert_false(actual, description) 305 | { 306 | assert(actual === false, "assert_false", description, 307 | "expected false got ${actual}", {actual:actual}); 308 | } 309 | expose(assert_false, "assert_false"); 310 | 311 | function same_value(x, y) { 312 | if (y !== y) { 313 | //NaN case 314 | return x !== x; 315 | } 316 | if (x === 0 && y === 0) { 317 | //Distinguish +0 and -0 318 | return 1/x === 1/y; 319 | } 320 | return x === y; 321 | } 322 | 323 | function assert_equals(actual, expected, description) 324 | { 325 | /* 326 | * Test if two primitives are equal or two objects 327 | * are the same object 328 | */ 329 | if (typeof actual != typeof expected) { 330 | assert(false, "assert_equals", description, 331 | "expected (" + typeof expected + ") ${expected} but got (" + typeof actual + ") ${actual}", 332 | {expected:expected, actual:actual}); 333 | return; 334 | } 335 | assert(same_value(actual, expected), "assert_equals", description, 336 | "expected ${expected} but got ${actual}", 337 | {expected:expected, actual:actual}); 338 | } 339 | expose(assert_equals, "assert_equals"); 340 | 341 | function assert_not_equals(actual, expected, description) 342 | { 343 | /* 344 | * Test if two primitives are unequal or two objects 345 | * are different objects 346 | */ 347 | assert(!same_value(actual, expected), "assert_not_equals", description, 348 | "got disallowed value ${actual}", 349 | {actual:actual}); 350 | } 351 | expose(assert_not_equals, "assert_not_equals"); 352 | 353 | function assert_in_array(actual, expected, description) 354 | { 355 | assert(expected.indexOf(actual) != -1, "assert_in_array", description, 356 | "value ${actual} not in array ${expected}", 357 | {actual:actual, expected:expected}); 358 | } 359 | expose(assert_in_array, "assert_in_array"); 360 | 361 | function assert_object_equals(actual, expected, description) 362 | { 363 | //This needs to be improved a great deal 364 | function check_equal(actual, expected, stack) 365 | { 366 | stack.push(actual); 367 | 368 | var p; 369 | for (p in actual) { 370 | assert(expected.hasOwnProperty(p), "assert_object_equals", description, 371 | "unexpected property ${p}", {p:p}); 372 | 373 | if (typeof actual[p] === "object" && actual[p] !== null) { 374 | if (stack.indexOf(actual[p]) === -1) { 375 | check_equal(actual[p], expected[p], stack); 376 | } 377 | } else { 378 | assert(same_value(actual[p], expected[p]), "assert_object_equals", description, 379 | "property ${p} expected ${expected} got ${actual}", 380 | {p:p, expected:expected, actual:actual}); 381 | } 382 | } 383 | for (p in expected) { 384 | assert(actual.hasOwnProperty(p), 385 | "assert_object_equals", description, 386 | "expected property ${p} missing", {p:p}); 387 | } 388 | stack.pop(); 389 | } 390 | check_equal(actual, expected, []); 391 | } 392 | expose(assert_object_equals, "assert_object_equals"); 393 | 394 | function assert_array_equals(actual, expected, description) 395 | { 396 | assert(actual.length === expected.length, 397 | "assert_array_equals", description, 398 | "lengths differ, expected ${expected} got ${actual}", 399 | {expected:expected.length, actual:actual.length}); 400 | 401 | for (var i = 0; i < actual.length; i++) { 402 | assert(actual.hasOwnProperty(i) === expected.hasOwnProperty(i), 403 | "assert_array_equals", description, 404 | "property ${i}, property expected to be $expected but was $actual", 405 | {i:i, expected:expected.hasOwnProperty(i) ? "present" : "missing", 406 | actual:actual.hasOwnProperty(i) ? "present" : "missing"}); 407 | assert(same_value(expected[i], actual[i]), 408 | "assert_array_equals", description, 409 | "property ${i}, expected ${expected} but got ${actual}", 410 | {i:i, expected:expected[i], actual:actual[i]}); 411 | } 412 | } 413 | expose(assert_array_equals, "assert_array_equals"); 414 | 415 | function assert_approx_equals(actual, expected, epsilon, description) 416 | { 417 | /* 418 | * Test if two primitive numbers are equal withing +/- epsilon 419 | */ 420 | assert(typeof actual === "number", 421 | "assert_approx_equals", description, 422 | "expected a number but got a ${type_actual}", 423 | {type_actual:typeof actual}); 424 | 425 | assert(Math.abs(actual - expected) <= epsilon, 426 | "assert_approx_equals", description, 427 | "expected ${expected} +/- ${epsilon} but got ${actual}", 428 | {expected:expected, actual:actual, epsilon:epsilon}); 429 | } 430 | expose(assert_approx_equals, "assert_approx_equals"); 431 | 432 | function assert_less_than(actual, expected, description) 433 | { 434 | /* 435 | * Test if a primitive number is less than another 436 | */ 437 | assert(typeof actual === "number", 438 | "assert_less_than", description, 439 | "expected a number but got a ${type_actual}", 440 | {type_actual:typeof actual}); 441 | 442 | assert(actual < expected, 443 | "assert_less_than", description, 444 | "expected a number less than ${expected} but got ${actual}", 445 | {expected:expected, actual:actual}); 446 | } 447 | expose(assert_less_than, "assert_less_than"); 448 | 449 | function assert_greater_than(actual, expected, description) 450 | { 451 | /* 452 | * Test if a primitive number is greater than another 453 | */ 454 | assert(typeof actual === "number", 455 | "assert_greater_than", description, 456 | "expected a number but got a ${type_actual}", 457 | {type_actual:typeof actual}); 458 | 459 | assert(actual > expected, 460 | "assert_greater_than", description, 461 | "expected a number greater than ${expected} but got ${actual}", 462 | {expected:expected, actual:actual}); 463 | } 464 | expose(assert_greater_than, "assert_greater_than"); 465 | 466 | function assert_less_than_equal(actual, expected, description) 467 | { 468 | /* 469 | * Test if a primitive number is less than or equal to another 470 | */ 471 | assert(typeof actual === "number", 472 | "assert_less_than_equal", description, 473 | "expected a number but got a ${type_actual}", 474 | {type_actual:typeof actual}); 475 | 476 | assert(actual <= expected, 477 | "assert_less_than", description, 478 | "expected a number less than or equal to ${expected} but got ${actual}", 479 | {expected:expected, actual:actual}); 480 | } 481 | expose(assert_less_than_equal, "assert_less_than_equal"); 482 | 483 | function assert_greater_than_equal(actual, expected, description) 484 | { 485 | /* 486 | * Test if a primitive number is greater than or equal to another 487 | */ 488 | assert(typeof actual === "number", 489 | "assert_greater_than_equal", description, 490 | "expected a number but got a ${type_actual}", 491 | {type_actual:typeof actual}); 492 | 493 | assert(actual >= expected, 494 | "assert_greater_than_equal", description, 495 | "expected a number greater than or equal to ${expected} but got ${actual}", 496 | {expected:expected, actual:actual}); 497 | } 498 | expose(assert_greater_than_equal, "assert_greater_than_equal"); 499 | 500 | function assert_regexp_match(actual, expected, description) { 501 | /* 502 | * Test if a string (actual) matches a regexp (expected) 503 | */ 504 | assert(expected.test(actual), 505 | "assert_regexp_match", description, 506 | "expected ${expected} but got ${actual}", 507 | {expected:expected, actual:actual}); 508 | } 509 | expose(assert_regexp_match, "assert_regexp_match"); 510 | 511 | function assert_class_string(object, class_string, description) { 512 | assert_equals({}.toString.call(object), "[object " + class_string + "]", 513 | description); 514 | } 515 | expose(assert_class_string, "assert_class_string"); 516 | 517 | 518 | function _assert_own_property(name) { 519 | return function(object, property_name, description) 520 | { 521 | assert(object.hasOwnProperty(property_name), 522 | name, description, 523 | "expected property ${p} missing", {p:property_name}); 524 | }; 525 | } 526 | expose(_assert_own_property("assert_exists"), "assert_exists"); 527 | expose(_assert_own_property("assert_own_property"), "assert_own_property"); 528 | 529 | function assert_not_exists(object, property_name, description) 530 | { 531 | assert(!object.hasOwnProperty(property_name), 532 | "assert_not_exists", description, 533 | "unexpected property ${p} found", {p:property_name}); 534 | } 535 | expose(assert_not_exists, "assert_not_exists"); 536 | 537 | function _assert_inherits(name) { 538 | return function (object, property_name, description) 539 | { 540 | assert(typeof object === "object", 541 | name, description, 542 | "provided value is not an object"); 543 | 544 | assert("hasOwnProperty" in object, 545 | name, description, 546 | "provided value is an object but has no hasOwnProperty method"); 547 | 548 | assert(!object.hasOwnProperty(property_name), 549 | name, description, 550 | "property ${p} found on object expected in prototype chain", 551 | {p:property_name}); 552 | 553 | assert(property_name in object, 554 | name, description, 555 | "property ${p} not found in prototype chain", 556 | {p:property_name}); 557 | }; 558 | } 559 | expose(_assert_inherits("assert_inherits"), "assert_inherits"); 560 | expose(_assert_inherits("assert_idl_attribute"), "assert_idl_attribute"); 561 | 562 | function assert_readonly(object, property_name, description) 563 | { 564 | var initial_value = object[property_name]; 565 | try { 566 | //Note that this can have side effects in the case where 567 | //the property has PutForwards 568 | object[property_name] = initial_value + "a"; //XXX use some other value here? 569 | assert(same_value(object[property_name], initial_value), 570 | "assert_readonly", description, 571 | "changing property ${p} succeeded", 572 | {p:property_name}); 573 | } finally { 574 | object[property_name] = initial_value; 575 | } 576 | } 577 | expose(assert_readonly, "assert_readonly"); 578 | 579 | function assert_throws(code, func, description) 580 | { 581 | try { 582 | func.call(this); 583 | assert(false, "assert_throws", description, 584 | "${func} did not throw", {func:func}); 585 | } catch (e) { 586 | if (e instanceof AssertionError) { 587 | throw e; 588 | } 589 | if (code === null) { 590 | return; 591 | } 592 | if (typeof code === "object") { 593 | assert(typeof e == "object" && "name" in e && e.name == code.name, 594 | "assert_throws", description, 595 | "${func} threw ${actual} (${actual_name}) expected ${expected} (${expected_name})", 596 | {func:func, actual:e, actual_name:e.name, 597 | expected:code, 598 | expected_name:code.name}); 599 | return; 600 | } 601 | 602 | var code_name_map = { 603 | INDEX_SIZE_ERR: 'IndexSizeError', 604 | HIERARCHY_REQUEST_ERR: 'HierarchyRequestError', 605 | WRONG_DOCUMENT_ERR: 'WrongDocumentError', 606 | INVALID_CHARACTER_ERR: 'InvalidCharacterError', 607 | NO_MODIFICATION_ALLOWED_ERR: 'NoModificationAllowedError', 608 | NOT_FOUND_ERR: 'NotFoundError', 609 | NOT_SUPPORTED_ERR: 'NotSupportedError', 610 | INVALID_STATE_ERR: 'InvalidStateError', 611 | SYNTAX_ERR: 'SyntaxError', 612 | INVALID_MODIFICATION_ERR: 'InvalidModificationError', 613 | NAMESPACE_ERR: 'NamespaceError', 614 | INVALID_ACCESS_ERR: 'InvalidAccessError', 615 | TYPE_MISMATCH_ERR: 'TypeMismatchError', 616 | SECURITY_ERR: 'SecurityError', 617 | NETWORK_ERR: 'NetworkError', 618 | ABORT_ERR: 'AbortError', 619 | URL_MISMATCH_ERR: 'URLMismatchError', 620 | QUOTA_EXCEEDED_ERR: 'QuotaExceededError', 621 | TIMEOUT_ERR: 'TimeoutError', 622 | INVALID_NODE_TYPE_ERR: 'InvalidNodeTypeError', 623 | DATA_CLONE_ERR: 'DataCloneError' 624 | }; 625 | 626 | var name = code in code_name_map ? code_name_map[code] : code; 627 | 628 | var name_code_map = { 629 | IndexSizeError: 1, 630 | HierarchyRequestError: 3, 631 | WrongDocumentError: 4, 632 | InvalidCharacterError: 5, 633 | NoModificationAllowedError: 7, 634 | NotFoundError: 8, 635 | NotSupportedError: 9, 636 | InvalidStateError: 11, 637 | SyntaxError: 12, 638 | InvalidModificationError: 13, 639 | NamespaceError: 14, 640 | InvalidAccessError: 15, 641 | TypeMismatchError: 17, 642 | SecurityError: 18, 643 | NetworkError: 19, 644 | AbortError: 20, 645 | URLMismatchError: 21, 646 | QuotaExceededError: 22, 647 | TimeoutError: 23, 648 | InvalidNodeTypeError: 24, 649 | DataCloneError: 25, 650 | 651 | UnknownError: 0, 652 | ConstraintError: 0, 653 | DataError: 0, 654 | TransactionInactiveError: 0, 655 | ReadOnlyError: 0, 656 | VersionError: 0 657 | }; 658 | 659 | if (!(name in name_code_map)) { 660 | throw new AssertionError('Test bug: unrecognized DOMException code "' + code + '" passed to assert_throws()'); 661 | } 662 | 663 | var required_props = { code: name_code_map[name] }; 664 | 665 | if (required_props.code === 0 || 666 | ("name" in e && e.name !== e.name.toUpperCase() && e.name !== "DOMException")) { 667 | // New style exception: also test the name property. 668 | required_props.name = name; 669 | } 670 | 671 | //We'd like to test that e instanceof the appropriate interface, 672 | //but we can't, because we don't know what window it was created 673 | //in. It might be an instanceof the appropriate interface on some 674 | //unknown other window. TODO: Work around this somehow? 675 | 676 | assert(typeof e == "object", 677 | "assert_throws", description, 678 | "${func} threw ${e} with type ${type}, not an object", 679 | {func:func, e:e, type:typeof e}); 680 | 681 | for (var prop in required_props) { 682 | assert(typeof e == "object" && prop in e && e[prop] == required_props[prop], 683 | "assert_throws", description, 684 | "${func} threw ${e} that is not a DOMException " + code + ": property ${prop} is equal to ${actual}, expected ${expected}", 685 | {func:func, e:e, prop:prop, actual:e[prop], expected:required_props[prop]}); 686 | } 687 | } 688 | } 689 | expose(assert_throws, "assert_throws"); 690 | 691 | function assert_unreached(description) { 692 | assert(false, "assert_unreached", description, 693 | "Reached unreachable code"); 694 | } 695 | expose(assert_unreached, "assert_unreached"); 696 | 697 | function assert_any(assert_func, actual, expected_array) 698 | { 699 | var args = [].slice.call(arguments, 3); 700 | var errors = []; 701 | var passed = false; 702 | forEach(expected_array, 703 | function(expected) 704 | { 705 | try { 706 | assert_func.apply(this, [actual, expected].concat(args)); 707 | passed = true; 708 | } catch (e) { 709 | errors.push(e.message); 710 | } 711 | }); 712 | if (!passed) { 713 | throw new AssertionError(errors.join("\n\n")); 714 | } 715 | } 716 | expose(assert_any, "assert_any"); 717 | 718 | function Test(name, properties) 719 | { 720 | if (tests.file_is_test && tests.tests.length) { 721 | throw new Error("Tried to create a test with file_is_test"); 722 | } 723 | this.name = name; 724 | 725 | this.phases = { 726 | INITIAL:0, 727 | STARTED:1, 728 | HAS_RESULT:2, 729 | COMPLETE:3 730 | }; 731 | this.phase = this.phases.INITIAL; 732 | 733 | this.status = this.NOTRUN; 734 | this.timeout_id = null; 735 | 736 | this.properties = properties; 737 | var timeout = properties.timeout ? properties.timeout : settings.test_timeout; 738 | if (timeout != null) { 739 | this.timeout_length = timeout * tests.timeout_multiplier; 740 | } else { 741 | this.timeout_length = null; 742 | } 743 | 744 | this.message = null; 745 | 746 | this.steps = []; 747 | 748 | this.cleanup_callbacks = []; 749 | 750 | tests.push(this); 751 | } 752 | 753 | Test.statuses = { 754 | PASS:0, 755 | FAIL:1, 756 | TIMEOUT:2, 757 | NOTRUN:3 758 | }; 759 | 760 | Test.prototype = merge({}, Test.statuses); 761 | 762 | Test.prototype.structured_clone = function() 763 | { 764 | if (!this._structured_clone) { 765 | var msg = this.message; 766 | msg = msg ? String(msg) : msg; 767 | this._structured_clone = merge({ 768 | name:String(this.name), 769 | status:this.status, 770 | message:msg 771 | }, Test.statuses); 772 | } 773 | return this._structured_clone; 774 | }; 775 | 776 | Test.prototype.step = function(func, this_obj) 777 | { 778 | if (this.phase > this.phases.STARTED) { 779 | return; 780 | } 781 | this.phase = this.phases.STARTED; 782 | //If we don't get a result before the harness times out that will be a test timeout 783 | this.set_status(this.TIMEOUT, "Test timed out"); 784 | 785 | tests.started = true; 786 | 787 | if (this.timeout_id === null) { 788 | this.set_timeout(); 789 | } 790 | 791 | this.steps.push(func); 792 | 793 | if (arguments.length === 1) { 794 | this_obj = this; 795 | } 796 | 797 | try { 798 | return func.apply(this_obj, Array.prototype.slice.call(arguments, 2)); 799 | } catch (e) { 800 | if (this.phase >= this.phases.HAS_RESULT) { 801 | return; 802 | } 803 | var message = (typeof e === "object" && e !== null) ? e.message : e; 804 | if (typeof e.stack != "undefined" && typeof e.message == "string") { 805 | //Try to make it more informative for some exceptions, at least 806 | //in Gecko and WebKit. This results in a stack dump instead of 807 | //just errors like "Cannot read property 'parentNode' of null" 808 | //or "root is null". Makes it a lot longer, of course. 809 | message += "(stack: " + e.stack + ")"; 810 | } 811 | this.set_status(this.FAIL, message); 812 | this.phase = this.phases.HAS_RESULT; 813 | this.done(); 814 | } 815 | }; 816 | 817 | Test.prototype.step_func = function(func, this_obj) 818 | { 819 | var test_this = this; 820 | 821 | if (arguments.length === 1) { 822 | this_obj = test_this; 823 | } 824 | 825 | return function() 826 | { 827 | return test_this.step.apply(test_this, [func, this_obj].concat( 828 | Array.prototype.slice.call(arguments))); 829 | }; 830 | }; 831 | 832 | Test.prototype.step_func_done = function(func, this_obj) 833 | { 834 | var test_this = this; 835 | 836 | if (arguments.length === 1) { 837 | this_obj = test_this; 838 | } 839 | 840 | return function() 841 | { 842 | if (func) { 843 | test_this.step.apply(test_this, [func, this_obj].concat( 844 | Array.prototype.slice.call(arguments))); 845 | } 846 | test_this.done(); 847 | }; 848 | }; 849 | 850 | Test.prototype.unreached_func = function(description) 851 | { 852 | return this.step_func(function() { 853 | assert_unreached(description); 854 | }); 855 | }; 856 | 857 | Test.prototype.add_cleanup = function(callback) { 858 | this.cleanup_callbacks.push(callback); 859 | }; 860 | 861 | Test.prototype.force_timeout = function() { 862 | this.set_status(this.TIMEOUT); 863 | this.phase = this.phases.HAS_RESULT; 864 | } 865 | 866 | Test.prototype.set_timeout = function() 867 | { 868 | if (this.timeout_length !== null) { 869 | var this_obj = this; 870 | this.timeout_id = setTimeout(function() 871 | { 872 | this_obj.timeout(); 873 | }, this.timeout_length); 874 | } 875 | }; 876 | 877 | Test.prototype.set_status = function(status, message) 878 | { 879 | this.status = status; 880 | this.message = message; 881 | }; 882 | 883 | Test.prototype.timeout = function() 884 | { 885 | this.timeout_id = null; 886 | this.set_status(this.TIMEOUT, "Test timed out"); 887 | this.phase = this.phases.HAS_RESULT; 888 | this.done(); 889 | }; 890 | 891 | Test.prototype.done = function() 892 | { 893 | if (this.phase == this.phases.COMPLETE) { 894 | return; 895 | } 896 | 897 | if (this.phase <= this.phases.STARTED) { 898 | this.set_status(this.PASS, null); 899 | } 900 | 901 | if (this.status == this.NOTRUN) { 902 | alert(this.phase); 903 | } 904 | 905 | this.phase = this.phases.COMPLETE; 906 | 907 | clearTimeout(this.timeout_id); 908 | tests.result(this); 909 | this.cleanup(); 910 | }; 911 | 912 | Test.prototype.cleanup = function() { 913 | forEach(this.cleanup_callbacks, 914 | function(cleanup_callback) { 915 | cleanup_callback(); 916 | }); 917 | }; 918 | 919 | /* 920 | * Harness 921 | */ 922 | 923 | function TestsStatus() 924 | { 925 | this.status = null; 926 | this.message = null; 927 | } 928 | 929 | TestsStatus.statuses = { 930 | OK:0, 931 | ERROR:1, 932 | TIMEOUT:2 933 | }; 934 | 935 | TestsStatus.prototype = merge({}, TestsStatus.statuses); 936 | 937 | TestsStatus.prototype.structured_clone = function() 938 | { 939 | if (!this._structured_clone) { 940 | var msg = this.message; 941 | msg = msg ? String(msg) : msg; 942 | this._structured_clone = merge({ 943 | status:this.status, 944 | message:msg 945 | }, TestsStatus.statuses); 946 | } 947 | return this._structured_clone; 948 | }; 949 | 950 | function Tests() 951 | { 952 | this.tests = []; 953 | this.num_pending = 0; 954 | 955 | this.phases = { 956 | INITIAL:0, 957 | SETUP:1, 958 | HAVE_TESTS:2, 959 | HAVE_RESULTS:3, 960 | COMPLETE:4 961 | }; 962 | this.phase = this.phases.INITIAL; 963 | 964 | this.properties = {}; 965 | 966 | //All tests can't be done until the load event fires 967 | this.all_loaded = false; 968 | this.wait_for_finish = false; 969 | this.processing_callbacks = false; 970 | 971 | this.allow_uncaught_exception = false; 972 | 973 | this.file_is_test = false; 974 | 975 | this.timeout_multiplier = 1; 976 | this.timeout_length = this.get_timeout(); 977 | this.timeout_id = null; 978 | 979 | this.start_callbacks = []; 980 | this.test_done_callbacks = []; 981 | this.all_done_callbacks = []; 982 | 983 | this.status = new TestsStatus(); 984 | 985 | var this_obj = this; 986 | 987 | on_event(window, "load", 988 | function() 989 | { 990 | this_obj.all_loaded = true; 991 | if (this_obj.all_done()) 992 | { 993 | this_obj.complete(); 994 | } 995 | }); 996 | 997 | this.set_timeout(); 998 | } 999 | 1000 | Tests.prototype.setup = function(func, properties) 1001 | { 1002 | if (this.phase >= this.phases.HAVE_RESULTS) { 1003 | return; 1004 | } 1005 | 1006 | if (this.phase < this.phases.SETUP) { 1007 | this.phase = this.phases.SETUP; 1008 | } 1009 | 1010 | this.properties = properties; 1011 | 1012 | for (var p in properties) { 1013 | if (properties.hasOwnProperty(p)) { 1014 | var value = properties[p]; 1015 | if (p == "allow_uncaught_exception") { 1016 | this.allow_uncaught_exception = value; 1017 | } else if (p == "explicit_done" && value) { 1018 | this.wait_for_finish = true; 1019 | } else if (p == "explicit_timeout" && value) { 1020 | this.timeout_length = null; 1021 | if (this.timeout_id) 1022 | { 1023 | clearTimeout(this.timeout_id); 1024 | } 1025 | } else if (p == "timeout_multiplier") { 1026 | this.timeout_multiplier = value; 1027 | } 1028 | } 1029 | } 1030 | 1031 | if (func) { 1032 | try { 1033 | func(); 1034 | } catch (e) { 1035 | this.status.status = this.status.ERROR; 1036 | this.status.message = String(e); 1037 | } 1038 | } 1039 | this.set_timeout(); 1040 | }; 1041 | 1042 | Tests.prototype.set_file_is_test = function() { 1043 | if (this.tests.length > 0) { 1044 | throw new Error("Tried to set file as test after creating a test"); 1045 | } 1046 | this.wait_for_finish = true; 1047 | this.file_is_test = true; 1048 | // Create the test, which will add it to the list of tests 1049 | async_test(); 1050 | }; 1051 | 1052 | Tests.prototype.get_timeout = function() { 1053 | var metas = document.getElementsByTagName("meta"); 1054 | for (var i = 0; i < metas.length; i++) { 1055 | if (metas[i].name == "timeout") { 1056 | if (metas[i].content == "long") { 1057 | return settings.harness_timeout.long; 1058 | } 1059 | break; 1060 | } 1061 | } 1062 | return settings.harness_timeout.normal; 1063 | }; 1064 | 1065 | Tests.prototype.set_timeout = function() { 1066 | var this_obj = this; 1067 | clearTimeout(this.timeout_id); 1068 | if (this.timeout_length !== null) { 1069 | this.timeout_id = setTimeout(function() { 1070 | this_obj.timeout(); 1071 | }, this.timeout_length); 1072 | } 1073 | }; 1074 | 1075 | Tests.prototype.timeout = function() { 1076 | if (this.status.status === null) { 1077 | this.status.status = this.status.TIMEOUT; 1078 | } 1079 | this.complete(); 1080 | }; 1081 | 1082 | Tests.prototype.end_wait = function() 1083 | { 1084 | this.wait_for_finish = false; 1085 | if (this.all_done()) { 1086 | this.complete(); 1087 | } 1088 | }; 1089 | 1090 | Tests.prototype.push = function(test) 1091 | { 1092 | if (this.phase < this.phases.HAVE_TESTS) { 1093 | this.start(); 1094 | } 1095 | this.num_pending++; 1096 | this.tests.push(test); 1097 | }; 1098 | 1099 | Tests.prototype.all_done = function() { 1100 | return (this.tests.length > 0 && this.all_loaded && this.num_pending === 0 && 1101 | !this.wait_for_finish && !this.processing_callbacks); 1102 | }; 1103 | 1104 | Tests.prototype.start = function() { 1105 | this.phase = this.phases.HAVE_TESTS; 1106 | this.notify_start(); 1107 | }; 1108 | 1109 | Tests.prototype.notify_start = function() { 1110 | var this_obj = this; 1111 | forEach (this.start_callbacks, 1112 | function(callback) 1113 | { 1114 | callback(this_obj.properties); 1115 | }); 1116 | forEach_windows( 1117 | function(w, is_same_origin) 1118 | { 1119 | if (is_same_origin && w.start_callback) { 1120 | try { 1121 | w.start_callback(this_obj.properties); 1122 | } catch (e) { 1123 | if (debug) { 1124 | throw e; 1125 | } 1126 | } 1127 | } 1128 | if (supports_post_message(w) && w !== self) { 1129 | w.postMessage({ 1130 | type: "start", 1131 | properties: this_obj.properties 1132 | }, "*"); 1133 | } 1134 | }); 1135 | }; 1136 | 1137 | Tests.prototype.result = function(test) 1138 | { 1139 | if (this.phase > this.phases.HAVE_RESULTS) { 1140 | return; 1141 | } 1142 | this.phase = this.phases.HAVE_RESULTS; 1143 | this.num_pending--; 1144 | this.notify_result(test); 1145 | }; 1146 | 1147 | Tests.prototype.notify_result = function(test) { 1148 | var this_obj = this; 1149 | this.processing_callbacks = true; 1150 | forEach(this.test_done_callbacks, 1151 | function(callback) 1152 | { 1153 | callback(test, this_obj); 1154 | }); 1155 | 1156 | forEach_windows( 1157 | function(w, is_same_origin) 1158 | { 1159 | if (is_same_origin && w.result_callback) { 1160 | try { 1161 | w.result_callback(test); 1162 | } catch (e) { 1163 | if (debug) { 1164 | throw e; 1165 | } 1166 | } 1167 | } 1168 | if (supports_post_message(w) && w !== self) { 1169 | w.postMessage({ 1170 | type: "result", 1171 | test: test.structured_clone() 1172 | }, "*"); 1173 | } 1174 | }); 1175 | this.processing_callbacks = false; 1176 | if (this_obj.all_done()) { 1177 | this_obj.complete(); 1178 | } 1179 | }; 1180 | 1181 | Tests.prototype.complete = function() { 1182 | if (this.phase === this.phases.COMPLETE) { 1183 | return; 1184 | } 1185 | this.phase = this.phases.COMPLETE; 1186 | var this_obj = this; 1187 | this.tests.forEach( 1188 | function(x) 1189 | { 1190 | if (x.status === x.NOTRUN) { 1191 | this_obj.notify_result(x); 1192 | x.cleanup(); 1193 | } 1194 | } 1195 | ); 1196 | this.notify_complete(); 1197 | }; 1198 | 1199 | Tests.prototype.notify_complete = function() 1200 | { 1201 | clearTimeout(this.timeout_id); 1202 | var this_obj = this; 1203 | var tests = map(this_obj.tests, 1204 | function(test) 1205 | { 1206 | return test.structured_clone(); 1207 | }); 1208 | if (this.status.status === null) { 1209 | this.status.status = this.status.OK; 1210 | } 1211 | 1212 | forEach (this.all_done_callbacks, 1213 | function(callback) 1214 | { 1215 | callback(this_obj.tests, this_obj.status); 1216 | }); 1217 | 1218 | forEach_windows( 1219 | function(w, is_same_origin) 1220 | { 1221 | if (is_same_origin && w.completion_callback) { 1222 | try { 1223 | w.completion_callback(this_obj.tests, this_obj.status); 1224 | } catch (e) { 1225 | if (debug) { 1226 | throw e; 1227 | } 1228 | } 1229 | } 1230 | if (supports_post_message(w) && w !== self) { 1231 | w.postMessage({ 1232 | type: "complete", 1233 | tests: tests, 1234 | status: this_obj.status.structured_clone() 1235 | }, "*"); 1236 | } 1237 | }); 1238 | }; 1239 | 1240 | var tests = new Tests(); 1241 | 1242 | addEventListener("error", function(e) { 1243 | if (tests.file_is_test) { 1244 | var test = tests.tests[0]; 1245 | if (test.phase >= test.phases.HAS_RESULT) { 1246 | return; 1247 | } 1248 | var message = e.message; 1249 | test.set_status(test.FAIL, message); 1250 | test.phase = test.phases.HAS_RESULT; 1251 | test.done(); 1252 | done(); 1253 | } else if (!tests.allow_uncaught_exception) { 1254 | tests.status.status = tests.status.ERROR; 1255 | tests.status.message = e.message; 1256 | } 1257 | }); 1258 | 1259 | function timeout() { 1260 | if (tests.timeout_length === null) { 1261 | tests.timeout(); 1262 | } 1263 | } 1264 | expose(timeout, 'timeout'); 1265 | 1266 | function add_start_callback(callback) { 1267 | tests.start_callbacks.push(callback); 1268 | } 1269 | 1270 | function add_result_callback(callback) 1271 | { 1272 | tests.test_done_callbacks.push(callback); 1273 | } 1274 | 1275 | function add_completion_callback(callback) 1276 | { 1277 | tests.all_done_callbacks.push(callback); 1278 | } 1279 | 1280 | expose(add_start_callback, 'add_start_callback'); 1281 | expose(add_result_callback, 'add_result_callback'); 1282 | expose(add_completion_callback, 'add_completion_callback'); 1283 | 1284 | /* 1285 | * Output listener 1286 | */ 1287 | 1288 | function Output() { 1289 | this.output_document = document; 1290 | this.output_node = null; 1291 | this.done_count = 0; 1292 | this.enabled = settings.output; 1293 | this.phase = this.INITIAL; 1294 | } 1295 | 1296 | Output.prototype.INITIAL = 0; 1297 | Output.prototype.STARTED = 1; 1298 | Output.prototype.HAVE_RESULTS = 2; 1299 | Output.prototype.COMPLETE = 3; 1300 | 1301 | Output.prototype.setup = function(properties) { 1302 | if (this.phase > this.INITIAL) { 1303 | return; 1304 | } 1305 | 1306 | //If output is disabled in testharnessreport.js the test shouldn't be 1307 | //able to override that 1308 | this.enabled = this.enabled && (properties.hasOwnProperty("output") ? 1309 | properties.output : settings.output); 1310 | }; 1311 | 1312 | Output.prototype.init = function(properties) { 1313 | if (this.phase >= this.STARTED) { 1314 | return; 1315 | } 1316 | if (properties.output_document) { 1317 | this.output_document = properties.output_document; 1318 | } else { 1319 | this.output_document = document; 1320 | } 1321 | this.phase = this.STARTED; 1322 | }; 1323 | 1324 | Output.prototype.resolve_log = function() { 1325 | var output_document; 1326 | if (typeof this.output_document === "function") { 1327 | output_document = this.output_document.apply(undefined); 1328 | } else { 1329 | output_document = this.output_document; 1330 | } 1331 | if (!output_document) { 1332 | return; 1333 | } 1334 | var node = output_document.getElementById("log"); 1335 | if (!node) { 1336 | if (!document.body || document.readyState == "loading") { 1337 | return; 1338 | } 1339 | node = output_document.createElement("div"); 1340 | node.id = "log"; 1341 | output_document.body.appendChild(node); 1342 | } 1343 | this.output_document = output_document; 1344 | this.output_node = node; 1345 | }; 1346 | 1347 | Output.prototype.show_status = function() { 1348 | if (this.phase < this.STARTED) { 1349 | this.init(); 1350 | } 1351 | if (!this.enabled) { 1352 | return; 1353 | } 1354 | if (this.phase < this.HAVE_RESULTS) { 1355 | this.resolve_log(); 1356 | this.phase = this.HAVE_RESULTS; 1357 | } 1358 | this.done_count++; 1359 | if (this.output_node) { 1360 | if (this.done_count < 100 || 1361 | (this.done_count < 1000 && this.done_count % 100 === 0) || 1362 | this.done_count % 1000 === 0) { 1363 | this.output_node.textContent = "Running, " + 1364 | this.done_count + " complete, " + 1365 | tests.num_pending + " remain"; 1366 | } 1367 | } 1368 | }; 1369 | 1370 | Output.prototype.show_results = function (tests, harness_status) { 1371 | if (this.phase >= this.COMPLETE) { 1372 | return; 1373 | } 1374 | if (!this.enabled) { 1375 | return; 1376 | } 1377 | if (!this.output_node) { 1378 | this.resolve_log(); 1379 | } 1380 | this.phase = this.COMPLETE; 1381 | 1382 | var log = this.output_node; 1383 | if (!log) { 1384 | return; 1385 | } 1386 | var output_document = this.output_document; 1387 | 1388 | while (log.lastChild) { 1389 | log.removeChild(log.lastChild); 1390 | } 1391 | 1392 | if (script_prefix != null) { 1393 | var stylesheet = output_document.createElementNS(xhtml_ns, "link"); 1394 | stylesheet.setAttribute("rel", "stylesheet"); 1395 | stylesheet.setAttribute("href", script_prefix + "testharness.css"); 1396 | var heads = output_document.getElementsByTagName("head"); 1397 | if (heads.length) { 1398 | heads[0].appendChild(stylesheet); 1399 | } 1400 | } 1401 | 1402 | var status_text_harness = {}; 1403 | status_text_harness[harness_status.OK] = "OK"; 1404 | status_text_harness[harness_status.ERROR] = "Error"; 1405 | status_text_harness[harness_status.TIMEOUT] = "Timeout"; 1406 | 1407 | var status_text = {}; 1408 | status_text[Test.prototype.PASS] = "Pass"; 1409 | status_text[Test.prototype.FAIL] = "Fail"; 1410 | status_text[Test.prototype.TIMEOUT] = "Timeout"; 1411 | status_text[Test.prototype.NOTRUN] = "Not Run"; 1412 | 1413 | var status_number = {}; 1414 | forEach(tests, 1415 | function(test) { 1416 | var status = status_text[test.status]; 1417 | if (status_number.hasOwnProperty(status)) { 1418 | status_number[status] += 1; 1419 | } else { 1420 | status_number[status] = 1; 1421 | } 1422 | }); 1423 | 1424 | function status_class(status) 1425 | { 1426 | return status.replace(/\s/g, '').toLowerCase(); 1427 | } 1428 | 1429 | var summary_template = ["section", {"id":"summary"}, 1430 | ["h2", {}, "Summary"], 1431 | function() 1432 | { 1433 | 1434 | var status = status_text_harness[harness_status.status]; 1435 | var rv = [["section", {}, 1436 | ["p", {}, 1437 | "Harness status: ", 1438 | ["span", {"class":status_class(status)}, 1439 | status 1440 | ], 1441 | ] 1442 | ]]; 1443 | 1444 | if (harness_status.status === harness_status.ERROR) { 1445 | rv[0].push(["pre", {}, harness_status.message]); 1446 | } 1447 | return rv; 1448 | }, 1449 | ["p", {}, "Found ${num_tests} tests"], 1450 | function() { 1451 | var rv = [["div", {}]]; 1452 | var i = 0; 1453 | while (status_text.hasOwnProperty(i)) { 1454 | if (status_number.hasOwnProperty(status_text[i])) { 1455 | var status = status_text[i]; 1456 | rv[0].push(["div", {"class":status_class(status)}, 1457 | ["label", {}, 1458 | ["input", {type:"checkbox", checked:"checked"}], 1459 | status_number[status] + " " + status]]); 1460 | } 1461 | i++; 1462 | } 1463 | return rv; 1464 | }, 1465 | ]; 1466 | 1467 | log.appendChild(render(summary_template, {num_tests:tests.length}, output_document)); 1468 | 1469 | forEach(output_document.querySelectorAll("section#summary label"), 1470 | function(element) 1471 | { 1472 | on_event(element, "click", 1473 | function(e) 1474 | { 1475 | if (output_document.getElementById("results") === null) { 1476 | e.preventDefault(); 1477 | return; 1478 | } 1479 | var result_class = element.parentNode.getAttribute("class"); 1480 | var style_element = output_document.querySelector("style#hide-" + result_class); 1481 | var input_element = element.querySelector("input"); 1482 | if (!style_element && !input_element.checked) { 1483 | style_element = output_document.createElementNS(xhtml_ns, "style"); 1484 | style_element.id = "hide-" + result_class; 1485 | style_element.textContent = "table#results > tbody > tr."+result_class+"{display:none}"; 1486 | output_document.body.appendChild(style_element); 1487 | } else if (style_element && input_element.checked) { 1488 | style_element.parentNode.removeChild(style_element); 1489 | } 1490 | }); 1491 | }); 1492 | 1493 | // This use of innerHTML plus manual escaping is not recommended in 1494 | // general, but is necessary here for performance. Using textContent 1495 | // on each individual adds tens of seconds of execution time for 1496 | // large test suites (tens of thousands of tests). 1497 | function escape_html(s) 1498 | { 1499 | return s.replace(/\&/g, "&") 1500 | .replace(/" + 1529 | "ResultTest Name" + 1530 | (assertions ? "Assertion" : "") + 1531 | "Message" + 1532 | ""; 1533 | for (var i = 0; i < tests.length; i++) { 1534 | html += '' + 1537 | escape_html(status_text[tests[i].status]) + 1538 | "" + 1539 | escape_html(tests[i].name) + 1540 | "" + 1541 | (assertions ? escape_html(get_assertion(tests[i])) + "" : "") + 1542 | escape_html(tests[i].message ? tests[i].message : " ") + 1543 | ""; 1544 | } 1545 | html += ""; 1546 | try { 1547 | log.lastChild.innerHTML = html; 1548 | } catch (e) { 1549 | log.appendChild(document.createElementNS(xhtml_ns, "p")) 1550 | .textContent = "Setting innerHTML for the log threw an exception."; 1551 | log.appendChild(document.createElementNS(xhtml_ns, "pre")) 1552 | .textContent = html; 1553 | } 1554 | }; 1555 | 1556 | var output = new Output(); 1557 | add_start_callback(function (properties) {output.init(properties);}); 1558 | add_result_callback(function () {output.show_status();}); 1559 | add_completion_callback(function (tests, harness_status) {output.show_results(tests, harness_status);}); 1560 | 1561 | /* 1562 | * Template code 1563 | * 1564 | * A template is just a javascript structure. An element is represented as: 1565 | * 1566 | * [tag_name, {attr_name:attr_value}, child1, child2] 1567 | * 1568 | * the children can either be strings (which act like text nodes), other templates or 1569 | * functions (see below) 1570 | * 1571 | * A text node is represented as 1572 | * 1573 | * ["{text}", value] 1574 | * 1575 | * String values have a simple substitution syntax; ${foo} represents a variable foo. 1576 | * 1577 | * It is possible to embed logic in templates by using a function in a place where a 1578 | * node would usually go. The function must either return part of a template or null. 1579 | * 1580 | * In cases where a set of nodes are required as output rather than a single node 1581 | * with children it is possible to just use a list 1582 | * [node1, node2, node3] 1583 | * 1584 | * Usage: 1585 | * 1586 | * render(template, substitutions) - take a template and an object mapping 1587 | * variable names to parameters and return either a DOM node or a list of DOM nodes 1588 | * 1589 | * substitute(template, substitutions) - take a template and variable mapping object, 1590 | * make the variable substitutions and return the substituted template 1591 | * 1592 | */ 1593 | 1594 | function is_single_node(template) 1595 | { 1596 | return typeof template[0] === "string"; 1597 | } 1598 | 1599 | function substitute(template, substitutions) 1600 | { 1601 | if (typeof template === "function") { 1602 | var replacement = template(substitutions); 1603 | if (!replacement) { 1604 | return null; 1605 | } 1606 | 1607 | return substitute(replacement, substitutions); 1608 | } 1609 | 1610 | if (is_single_node(template)) { 1611 | return substitute_single(template, substitutions); 1612 | } 1613 | 1614 | return filter(map(template, function(x) { 1615 | return substitute(x, substitutions); 1616 | }), function(x) {return x !== null;}); 1617 | } 1618 | 1619 | function substitute_single(template, substitutions) 1620 | { 1621 | var substitution_re = /\$\{([^ }]*)\}/g; 1622 | 1623 | function do_substitution(input) { 1624 | var components = input.split(substitution_re); 1625 | var rv = []; 1626 | for (var i = 0; i < components.length; i += 2) { 1627 | rv.push(components[i]); 1628 | if (components[i + 1]) { 1629 | rv.push(String(substitutions[components[i + 1]])); 1630 | } 1631 | } 1632 | return rv; 1633 | } 1634 | 1635 | function substitute_attrs(attrs, rv) 1636 | { 1637 | rv[1] = {}; 1638 | for (var name in template[1]) { 1639 | if (attrs.hasOwnProperty(name)) { 1640 | var new_name = do_substitution(name).join(""); 1641 | var new_value = do_substitution(attrs[name]).join(""); 1642 | rv[1][new_name] = new_value; 1643 | } 1644 | } 1645 | } 1646 | 1647 | function substitute_children(children, rv) 1648 | { 1649 | for (var i = 0; i < children.length; i++) { 1650 | if (children[i] instanceof Object) { 1651 | var replacement = substitute(children[i], substitutions); 1652 | if (replacement !== null) { 1653 | if (is_single_node(replacement)) { 1654 | rv.push(replacement); 1655 | } else { 1656 | extend(rv, replacement); 1657 | } 1658 | } 1659 | } else { 1660 | extend(rv, do_substitution(String(children[i]))); 1661 | } 1662 | } 1663 | return rv; 1664 | } 1665 | 1666 | var rv = []; 1667 | rv.push(do_substitution(String(template[0])).join("")); 1668 | 1669 | if (template[0] === "{text}") { 1670 | substitute_children(template.slice(1), rv); 1671 | } else { 1672 | substitute_attrs(template[1], rv); 1673 | substitute_children(template.slice(2), rv); 1674 | } 1675 | 1676 | return rv; 1677 | } 1678 | 1679 | function make_dom_single(template, doc) 1680 | { 1681 | var output_document = doc || document; 1682 | var element; 1683 | if (template[0] === "{text}") { 1684 | element = output_document.createTextNode(""); 1685 | for (var i = 1; i < template.length; i++) { 1686 | element.data += template[i]; 1687 | } 1688 | } else { 1689 | element = output_document.createElementNS(xhtml_ns, template[0]); 1690 | for (var name in template[1]) { 1691 | if (template[1].hasOwnProperty(name)) { 1692 | element.setAttribute(name, template[1][name]); 1693 | } 1694 | } 1695 | for (var i = 2; i < template.length; i++) { 1696 | if (template[i] instanceof Object) { 1697 | var sub_element = make_dom(template[i]); 1698 | element.appendChild(sub_element); 1699 | } else { 1700 | var text_node = output_document.createTextNode(template[i]); 1701 | element.appendChild(text_node); 1702 | } 1703 | } 1704 | } 1705 | 1706 | return element; 1707 | } 1708 | 1709 | 1710 | 1711 | function make_dom(template, substitutions, output_document) 1712 | { 1713 | if (is_single_node(template)) { 1714 | return make_dom_single(template, output_document); 1715 | } 1716 | 1717 | return map(template, function(x) { 1718 | return make_dom_single(x, output_document); 1719 | }); 1720 | } 1721 | 1722 | function render(template, substitutions, output_document) 1723 | { 1724 | return make_dom(substitute(template, substitutions), output_document); 1725 | } 1726 | 1727 | /* 1728 | * Utility funcions 1729 | */ 1730 | function assert(expected_true, function_name, description, error, substitutions) 1731 | { 1732 | if (tests.tests.length === 0) { 1733 | tests.set_file_is_test(); 1734 | } 1735 | if (expected_true !== true) { 1736 | var msg = make_message(function_name, description, 1737 | error, substitutions); 1738 | throw new AssertionError(msg); 1739 | } 1740 | } 1741 | 1742 | function AssertionError(message) 1743 | { 1744 | this.message = message; 1745 | } 1746 | 1747 | AssertionError.prototype.toString = function() { 1748 | return this.message; 1749 | }; 1750 | 1751 | function make_message(function_name, description, error, substitutions) 1752 | { 1753 | for (var p in substitutions) { 1754 | if (substitutions.hasOwnProperty(p)) { 1755 | substitutions[p] = format_value(substitutions[p]); 1756 | } 1757 | } 1758 | var node_form = substitute(["{text}", "${function_name}: ${description}" + error], 1759 | merge({function_name:function_name, 1760 | description:(description?description + " ":"")}, 1761 | substitutions)); 1762 | return node_form.slice(1).join(""); 1763 | } 1764 | 1765 | function filter(array, callable, thisObj) { 1766 | var rv = []; 1767 | for (var i = 0; i < array.length; i++) { 1768 | if (array.hasOwnProperty(i)) { 1769 | var pass = callable.call(thisObj, array[i], i, array); 1770 | if (pass) { 1771 | rv.push(array[i]); 1772 | } 1773 | } 1774 | } 1775 | return rv; 1776 | } 1777 | 1778 | function map(array, callable, thisObj) 1779 | { 1780 | var rv = []; 1781 | rv.length = array.length; 1782 | for (var i = 0; i < array.length; i++) { 1783 | if (array.hasOwnProperty(i)) { 1784 | rv[i] = callable.call(thisObj, array[i], i, array); 1785 | } 1786 | } 1787 | return rv; 1788 | } 1789 | 1790 | function extend(array, items) 1791 | { 1792 | Array.prototype.push.apply(array, items); 1793 | } 1794 | 1795 | function forEach (array, callback, thisObj) 1796 | { 1797 | for (var i = 0; i < array.length; i++) { 1798 | if (array.hasOwnProperty(i)) { 1799 | callback.call(thisObj, array[i], i, array); 1800 | } 1801 | } 1802 | } 1803 | 1804 | function merge(a,b) 1805 | { 1806 | var rv = {}; 1807 | var p; 1808 | for (p in a) { 1809 | rv[p] = a[p]; 1810 | } 1811 | for (p in b) { 1812 | rv[p] = b[p]; 1813 | } 1814 | return rv; 1815 | } 1816 | 1817 | function expose(object, name) 1818 | { 1819 | var components = name.split("."); 1820 | var target = window; 1821 | for (var i = 0; i < components.length - 1; i++) { 1822 | if (!(components[i] in target)) { 1823 | target[components[i]] = {}; 1824 | } 1825 | target = target[components[i]]; 1826 | } 1827 | target[components[components.length - 1]] = object; 1828 | } 1829 | 1830 | function forEach_windows(callback) { 1831 | // Iterate of the the windows [self ... top, opener]. The callback is passed 1832 | // two objects, the first one is the windows object itself, the second one 1833 | // is a boolean indicating whether or not its on the same origin as the 1834 | // current window. 1835 | var cache = forEach_windows.result_cache; 1836 | if (!cache) { 1837 | cache = [[self, true]]; 1838 | var w = self; 1839 | var i = 0; 1840 | var so; 1841 | var origins = location.ancestorOrigins; 1842 | while (w != w.parent) { 1843 | w = w.parent; 1844 | // In WebKit, calls to parent windows' properties that aren't on the same 1845 | // origin cause an error message to be displayed in the error console but 1846 | // don't throw an exception. This is a deviation from the current HTML5 1847 | // spec. See: https://bugs.webkit.org/show_bug.cgi?id=43504 1848 | // The problem with WebKit's behavior is that it pollutes the error console 1849 | // with error messages that can't be caught. 1850 | // 1851 | // This issue can be mitigated by relying on the (for now) proprietary 1852 | // `location.ancestorOrigins` property which returns an ordered list of 1853 | // the origins of enclosing windows. See: 1854 | // http://trac.webkit.org/changeset/113945. 1855 | if (origins) { 1856 | so = (location.origin == origins[i]); 1857 | } else { 1858 | so = is_same_origin(w); 1859 | } 1860 | cache.push([w, so]); 1861 | i++; 1862 | } 1863 | w = window.opener; 1864 | if (w) { 1865 | // window.opener isn't included in the `location.ancestorOrigins` prop. 1866 | // We'll just have to deal with a simple check and an error msg on WebKit 1867 | // browsers in this case. 1868 | cache.push([w, is_same_origin(w)]); 1869 | } 1870 | forEach_windows.result_cache = cache; 1871 | } 1872 | 1873 | forEach(cache, 1874 | function(a) 1875 | { 1876 | callback.apply(null, a); 1877 | }); 1878 | } 1879 | 1880 | function is_same_origin(w) { 1881 | try { 1882 | 'random_prop' in w; 1883 | return true; 1884 | } catch (e) { 1885 | return false; 1886 | } 1887 | } 1888 | 1889 | function supports_post_message(w) 1890 | { 1891 | var supports; 1892 | var type; 1893 | // Given IE implements postMessage across nested iframes but not across 1894 | // windows or tabs, you can't infer cross-origin communication from the presence 1895 | // of postMessage on the current window object only. 1896 | // 1897 | // Touching the postMessage prop on a window can throw if the window is 1898 | // not from the same origin AND post message is not supported in that 1899 | // browser. So just doing an existence test here won't do, you also need 1900 | // to wrap it in a try..cacth block. 1901 | try { 1902 | type = typeof w.postMessage; 1903 | if (type === "function") { 1904 | supports = true; 1905 | } 1906 | 1907 | // IE8 supports postMessage, but implements it as a host object which 1908 | // returns "object" as its `typeof`. 1909 | else if (type === "object") { 1910 | supports = true; 1911 | } 1912 | 1913 | // This is the case where postMessage isn't supported AND accessing a 1914 | // window property across origins does NOT throw (e.g. old Safari browser). 1915 | else { 1916 | supports = false; 1917 | } 1918 | } catch (e) { 1919 | // This is the case where postMessage isn't supported AND accessing a 1920 | // window property across origins throws (e.g. old Firefox browser). 1921 | supports = false; 1922 | } 1923 | return supports; 1924 | } 1925 | })(); 1926 | // vim: set expandtab shiftwidth=4 tabstop=4: 1927 | -------------------------------------------------------------------------------- /tests/web-platform-tests/resources/testharnessreport.js: -------------------------------------------------------------------------------- 1 | /*global add_completion_callback, setup */ 2 | /* 3 | * This file is intended for vendors to implement 4 | * code needed to integrate testharness.js tests with their own test systems. 5 | * 6 | * The default implementation extracts metadata from the tests and validates 7 | * it against the cached version that should be present in the test source 8 | * file. If the cache is not found or is out of sync, source code suitable for 9 | * caching the metadata is optionally generated. 10 | * 11 | * The cached metadata is present for extraction by test processing tools that 12 | * are unable to execute javascript. 13 | * 14 | * Metadata is attached to tests via the properties parameter in the test 15 | * constructor. See testharness.js for details. 16 | * 17 | * Typically test system integration will attach callbacks when each test has 18 | * run, using add_result_callback(callback(test)), or when the whole test file 19 | * has completed, using 20 | * add_completion_callback(callback(tests, harness_status)). 21 | * 22 | * For more documentation about the callback functions and the 23 | * parameters they are called with see testharness.js 24 | */ 25 | 26 | 27 | 28 | var metadata_generator = { 29 | 30 | currentMetadata: {}, 31 | cachedMetadata: false, 32 | metadataProperties: ['help', 'assert', 'author'], 33 | 34 | error: function(message) { 35 | var messageElement = document.createElement('p'); 36 | messageElement.setAttribute('class', 'error'); 37 | this.appendText(messageElement, message); 38 | 39 | var summary = document.getElementById('summary'); 40 | if (summary) { 41 | summary.parentNode.insertBefore(messageElement, summary); 42 | } 43 | else { 44 | document.body.appendChild(messageElement); 45 | } 46 | }, 47 | 48 | /** 49 | * Ensure property value has contact information 50 | */ 51 | validateContact: function(test, propertyName) { 52 | var result = true; 53 | var value = test.properties[propertyName]; 54 | var values = Array.isArray(value) ? value : [value]; 55 | for (var index = 0; index < values.length; index++) { 56 | value = values[index]; 57 | var re = /(\S+)(\s*)<(.*)>(.*)/; 58 | if (! re.test(value)) { 59 | re = /(\S+)(\s+)(http[s]?:\/\/)(.*)/; 60 | if (! re.test(value)) { 61 | this.error('Metadata property "' + propertyName + 62 | '" for test: "' + test.name + 63 | '" must have name and contact information ' + 64 | '("name " or "name http(s)://")'); 65 | result = false; 66 | } 67 | } 68 | } 69 | return result; 70 | }, 71 | 72 | /** 73 | * Extract metadata from test object 74 | */ 75 | extractFromTest: function(test) { 76 | var testMetadata = {}; 77 | // filter out metadata from other properties in test 78 | for (var metaIndex = 0; metaIndex < this.metadataProperties.length; 79 | metaIndex++) { 80 | var meta = this.metadataProperties[metaIndex]; 81 | if (test.properties.hasOwnProperty(meta)) { 82 | if ('author' == meta) { 83 | this.validateContact(test, meta); 84 | } 85 | testMetadata[meta] = test.properties[meta]; 86 | } 87 | } 88 | return testMetadata; 89 | }, 90 | 91 | /** 92 | * Compare cached metadata to extracted metadata 93 | */ 94 | validateCache: function() { 95 | for (var testName in this.currentMetadata) { 96 | if (! this.cachedMetadata.hasOwnProperty(testName)) { 97 | return false; 98 | } 99 | var testMetadata = this.currentMetadata[testName]; 100 | var cachedTestMetadata = this.cachedMetadata[testName]; 101 | delete this.cachedMetadata[testName]; 102 | 103 | for (var metaIndex = 0; metaIndex < this.metadataProperties.length; 104 | metaIndex++) { 105 | var meta = this.metadataProperties[metaIndex]; 106 | if (cachedTestMetadata.hasOwnProperty(meta) && 107 | testMetadata.hasOwnProperty(meta)) { 108 | if (Array.isArray(cachedTestMetadata[meta])) { 109 | if (! Array.isArray(testMetadata[meta])) { 110 | return false; 111 | } 112 | if (cachedTestMetadata[meta].length == 113 | testMetadata[meta].length) { 114 | for (var index = 0; 115 | index < cachedTestMetadata[meta].length; 116 | index++) { 117 | if (cachedTestMetadata[meta][index] != 118 | testMetadata[meta][index]) { 119 | return false; 120 | } 121 | } 122 | } 123 | else { 124 | return false; 125 | } 126 | } 127 | else { 128 | if (Array.isArray(testMetadata[meta])) { 129 | return false; 130 | } 131 | if (cachedTestMetadata[meta] != testMetadata[meta]) { 132 | return false; 133 | } 134 | } 135 | } 136 | else if (cachedTestMetadata.hasOwnProperty(meta) || 137 | testMetadata.hasOwnProperty(meta)) { 138 | return false; 139 | } 140 | } 141 | } 142 | for (var testName in this.cachedMetadata) { 143 | return false; 144 | } 145 | return true; 146 | }, 147 | 148 | appendText: function(elemement, text) { 149 | elemement.appendChild(document.createTextNode(text)); 150 | }, 151 | 152 | jsonifyArray: function(arrayValue, indent) { 153 | var output = '['; 154 | 155 | if (1 == arrayValue.length) { 156 | output += JSON.stringify(arrayValue[0]); 157 | } 158 | else { 159 | for (var index = 0; index < arrayValue.length; index++) { 160 | if (0 < index) { 161 | output += ',\n ' + indent; 162 | } 163 | output += JSON.stringify(arrayValue[index]); 164 | } 165 | } 166 | output += ']'; 167 | return output; 168 | }, 169 | 170 | jsonifyObject: function(objectValue, indent) { 171 | var output = '{'; 172 | var value; 173 | 174 | var count = 0; 175 | for (var property in objectValue) { 176 | ++count; 177 | if (Array.isArray(objectValue[property]) || 178 | ('object' == typeof(value))) { 179 | ++count; 180 | } 181 | } 182 | if (1 == count) { 183 | for (var property in objectValue) { 184 | output += ' "' + property + '": ' + 185 | JSON.stringify(objectValue[property]) + 186 | ' '; 187 | } 188 | } 189 | else { 190 | var first = true; 191 | for (var property in objectValue) { 192 | if (! first) { 193 | output += ','; 194 | } 195 | first = false; 196 | output += '\n ' + indent + '"' + property + '": '; 197 | value = objectValue[property]; 198 | if (Array.isArray(value)) { 199 | output += this.jsonifyArray(value, indent + 200 | ' '.substr(0, 5 + property.length)); 201 | } 202 | else if ('object' == typeof(value)) { 203 | output += this.jsonifyObject(value, indent + ' '); 204 | } 205 | else { 206 | output += JSON.stringify(value); 207 | } 208 | } 209 | if (1 < output.length) { 210 | output += '\n' + indent; 211 | } 212 | } 213 | output += '}'; 214 | return output; 215 | }, 216 | 217 | /** 218 | * Generate javascript source code for captured metadata 219 | * Metadata is in pretty-printed JSON format 220 | */ 221 | generateSource: function() { 222 | var source = 223 | '\n'; 226 | return source; 227 | }, 228 | 229 | /** 230 | * Add element containing metadata source code 231 | */ 232 | addSourceElement: function(event) { 233 | var sourceWrapper = document.createElement('div'); 234 | sourceWrapper.setAttribute('id', 'metadata_source'); 235 | 236 | var instructions = document.createElement('p'); 237 | if (this.cachedMetadata) { 238 | this.appendText(instructions, 239 | 'Replace the existing 22 | 23 | 24 | 25 | 26 | 173 | 183 | 184 | 185 |

Touch Events: createTouch and createTouchList tests

186 |
Please wait for test to complete...
187 |
188 | 189 | 190 | -------------------------------------------------------------------------------- /tests/web-platform-tests/touch-events/multi-touch-interactions-manual.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 18 | Touch Events Multi-Touch Interaction Test 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 41 | 42 | 43 |

Touch Events: Multi-Touch Interaction Test

44 |
45 | Touch this box with one finger, then another one... 46 |
47 |
48 | ...then drag to this box, then touch with a third finger, and lift all your fingers. 49 |
50 |
51 |
52 | 53 | 54 | -------------------------------------------------------------------------------- /tests/web-platform-tests/touch-events/multi-touch-interactions.js: -------------------------------------------------------------------------------- 1 | setup({explicit_done: true}); 2 | 3 | var debug = document.getElementById("debug"); 4 | 5 | function debug_print (x) { 6 | /* uncomment below statement to show debug messages */ 7 | // document.getElementById("debug").innerHTML += x; 8 | } 9 | 10 | var starting_elements = {}; 11 | 12 | function check_list_subset_of_targetlist(list, list_name, targetlist, targetlist_name) { 13 | var exist_in_targetlist; 14 | for(i=0; i= 1, "changedTouches.length is at least 1"); 110 | assert_true(ev.changedTouches.length <= ev.touches.length, "changedTouches.length is smaller than touches.length"); 111 | check_list_subset_of_targetlist(ev.changedTouches, "changedTouches", ev.touches, "touches"); 112 | }, "touchstart #" + touchstart_received + ": changedTouches is a subset of touches"); 113 | 114 | // TA: 1.3.3.2, 1.3.3.3 115 | test(function() { 116 | assert_true(ev.targetTouches.length >= 1, "targetTouches.length is at least 1"); 117 | assert_true(ev.targetTouches.length <= ev.touches.length, "targetTouches.length is smaller than touches.length"); 118 | check_list_subset_of_targetlist(ev.targetTouches, "targetTouches", ev.touches, "touches"); 119 | }, "touchstart #" + touchstart_received + ": targetTouches is a subset of touches"); 120 | 121 | // TA: 1.3.3.9 122 | test(function() { 123 | check_targets(ev.targetTouches, ev.target); 124 | }, "touchstart #" + touchstart_received + ": targets of targetTouches are correct"); 125 | 126 | // TA: 1.3.4.2 127 | test(function() { 128 | assert_true(ev.touches.length >= 1, "touches.length is at least 1"); 129 | }, "touchstart #" + touchstart_received + ": touches.length is valid"); 130 | 131 | if(touchstart_received == 1) { 132 | // TA: 1.3.3.5, 1.3.3.7 133 | test(function() { 134 | assert_true(ev.targetTouches.length <= ev.changedTouches.length, "targetTouches.length is smaller than changedTouches.length"); 135 | check_list_subset_of_targetlist(ev.targetTouches, "targetTouches", ev.changedTouches, "changedTouches"); 136 | }, "touchstart #" + touchstart_received + ": targetTouches is a subset of changedTouches"); 137 | 138 | // TA: 1.3.4.3 139 | test(function() { 140 | assert_true(ev.touches.length==ev.changedTouches.length, "touches and changedTouches have the same length"); 141 | }, "touchstart #" + touchstart_received + ": touches and changedTouches have the same length"); 142 | } else { 143 | // TA: 1.3.3.6 144 | test(function() { 145 | var diff_in_targetTouches = ev.targetTouches.length - (last_targetTouches[ev.target.id] ? last_targetTouches[ev.target.id].length : 0); 146 | assert_true(diff_in_targetTouches > 0, "targetTouches.length is larger than last received targetTouches.length"); 147 | assert_true(diff_in_targetTouches <= ev.changedTouches.length, "change in targetTouches.length is smaller than changedTouches.length"); 148 | }, "touchstart #" + touchstart_received + ": change in targetTouches.length is valid"); 149 | 150 | // TA: 1.3.3.8 151 | test(function() { 152 | assert_true(is_at_least_one_item_in_targetlist(ev.targetTouches, ev.changedTouches), "at least one item of targetTouches is in changedTouches"); 153 | }, "touchstart #" + touchstart_received + ": at least one targetTouches item in changedTouches"); 154 | 155 | // TA: 1.3.4.4 156 | test(function() { 157 | var diff_in_touches = ev.touches.length - last_touches.length; 158 | assert_true(diff_in_touches > 0, "touches.length is larger than last received touches.length"); 159 | assert_true(diff_in_touches == ev.changedTouches.length, "change in touches.length equals changedTouches.length"); 160 | }, "touchstart #" + touchstart_received + ": change in touches.length is valid"); 161 | 162 | // TA: 1.3.4.5 163 | test(function() { 164 | check_list_subset_of_two_targetlists(ev.touches, "touches", ev.changedTouches, "changedTouches", last_touches, "last touches"); 165 | }, "touchstart #" + touchstart_received + ": touches is subset of {changedTouches, last received touches}"); 166 | } 167 | 168 | // save starting element of each new touch point 169 | for (i=0; i0, "touchmove follows touchstart"); 188 | // assert_false(touchend_received, "touchmove precedes touchend"); // this applies to scenario tests 189 | }); 190 | test_touchmove.done(); 191 | 192 | touchmove_received++; 193 | 194 | // do the detailed checking only for a few times 195 | if(touchmove_received<6) { 196 | // TA: 1.4.2.2, 1.4.2.4 197 | test(function() { 198 | assert_true(ev.changedTouches.length >= 1, "changedTouches.length is at least 1"); 199 | assert_true(ev.changedTouches.length <= ev.touches.length, "changedTouches.length is smaller than touches.length"); 200 | check_list_subset_of_targetlist(ev.changedTouches, "changedTouches", ev.touches, "touches"); 201 | }, "touchmove #" + touchmove_received + ": changedTouches is a subset of touches"); 202 | 203 | // TA: 1.4.3.2, 1.4.3.4 204 | test(function() { 205 | assert_true(ev.targetTouches.length >= 1, "targetTouches.length is at least 1"); 206 | assert_true(ev.targetTouches.length <= ev.touches.length, "targetTouches.length is smaller than touches.length"); 207 | check_list_subset_of_targetlist(ev.targetTouches, "targetTouches", ev.touches, "touches"); 208 | }, "touchmove #" + touchmove_received + ": targetTouches is a subset of touches"); 209 | 210 | // TA: 1.4.3.6 211 | test(function() { 212 | assert_true(is_at_least_one_item_in_targetlist(ev.targetTouches, ev.changedTouches), "at least one item of targetTouches is in changedTouches"); 213 | }, "touchmove #" + touchmove_received + ": at least one targetTouches item in changedTouches"); 214 | 215 | // TA: 1.4.3.8 216 | test(function() { 217 | check_targets(ev.targetTouches, ev.target); 218 | }, "touchmove #" + touchmove_received + ": targets of targetTouches are correct"); 219 | 220 | // TA: 1.4.4.2 221 | test(function() { 222 | assert_true(ev.touches.length==last_touches.length, "length of touches is same as length of last received touches"); 223 | check_list_subset_of_targetlist(ev.touches, "touches", last_touches, "last received touches"); 224 | }, "touchmove #" + touchmove_received + ": touches must be same as last received touches"); 225 | 226 | // TA: 1.6.3 227 | check_starting_elements(ev.changedTouches); 228 | } 229 | 230 | last_touches = ev.touches; 231 | last_targetTouches[ev.target.id] = ev.targetTouches; 232 | last_changedTouches = {}; // changedTouches are only saved for touchend events 233 | }); 234 | 235 | on_event(window, "touchend", function onTouchEnd(ev) { 236 | // process event only if it's targeted at target0 or target1 237 | if(ev.target != target0 && ev.target != target1 ) 238 | return; 239 | 240 | test_touchend.step(function() { 241 | assert_true(touchstart_received>0, "touchend follows touchstart"); 242 | }); 243 | test_touchend.done(); 244 | 245 | touchend_received++; 246 | 247 | debug_print("touchend #" + touchend_received + ":
"); 248 | debug_print("changedTouches.length=" + ev.changedTouches.length + "
"); 249 | debug_print("targetTouches.length=" + ev.targetTouches.length + "
"); 250 | debug_print("touches.length=" + ev.touches.length + "
"); 251 | for(i=0; i"); 253 | 254 | // TA: 1.5.2.2 255 | test(function() { 256 | assert_true(ev.changedTouches.length >= 1, "changedTouches.length is at least 1"); 257 | }, "touchend #" + touchend_received + ": length of changedTouches is valid"); 258 | 259 | // TA: 1.5.2.3 260 | test(function() { 261 | check_list_subset_of_targetlist(ev.changedTouches, "changedTouches", last_touches, "last received touches"); 262 | }, "touchend #" + touchend_received + ": changedTouches is a subset of last received touches"); 263 | 264 | // TA: 1.5.2.4, 1.5.2.5 265 | test(function() { 266 | check_no_item_in_targetlist(ev.changedTouches, "changedTouches", ev.touches, "touches"); 267 | check_no_item_in_targetlist(ev.changedTouches, "changedTouches", ev.targetTouches, "targetTouches"); 268 | }, "touchend #" + touchend_received + ": no item in changedTouches are in touches or targetTouches"); 269 | 270 | // TA: 1.5.2.6 271 | test(function() { 272 | var found=false; 273 | for (i=0; i= 0, "targetTouches.length is non-negative"); 282 | assert_true(ev.targetTouches.length <= ev.touches.length, "targetTouches.length is smaller than touches.length"); 283 | check_list_subset_of_targetlist(ev.targetTouches, "targetTouches", ev.touches, "touches"); 284 | }, "touchend #" + touchend_received + ": targetTouches is a subset of touches"); 285 | 286 | // TA: 1.5.3.5 (new) 287 | test(function() { 288 | check_targets(ev.targetTouches, ev.target); 289 | }, "touchend #" + touchend_received + ": targets of targetTouches are correct"); 290 | 291 | // In some cases, when multiple touch points are released simultaneously 292 | // the UA would dispatch the "same" touchend event (same changedTouches, same touches, but possibly different targetTouches) 293 | // to each of the elements that are starting elments of the released touch points. 294 | // in these situations, the subsequent events are exempt from TA 1.5.3.4 and 1.5.4.2 295 | var same_event_as_last = false; 296 | if (last_changedTouches && last_changedTouches.length==ev.changedTouches.length) { 297 | same_event_as_last = true; // assume true until proven otherwise 298 | for (i=0; i 0, "last received targetTouches.length is not zero"); 316 | var diff_in_targetTouches = last_targetTouches[ev.target.id].length - ev.targetTouches.length; 317 | debug_print("diff_in_targetTouches=" + diff_in_targetTouches + "
"); 318 | assert_true(diff_in_targetTouches > 0, "targetTouches.length is smaller than last received targetTouches.length"); 319 | assert_true(diff_in_targetTouches <= ev.changedTouches.length, "change in targetTouches.length is smaller than changedTouches.length"); 320 | }, "touchend #" + touchend_received + ": change in targetTouches.length is valid"); 321 | 322 | // TA: 1.5.4.2 323 | // Getting semi-random failures on this and 1.5.3.4. 324 | // It looks like if fingers are lifted simultaneously, the "same" touchend event can be dispatched to two target elements 325 | // but adapted to the element (same touches, changedTouches but different targetTouches). 326 | // When one event is processed after another, ev.touches would end up being identical to last_touches, leading to failure. 327 | // Question is why done() does not stop the processing of the latter event. 328 | test(function() { 329 | assert_true(last_touches.length > 0, "last received touches.length is not zero"); 330 | var diff_in_touches = last_touches.length - ev.touches.length; 331 | debug_print("diff_in_touches=" + diff_in_touches + "
"); 332 | assert_true(diff_in_touches > 0, "touches.length is smaller than last received touches.length"); 333 | assert_equals(diff_in_touches, ev.changedTouches.length, "change in touches.length equals changedTouches.length"); 334 | }, "touchend #" + touchend_received + ": change in touches.length is valid"); 335 | } 336 | 337 | // TA: 1.6.4 338 | debug_print("touchend #" + touchend_received + ": TA 1.6.4
"); 339 | test(function() { 340 | check_starting_elements(ev.changedTouches); 341 | }, "touchend #" + touchend_received + ": event dispatched to correct element
"); 342 | 343 | debug_print("touchend #" + touchend_received + ": saving touch lists
"); 344 | 345 | last_touches = ev.touches; 346 | last_targetTouches[ev.target.id] = ev.targetTouches; 347 | last_changedTouches = ev.changedTouches; 348 | 349 | debug_print("touchend #" + touchend_received + ": done
"); 350 | if(ev.touches.length==0) 351 | done(); 352 | }); 353 | 354 | on_event(target0, "mousedown", function onMouseDown(ev) { 355 | test_mousedown.step(function() { 356 | assert_true(touchstart_received, 357 | "The touchstart event must be dispatched before any mouse " + 358 | "events. (If this fails, it might mean that the user agent does " + 359 | "not implement W3C touch events at all.)" 360 | ); 361 | }); 362 | test_mousedown.done(); 363 | 364 | if (!touchstart_received) { 365 | // Abort the tests. If touch events are not supported, then most of 366 | // the other event handlers will never be called, and the test will 367 | // time out with misleading results. 368 | done(); 369 | } 370 | }); 371 | } 372 | -------------------------------------------------------------------------------- /tests/web-platform-tests/touch-events/multi-touch-interfaces-manual.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 19 | Touch Events Multi-Touch Interface Tests 20 | 21 | 22 | 23 | 24 | 25 | 26 | 252 | 266 | 267 | 268 |

Touch Events: multi-touch interface tests

269 |
270 | Touch this box with one finger, then another one... 271 |
272 |
273 | ...then drag to this box and lift your fingers. 274 |
275 |
276 | 277 | 278 | -------------------------------------------------------------------------------- /tests/web-platform-tests/touch-events/single-touch-manual.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 | Touch Events Single Touch Tests 16 | 17 | 18 | 19 | 20 | 21 | 22 | 357 | 371 | 372 | 373 |

Touch Events: single-touch tests

374 |
375 | Touch this box with one finger (or other pointing device)... 376 |
377 |
378 | ...then drag to this box and lift your finger. 379 |
380 |
381 | 382 | 383 | -------------------------------------------------------------------------------- /touch-emulator.js: -------------------------------------------------------------------------------- 1 | (function(window, document, exportName, undefined) { 2 | "use strict"; 3 | 4 | var isMultiTouch = false; 5 | var multiTouchStartPos; 6 | var eventTarget; 7 | var touchElements = {}; 8 | 9 | // polyfills 10 | if(!document.createTouch) { 11 | document.createTouch = function(view, target, identifier, pageX, pageY, screenX, screenY, clientX, clientY) { 12 | // auto set 13 | if(clientX == undefined || clientY == undefined) { 14 | clientX = pageX - window.pageXOffset; 15 | clientY = pageY - window.pageYOffset; 16 | } 17 | 18 | return new Touch(target, identifier, { 19 | pageX: pageX, 20 | pageY: pageY, 21 | screenX: screenX, 22 | screenY: screenY, 23 | clientX: clientX, 24 | clientY: clientY 25 | }); 26 | }; 27 | } 28 | 29 | if(!document.createTouchList) { 30 | document.createTouchList = function() { 31 | var touchList = new TouchList(); 32 | for (var i = 0; i < arguments.length; i++) { 33 | touchList[i] = arguments[i]; 34 | } 35 | touchList.length = arguments.length; 36 | return touchList; 37 | }; 38 | } 39 | 40 | /** 41 | * create an touch point 42 | * @constructor 43 | * @param target 44 | * @param identifier 45 | * @param pos 46 | * @param deltaX 47 | * @param deltaY 48 | * @returns {Object} touchPoint 49 | */ 50 | function Touch(target, identifier, pos, deltaX, deltaY) { 51 | deltaX = deltaX || 0; 52 | deltaY = deltaY || 0; 53 | 54 | this.identifier = identifier; 55 | this.target = target; 56 | this.clientX = pos.clientX + deltaX; 57 | this.clientY = pos.clientY + deltaY; 58 | this.screenX = pos.screenX + deltaX; 59 | this.screenY = pos.screenY + deltaY; 60 | this.pageX = pos.pageX + deltaX; 61 | this.pageY = pos.pageY + deltaY; 62 | } 63 | 64 | /** 65 | * create empty touchlist with the methods 66 | * @constructor 67 | * @returns touchList 68 | */ 69 | function TouchList() { 70 | var touchList = []; 71 | 72 | touchList.item = function(index) { 73 | return this[index] || null; 74 | }; 75 | 76 | // specified by Mozilla 77 | touchList.identifiedTouch = function(id) { 78 | return this[id + 1] || null; 79 | }; 80 | 81 | return touchList; 82 | } 83 | 84 | 85 | /** 86 | * Simple trick to fake touch event support 87 | * this is enough for most libraries like Modernizr and Hammer 88 | */ 89 | function fakeTouchSupport() { 90 | var objs = [window, document.documentElement]; 91 | var props = ['ontouchstart', 'ontouchmove', 'ontouchcancel', 'ontouchend']; 92 | 93 | for(var o=0; o 2; // pointer events 110 | } 111 | 112 | /** 113 | * disable mouseevents on the page 114 | * @param ev 115 | */ 116 | function preventMouseEvents(ev) { 117 | ev.preventDefault(); 118 | ev.stopPropagation(); 119 | } 120 | 121 | /** 122 | * only trigger touches when the left mousebutton has been pressed 123 | * @param touchType 124 | * @returns {Function} 125 | */ 126 | function onMouse(touchType) { 127 | return function(ev) { 128 | if (TouchEmulator.ignoreTags.indexOf(ev.target.tagName) < 0) { 129 | // prevent mouse events 130 | preventMouseEvents(ev); 131 | } 132 | 133 | if (ev.which !== 1) { 134 | return; 135 | } 136 | 137 | // The EventTarget on which the touch point started when it was first placed on the surface, 138 | // even if the touch point has since moved outside the interactive area of that element. 139 | // also, when the target doesnt exist anymore, we update it 140 | if (ev.type == 'mousedown' || !eventTarget || (eventTarget && !eventTarget.dispatchEvent)) { 141 | if(ev.composedPath() && ev.composedPath().length > 0){ 142 | eventTarget = ev.composedPath()[0] 143 | }else { 144 | eventTarget = ev.target; 145 | } 146 | } 147 | 148 | // shiftKey has been lost, so trigger a touchend 149 | if (isMultiTouch && !ev.shiftKey) { 150 | triggerTouch('touchend', ev); 151 | isMultiTouch = false; 152 | } 153 | 154 | triggerTouch(touchType, ev); 155 | 156 | // we're entering the multi-touch mode! 157 | if (!isMultiTouch && ev.shiftKey) { 158 | isMultiTouch = true; 159 | multiTouchStartPos = { 160 | pageX: ev.pageX, 161 | pageY: ev.pageY, 162 | clientX: ev.clientX, 163 | clientY: ev.clientY, 164 | screenX: ev.screenX, 165 | screenY: ev.screenY 166 | }; 167 | triggerTouch('touchstart', ev); 168 | } 169 | 170 | // reset 171 | if (ev.type == 'mouseup') { 172 | multiTouchStartPos = null; 173 | isMultiTouch = false; 174 | eventTarget = null; 175 | } 176 | } 177 | } 178 | 179 | /** 180 | * trigger a touch event 181 | * @param eventName 182 | * @param mouseEv 183 | */ 184 | function triggerTouch(eventName, mouseEv) { 185 | var touchEvent = document.createEvent('Event'); 186 | touchEvent.initEvent(eventName, true, true); 187 | 188 | touchEvent.altKey = mouseEv.altKey; 189 | touchEvent.ctrlKey = mouseEv.ctrlKey; 190 | touchEvent.metaKey = mouseEv.metaKey; 191 | touchEvent.shiftKey = mouseEv.shiftKey; 192 | 193 | touchEvent.touches = getActiveTouches(mouseEv, eventName); 194 | touchEvent.targetTouches = getActiveTouches(mouseEv, eventName); 195 | touchEvent.changedTouches = getChangedTouches(mouseEv, eventName); 196 | 197 | eventTarget.dispatchEvent(touchEvent); 198 | } 199 | 200 | /** 201 | * create a touchList based on the mouse event 202 | * @param mouseEv 203 | * @returns {TouchList} 204 | */ 205 | function createTouchList(mouseEv) { 206 | var touchList = new TouchList(); 207 | 208 | if (isMultiTouch) { 209 | var f = TouchEmulator.multiTouchOffset; 210 | var deltaX = multiTouchStartPos.pageX - mouseEv.pageX; 211 | var deltaY = multiTouchStartPos.pageY - mouseEv.pageY; 212 | 213 | touchList.push(new Touch(eventTarget, 1, multiTouchStartPos, (deltaX*-1) - f, (deltaY*-1) + f)); 214 | touchList.push(new Touch(eventTarget, 2, multiTouchStartPos, deltaX+f, deltaY-f)); 215 | } else { 216 | touchList.push(new Touch(eventTarget, 1, mouseEv, 0, 0)); 217 | } 218 | 219 | return touchList; 220 | } 221 | 222 | /** 223 | * receive all active touches 224 | * @param mouseEv 225 | * @returns {TouchList} 226 | */ 227 | function getActiveTouches(mouseEv, eventName) { 228 | // empty list 229 | if (mouseEv.type == 'mouseup') { 230 | return new TouchList(); 231 | } 232 | 233 | var touchList = createTouchList(mouseEv); 234 | if(isMultiTouch && mouseEv.type != 'mouseup' && eventName == 'touchend') { 235 | touchList.splice(1, 1); 236 | } 237 | return touchList; 238 | } 239 | 240 | /** 241 | * receive a filtered set of touches with only the changed pointers 242 | * @param mouseEv 243 | * @param eventName 244 | * @returns {TouchList} 245 | */ 246 | function getChangedTouches(mouseEv, eventName) { 247 | var touchList = createTouchList(mouseEv); 248 | 249 | // we only want to return the added/removed item on multitouch 250 | // which is the second pointer, so remove the first pointer from the touchList 251 | // 252 | // but when the mouseEv.type is mouseup, we want to send all touches because then 253 | // no new input will be possible 254 | if(isMultiTouch && mouseEv.type != 'mouseup' && 255 | (eventName == 'touchstart' || eventName == 'touchend')) { 256 | touchList.splice(0, 1); 257 | } 258 | 259 | return touchList; 260 | } 261 | 262 | /** 263 | * show the touchpoints on the screen 264 | */ 265 | function showTouches(ev) { 266 | var touch, i, el, styles; 267 | 268 | // first all visible touches 269 | for(i = 0; i < ev.touches.length; i++) { 270 | touch = ev.touches[i]; 271 | el = touchElements[touch.identifier]; 272 | if(!el) { 273 | el = touchElements[touch.identifier] = document.createElement("div"); 274 | document.body.appendChild(el); 275 | } 276 | 277 | styles = TouchEmulator.template(touch); 278 | for(var prop in styles) { 279 | el.style[prop] = styles[prop]; 280 | } 281 | } 282 | 283 | // remove all ended touches 284 | if(ev.type == 'touchend' || ev.type == 'touchcancel') { 285 | for(i = 0; i < ev.changedTouches.length; i++) { 286 | touch = ev.changedTouches[i]; 287 | el = touchElements[touch.identifier]; 288 | if(el) { 289 | el.parentNode.removeChild(el); 290 | delete touchElements[touch.identifier]; 291 | } 292 | } 293 | } 294 | } 295 | 296 | /** 297 | * TouchEmulator initializer 298 | */ 299 | function TouchEmulator() { 300 | if (hasTouchSupport()) { 301 | return; 302 | } 303 | 304 | fakeTouchSupport(); 305 | 306 | window.addEventListener("mousedown", onMouse('touchstart'), true); 307 | window.addEventListener("mousemove", onMouse('touchmove'), true); 308 | window.addEventListener("mouseup", onMouse('touchend'), true); 309 | 310 | window.addEventListener("mouseenter", preventMouseEvents, true); 311 | window.addEventListener("mouseleave", preventMouseEvents, true); 312 | window.addEventListener("mouseout", preventMouseEvents, true); 313 | window.addEventListener("mouseover", preventMouseEvents, true); 314 | 315 | // it uses itself! 316 | window.addEventListener("touchstart", showTouches, true); 317 | window.addEventListener("touchmove", showTouches, true); 318 | window.addEventListener("touchend", showTouches, true); 319 | window.addEventListener("touchcancel", showTouches, true); 320 | } 321 | 322 | // start distance when entering the multitouch mode 323 | TouchEmulator.multiTouchOffset = 75; 324 | 325 | // tags that shouldn't swallow mouse events 326 | TouchEmulator.ignoreTags = ['TEXTAREA', 'INPUT', 'SELECT']; 327 | 328 | /** 329 | * css template for the touch rendering 330 | * @param touch 331 | * @returns object 332 | */ 333 | TouchEmulator.template = function(touch) { 334 | var size = 30; 335 | var transform = 'translate('+ (touch.clientX-(size/2)) +'px, '+ (touch.clientY-(size/2)) +'px)'; 336 | return { 337 | position: 'fixed', 338 | left: 0, 339 | top: 0, 340 | background: '#fff', 341 | border: 'solid 1px #999', 342 | opacity: .6, 343 | borderRadius: '100%', 344 | height: size + 'px', 345 | width: size + 'px', 346 | padding: 0, 347 | margin: 0, 348 | display: 'block', 349 | overflow: 'hidden', 350 | pointerEvents: 'none', 351 | webkitUserSelect: 'none', 352 | mozUserSelect: 'none', 353 | userSelect: 'none', 354 | webkitTransform: transform, 355 | mozTransform: transform, 356 | transform: transform, 357 | zIndex: 100 358 | } 359 | }; 360 | 361 | // export 362 | if (typeof define == "function" && define.amd) { 363 | define(function() { 364 | return TouchEmulator; 365 | }); 366 | } else if (typeof module != "undefined" && module.exports) { 367 | module.exports = TouchEmulator; 368 | } else { 369 | window[exportName] = TouchEmulator; 370 | } 371 | })(window, document, "TouchEmulator"); 372 | --------------------------------------------------------------------------------