",
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 + "" + val.localName + ">";
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(/Details" +
1529 | "Result Test 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 |