summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/touch-events/multi-touch-interactions.js
blob: 5248e6a9430e44b6dfe4a32012ab347a3209b5fc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
setup({explicit_done: true});

var debug = document.getElementById("debug");

function debug_print (x) {
/* uncomment below statement to show debug messages */
//	document.getElementById("debug").innerHTML += x;
}

var starting_elements = {};

function check_list_subset_of_targetlist(list, list_name, targetlist, targetlist_name) {
	var exist_in_targetlist;
	for(i=0; i<list.length; i++) {
		exist_in_targetlist=false;
		for(j=0; j<targetlist.length; j++)
			if(list.item(i).identifier==targetlist.item(j).identifier)
				exist_in_targetlist=true;

		assert_true(exist_in_targetlist, list_name + ".item("+i+") exists in " + targetlist_name);
	}
}

function check_list_subset_of_two_targetlists(list, list_name, targetlist1, targetlist1_name, targetlist2, targetlist2_name) {
	var exist_in_targetlists;
	for(i=0; i<list.length; i++) {
		exist_in_targetlists=false;
		for(j=0; j<targetlist1.length; j++)
			if(list.item(i).identifier==targetlist1.item(j).identifier)
				exist_in_targetlists=true;

		if(!exist_in_targetlists)
			for(j=0; j<targetlist2.length; j++)
				if(list.item(i).identifier==targetlist2.item(j).identifier)
					exist_in_targetlists=true;

		assert_true(exist_in_targetlists, list_name + ".item("+i+") exists in " + targetlist1_name + " or " + targetlist2_name);
	}

}

function is_at_least_one_item_in_targetlist(list, targetlist) {
	for(i=0; i<list.length; i++)
		for(j=0; j<targetlist.length; j++)
			if(list.item(i).identifier==targetlist.item(j).identifier)
				return true;

	return false;
}

function check_no_item_in_targetlist(list, list_name, targetlist, targetlist_name) {
	for(i=0; i<list.length; i++)
		for(j=0; j<targetlist.length; j++) {
			assert_false(list.item(i).identifier==targetlist.item(j).identifier, list_name + ".item("+i+") exists in " + targetlist_name);
			return;
		}
}

function check_targets(list, target) {
	for(i=0; i<list.length; i++)
		assert_true(list.item(i).target==target, "item(" + i + ").target is element receiving event");
}

function check_starting_elements(list) {
	for (i=0; i<list.length; i++) {
		assert_equals(list.item(i).target, starting_elements[list.item(i).identifier], "item(" + i + ").target matches starting element");
	}
}

function run() {
	var target0 = document.getElementById("target0");
	var target1 = document.getElementById("target1");

	var test_touchstart = async_test("touchstart event received");
	var test_touchmove = async_test("touchmove event received");
	var test_touchend = async_test("touchend event received");
	var test_mousedown = async_test("Interaction with mouse events");

	var touchstart_received = 0;
	var touchmove_received = 0;
	var touchend_received = 0;
	var touchstart_identifier;

	// last received touch lists for comparison
	var last_touches;
	var last_targetTouches={};
	var last_changedTouches={};

	on_event(window, "touchstart", function onTouchStart(ev) {
		// process event only if it's targeted at target0 or target1
		if(ev.target != target0 && ev.target != target1 )
			return;

		ev.preventDefault();

		if(!touchstart_received) {
			// Check event ordering TA: 1.6.1
			test_touchstart.step(function() {
				assert_true(touchmove_received==0, "touchstart precedes touchmove");
				assert_true(touchend_received==0, "touchstart precedes touchend");
			});
			test_touchstart.done();
			test_mousedown.done(); // If we got here, then the mouse event test is not needed.
		}
		touchstart_received++;

		// TA: 1.3.2.2, 1.3.2.4
		test(function() {
			assert_true(ev.changedTouches.length >= 1, "changedTouches.length is at least 1");
			assert_true(ev.changedTouches.length <= ev.touches.length, "changedTouches.length is smaller than touches.length");
			check_list_subset_of_targetlist(ev.changedTouches, "changedTouches", ev.touches, "touches");
		}, "touchstart #" + touchstart_received + ": changedTouches is a subset of touches");

		// TA: 1.3.3.2, 1.3.3.3
		test(function() {
			assert_true(ev.targetTouches.length >= 1, "targetTouches.length is at least 1");
			assert_true(ev.targetTouches.length <= ev.touches.length, "targetTouches.length is smaller than touches.length");
			check_list_subset_of_targetlist(ev.targetTouches, "targetTouches", ev.touches, "touches");
		}, "touchstart #" + touchstart_received + ": targetTouches is a subset of touches");

		// TA: 1.3.3.9
		test(function() {
			check_targets(ev.targetTouches, ev.target);
		}, "touchstart #" + touchstart_received + ": targets of targetTouches are correct");

		// TA: 1.3.4.2
		test(function() {
			assert_true(ev.touches.length >= 1, "touches.length is at least 1");
		}, "touchstart #" + touchstart_received + ": touches.length is valid");

		if(touchstart_received == 1) {
			// TA: 1.3.3.5, 1.3.3.7
			test(function() {
				assert_true(ev.targetTouches.length <= ev.changedTouches.length, "targetTouches.length is smaller than changedTouches.length");
				check_list_subset_of_targetlist(ev.targetTouches, "targetTouches", ev.changedTouches, "changedTouches");
			}, "touchstart #" + touchstart_received + ": targetTouches is a subset of changedTouches");

			// TA: 1.3.4.3
			test(function() {
				assert_true(ev.touches.length==ev.changedTouches.length, "touches and changedTouches have the same length");
			}, "touchstart #" + touchstart_received + ": touches and changedTouches have the same length");
		} else {
			// TA: 1.3.3.6
			test(function() {
				var diff_in_targetTouches = ev.targetTouches.length - (last_targetTouches[ev.target.id] ? last_targetTouches[ev.target.id].length : 0);
				assert_true(diff_in_targetTouches > 0, "targetTouches.length is larger than last received targetTouches.length");
				assert_true(diff_in_targetTouches <= ev.changedTouches.length, "change in targetTouches.length is smaller than changedTouches.length");
			}, "touchstart #" + touchstart_received + ": change in targetTouches.length is valid");

			// TA: 1.3.3.8
			test(function() {
				assert_true(is_at_least_one_item_in_targetlist(ev.targetTouches, ev.changedTouches), "at least one item of targetTouches is in changedTouches");
			}, "touchstart #" + touchstart_received + ": at least one targetTouches item in changedTouches");

			// TA: 1.3.4.4
			test(function() {
				var diff_in_touches = ev.touches.length - last_touches.length;
				assert_true(diff_in_touches > 0, "touches.length is larger than last received touches.length");
				assert_true(diff_in_touches == ev.changedTouches.length, "change in touches.length equals changedTouches.length");
			}, "touchstart #" + touchstart_received + ": change in touches.length is valid");

			// TA: 1.3.4.5
			test(function() {
				check_list_subset_of_two_targetlists(ev.touches, "touches", ev.changedTouches, "changedTouches", last_touches, "last touches");
			}, "touchstart #" + touchstart_received + ": touches is subset of {changedTouches, last received touches}");
		}

		// save starting element of each new touch point
		for (i=0; i<ev.changedTouches.length; i++) {
			starting_elements[ev.changedTouches.item(i).identifier] = ev.changedTouches.item(i).target;
		}

		last_touches = ev.touches;
		last_targetTouches[ev.target.id] = ev.targetTouches;
		last_changedTouches = {};	// changedTouches are only saved for touchend events
	});

	on_event(window, "touchmove", function onTouchMove(ev) {
		// process event only if it's targeted at target0 or target1
		if(ev.target != target0 && ev.target != target1 )
			return;

		ev.preventDefault();

		// TA: 1.6.1
		test_touchmove.step(function() {
			assert_true(touchstart_received>0, "touchmove follows touchstart");
			// assert_false(touchend_received, "touchmove precedes touchend"); // this applies to scenario tests
		});
		test_touchmove.done();

		touchmove_received++;

		// do the detailed checking only for a few times
		if(touchmove_received<6) {
			// TA: 1.4.2.2, 1.4.2.4
			test(function() {
				assert_true(ev.changedTouches.length >= 1, "changedTouches.length is at least 1");
				assert_true(ev.changedTouches.length <= ev.touches.length, "changedTouches.length is smaller than touches.length");
				check_list_subset_of_targetlist(ev.changedTouches, "changedTouches", ev.touches, "touches");
			}, "touchmove #" + touchmove_received + ": changedTouches is a subset of touches");

			// TA: 1.4.3.2, 1.4.3.4
			test(function() {
				assert_true(ev.targetTouches.length >= 1, "targetTouches.length is at least 1");
				assert_true(ev.targetTouches.length <= ev.touches.length, "targetTouches.length is smaller than touches.length");
				check_list_subset_of_targetlist(ev.targetTouches, "targetTouches", ev.touches, "touches");
			}, "touchmove #" + touchmove_received + ": targetTouches is a subset of touches");

			// TA: 1.4.3.6
			test(function() {
				assert_true(is_at_least_one_item_in_targetlist(ev.targetTouches, ev.changedTouches), "at least one item of targetTouches is in changedTouches");
			}, "touchmove #" + touchmove_received + ": at least one targetTouches item in changedTouches");

			// TA: 1.4.3.8
			test(function() {
				check_targets(ev.targetTouches, ev.target);
			}, "touchmove #" + touchmove_received + ": targets of targetTouches are correct");

			// TA: 1.4.4.2
			test(function() {
				assert_true(ev.touches.length==last_touches.length, "length of touches is same as length of last received touches");
				check_list_subset_of_targetlist(ev.touches, "touches", last_touches, "last received touches");
			}, "touchmove #" + touchmove_received + ": touches must be same as last received touches");

			// TA: 1.6.3
			check_starting_elements(ev.changedTouches);
		}

		last_touches = ev.touches;
		last_targetTouches[ev.target.id] = ev.targetTouches;
		last_changedTouches = {};	// changedTouches are only saved for touchend events
	});

	on_event(window, "touchend", function onTouchEnd(ev) {
		// process event only if it's targeted at target0 or target1
		if(ev.target != target0 && ev.target != target1 )
			return;

		test_touchend.step(function() {
			assert_true(touchstart_received>0, "touchend follows touchstart");
		});
		test_touchend.done();

		touchend_received++;

		debug_print("touchend #" + touchend_received + ":<br>");
		debug_print("changedTouches.length=" + ev.changedTouches.length + "<br>");
		debug_print("targetTouches.length=" + ev.targetTouches.length + "<br>");
		debug_print("touches.length=" + ev.touches.length + "<br>");
		for(i=0; i<ev.changedTouches.length; i++)
			debug_print("changedTouches.item(" + i + ").target=" + ev.changedTouches.item(i).target.id + "<br>");

		// TA: 1.5.2.2
		test(function() {
			assert_true(ev.changedTouches.length >= 1, "changedTouches.length is at least 1");
		}, "touchend #" + touchend_received + ": length of changedTouches is valid");

		// TA: 1.5.2.3
		test(function() {
			check_list_subset_of_targetlist(ev.changedTouches, "changedTouches", last_touches, "last received touches");
		}, "touchend #" + touchend_received + ": changedTouches is a subset of last received touches");

		// TA: 1.5.2.4, 1.5.2.5
		test(function() {
			check_no_item_in_targetlist(ev.changedTouches, "changedTouches", ev.touches, "touches");
			check_no_item_in_targetlist(ev.changedTouches, "changedTouches", ev.targetTouches, "targetTouches");
		}, "touchend #" + touchend_received + ": no item in changedTouches are in touches or targetTouches");

		// TA: 1.5.2.6
		test(function() {
			var found=false;
			for (i=0; i<ev.changedTouches.length; i++)
				if (ev.changedTouches.item(i).target == ev.target)
					found=true;
			assert_true(found, "at least one item in changedTouches has matching target");
		}, "touchend #" + touchend_received + ": at least one item in changedTouches targeted at this element");

		// TA: 1.5.3.2, 1.5.3.3
		test(function() {
			assert_true(ev.targetTouches.length >= 0, "targetTouches.length is non-negative");
			assert_true(ev.targetTouches.length <= ev.touches.length, "targetTouches.length is smaller than touches.length");
			check_list_subset_of_targetlist(ev.targetTouches, "targetTouches", ev.touches, "touches");
		}, "touchend #" + touchend_received + ": targetTouches is a subset of touches");

		// TA: 1.5.3.5 (new)
		test(function() {
			check_targets(ev.targetTouches, ev.target);
		}, "touchend #" + touchend_received + ": targets of targetTouches are correct");

		// In some cases, when multiple touch points are released simultaneously
		// the UA would dispatch the "same" touchend event (same changedTouches, same touches, but possibly different targetTouches)
		// to each of the elements that are starting elements of the released touch points.
		// in these situations, the subsequent events are exempt from TA 1.5.3.4 and 1.5.4.2
		var same_event_as_last = false;
		if (last_changedTouches && last_changedTouches.length==ev.changedTouches.length) {
			same_event_as_last = true; // assume true until proven otherwise
			for (i=0; i<last_changedTouches.length; i++) {
				var match = false;
				for (j=0; j<ev.changedTouches.length; j++)
					if (last_changedTouches.item(i) == ev.changedTouches.item(j)) {
						match = true;
						break;
					}
				if (!match)
					same_event_as_last = false;
			}
		}

		if (!same_event_as_last) {
			// TA: 1.5.3.4
			// Getting semi-random failures on this and 1.5.4.2.
			// See 1.5.4.2. Not sure if it's the same issue...
			test(function() {
				assert_true(last_targetTouches[ev.target.id].length > 0, "last received targetTouches.length is not zero");
				var diff_in_targetTouches = last_targetTouches[ev.target.id].length - ev.targetTouches.length;
				debug_print("diff_in_targetTouches=" + diff_in_targetTouches + "<br>");
				assert_true(diff_in_targetTouches > 0, "targetTouches.length is smaller than last received targetTouches.length");
				assert_true(diff_in_targetTouches <= ev.changedTouches.length, "change in targetTouches.length is smaller than changedTouches.length");
			}, "touchend #" + touchend_received + ": change in targetTouches.length is valid");

			// TA: 1.5.4.2
			// Getting semi-random failures on this and 1.5.3.4.
			// It looks like if fingers are lifted simultaneously, the "same" touchend event can be dispatched to two target elements
			// but adapted to the element (same touches, changedTouches but different targetTouches).
			// When one event is processed after another, ev.touches would end up being identical to last_touches, leading  to failure.
			// Question is why done() does not stop the processing of the latter event.
			test(function() {
				assert_true(last_touches.length > 0, "last received touches.length is not zero");
				var diff_in_touches = last_touches.length - ev.touches.length;
				debug_print("diff_in_touches=" + diff_in_touches + "<br>");
				assert_true(diff_in_touches > 0, "touches.length is smaller than last received touches.length");
				assert_equals(diff_in_touches, ev.changedTouches.length, "change in touches.length equals changedTouches.length");
			}, "touchend #" + touchend_received + ": change in touches.length is valid");
		}

		// TA: 1.6.4
		debug_print("touchend #" + touchend_received + ": TA 1.6.4<br>");
		test(function() {
			check_starting_elements(ev.changedTouches);
		}, "touchend #" + touchend_received + ": event dispatched to correct element<br>");

		debug_print("touchend #" + touchend_received + ": saving touch lists<br>");

		last_touches = ev.touches;
		last_targetTouches[ev.target.id] = ev.targetTouches;
		last_changedTouches = ev.changedTouches;

		debug_print("touchend #" + touchend_received + ": done<br>");
		if(ev.touches.length==0)
			done();
	});

	on_event(target0, "mousedown", function onMouseDown(ev) {
		test_mousedown.step(function() {
			assert_true(touchstart_received,
				"The touchstart event must be dispatched before any mouse " +
				"events. (If this fails, it might mean that the user agent does " +
				"not implement W3C touch events at all.)"
			);
		});
		test_mousedown.done();

		if (!touchstart_received) {
			// Abort the tests.  If touch events are not supported, then most of
			// the other event handlers will never be called, and the test will
			// time out with misleading results.
			done();
		}
	});
}