Unity 8
UnityTestCase.qml
1 /*
2  * Copyright 2013-2015 Canonical Ltd.
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; version 3.
7  *
8  * This program is distributed in the hope that it will be useful,
9  * but WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11  * GNU General Public License for more details.
12  *
13  * You should have received a copy of the GNU General Public License
14  * along with this program. If not, see <http://www.gnu.org/licenses/>.
15  */
16 
17 import QtQuick 2.4
18 import QtTest 1.0
19 import Ubuntu.Components 1.3
20 import Unity.Test 0.1 as UT
21 
22 TestCase {
23  id: testCase
24  TestUtil {id:util}
25 
26  // This is needed for waitForRendering calls to return
27  // if the watched element already got rendered
28  Rectangle {
29  id: rotatingRectangle
30  width: units.gu(1)
31  height: width
32  parent: testCase.parent
33  border { width: units.dp(1); color: "black" }
34  opacity: 0.6
35 
36  visible: testCase.running
37 
38  RotationAnimation on rotation {
39  running: rotatingRectangle.visible
40  from: 0
41  to: 360
42  loops: Animation.Infinite
43  duration: 1000
44  }
45  }
46 
47  // Fake implementation to be provided to items under test
48  property var fakeDateTime: new function() {
49  this.currentTimeMs = 0
50  this.getCurrentTimeMs = function() {return this.currentTimeMs}
51  }
52 
53  // TODO This function can be removed altogether once we use Qt 5.5 which has the same feature
54  function mouseClick(item, x, y, button, modifiers, delay) {
55  if (button === undefined)
56  button = Qt.LeftButton;
57  if (modifiers === undefined)
58  modifiers = Qt.NoModifier;
59  if (delay === undefined)
60  delay = -1;
61  if (x === undefined)
62  x = item.width / 2;
63  if (y === undefined)
64  y = item.height / 2;
65  if (!qtest_events.mouseClick(item, x, y, button, modifiers, delay))
66  qtest_fail("window not shown", 2);
67  }
68 
69  // TODO This function can be removed altogether once we use Qt 5.5 which has the same feature
70  function mouseDoubleClick(item, x, y, button, modifiers, delay) {
71  if (button === undefined)
72  button = Qt.LeftButton;
73  if (modifiers === undefined)
74  modifiers = Qt.NoModifier;
75  if (delay === undefined)
76  delay = -1;
77  if (x === undefined)
78  x = item.width / 2;
79  if (y === undefined)
80  y = item.height / 2;
81  if (!qtest_events.mouseDoubleClick(item, x, y, button, modifiers, delay))
82  qtest_fail("window not shown", 2)
83  }
84 
85  // TODO This function can be removed altogether once we use Qt 5.5 which has the same feature
86  function mousePress(item, x, y, button, modifiers, delay) {
87  if (button === undefined)
88  button = Qt.LeftButton;
89  if (modifiers === undefined)
90  modifiers = Qt.NoModifier;
91  if (delay === undefined)
92  delay = -1;
93  if (x === undefined)
94  x = item.width / 2;
95  if (y === undefined)
96  y = item.height / 2;
97  if (!qtest_events.mousePress(item, x, y, button, modifiers, delay))
98  qtest_fail("window not shown", 2)
99  }
100 
101  // TODO This function can be removed altogether once we use Qt 5.5 which has the same feature
102  function mouseRelease(item, x, y, button, modifiers, delay) {
103  if (button === undefined)
104  button = Qt.LeftButton;
105  if (modifiers === undefined)
106  modifiers = Qt.NoModifier;
107  if (delay === undefined)
108  delay = -1;
109  if (x === undefined)
110  x = item.width / 2;
111  if (y === undefined)
112  y = item.height / 2;
113  if (!qtest_events.mouseRelease(item, x, y, button, modifiers, delay))
114  qtest_fail("window not shown", 2)
115  }
116 
117 
118  // Flickable won't recognise a single mouse move as dragging the flickable.
119  // Use 5 steps because it's what
120  // Qt uses in QQuickViewTestUtil::flick
121  // speed is in pixels/second
122  function mouseFlick(item, x, y, toX, toY, pressMouse, releaseMouse,
123  speed, iterations) {
124  pressMouse = ((pressMouse != null) ? pressMouse : true); // Default to true for pressMouse if not present
125  releaseMouse = ((releaseMouse != null) ? releaseMouse : true); // Default to true for releaseMouse if not present
126 
127  // set a default speed if not specified
128  speed = (speed != null) ? speed : units.gu(10);
129 
130  // set a default iterations if not specified
131  iterations = (iterations !== undefined) ? iterations : 5
132 
133  var distance = Math.sqrt(Math.pow(toX - x, 2) + Math.pow(toY - y, 2))
134  var totalTime = (distance / speed) * 1000 /* converting speed to pixels/ms */
135 
136  var timeStep = totalTime / iterations
137  var diffX = (toX - x) / iterations
138  var diffY = (toY - y) / iterations
139  if (pressMouse) {
140  fakeDateTime.currentTimeMs += timeStep
141  mousePress(item, x, y)
142  }
143  for (var i = 0; i < iterations; ++i) {
144  fakeDateTime.currentTimeMs += timeStep
145  if (i === iterations - 1) {
146  // Avoid any rounding errors by making the last move be at precisely
147  // the point specified
148  mouseMove(item, toX, toY, iterations / speed)
149  } else {
150  mouseMove(item, x + (i + 1) * diffX, y + (i + 1) * diffY, iterations / speed)
151  }
152  }
153  if (releaseMouse) {
154  fakeDateTime.currentTimeMs += timeStep
155  mouseRelease(item, toX, toY)
156  }
157  }
158 
159 
160  // Find an object with the given name in the children tree of "obj"
161  function findChild(obj, objectName) {
162  return findChildIn(obj, "children", objectName);
163  }
164 
165  // Find an object with the given name in the children tree of "obj"
166  // Including invisible children like animations, timers etc.
167  // Note: you should use findChild if you're not sure you need this
168  // as this tree is much bigger and might contain stuff that goes
169  // away randomly.
170  function findInvisibleChild(obj, objectName) {
171  return findChildIn(obj, "data", objectName);
172  }
173 
174  // Find a child in the named property
175  function findChildIn(obj, prop, objectName) {
176  var childs = new Array(0);
177  childs.push(obj)
178  while (childs.length > 0) {
179  if (childs[0].objectName == objectName) {
180  return childs[0]
181  }
182  for (var i in childs[0][prop]) {
183  childs.push(childs[0][prop][i])
184  }
185  childs.splice(0, 1);
186  }
187  return null;
188  }
189 
190  function findChildsByType(obj, typeName) {
191  var res = new Array(0);
192  for (var i in obj.children) {
193  var c = obj.children[i];
194  if (UT.Util.isInstanceOf(c, typeName)) {
195  res.push(c)
196  }
197  res = res.concat(findChildsByType(c, typeName));
198  }
199  return res;
200  }
201 
202  // Type a full string instead of keyClick letter by letter
203  function typeString(str) {
204  for (var i = 0; i < str.length; i++) {
205  keyClick(str[i])
206  }
207  }
208 
209  // Keeps executing a given parameter-less function until it returns the given
210  // expected result or the timemout is reached (in which case a test failure
211  // is generated)
212  function tryCompareFunction(func, expectedResult, timeout) {
213  var timeSpent = 0
214  if (timeout === undefined)
215  timeout = 5000;
216  var success = false
217  var actualResult
218  while (timeSpent < timeout && !success) {
219  actualResult = func()
220  success = qtest_compareInternal(actualResult, expectedResult)
221  if (success === false) {
222  wait(50)
223  timeSpent += 50
224  }
225  }
226 
227  var act = qtest_results.stringify(actualResult)
228  var exp = qtest_results.stringify(expectedResult)
229  if (!qtest_results.compare(success,
230  "function returned unexpected result",
231  act, exp,
232  util.callerFile(), util.callerLine())) {
233  throw new Error("QtQuickTest::fail")
234  }
235  }
236 
237  function flickToYEnd(item) {
238  var i = 0;
239  var x = item.width / 2;
240  var y = item.height - units.gu(1);
241  var toY = units.gu(1);
242  var maxIterations = 5 + item.contentHeight / item.height;
243  while (i < maxIterations && !item.atYEnd) {
244  touchFlick(item, x, y, x, toY);
245  tryCompare(item, "moving", false);
246  ++i;
247  }
248  tryCompare(item, "atYEnd", true);
249  }
250 
251  function touchEvent(item) {
252  return UT.Util.touchEvent(item)
253  }
254 
255  // speed is in pixels/second
256  function touchFlick(item, x, y, toX, toY, beginTouch, endTouch, speed, iterations) {
257  // Make sure the item is rendered
258  waitForRendering(item);
259 
260  var root = fetchRootItem(item);
261  var rootFrom = item.mapToItem(root, x, y);
262  var rootTo = item.mapToItem(root, toX, toY);
263 
264  // Default to true for beginTouch if not present
265  beginTouch = (beginTouch !== undefined) ? beginTouch : true
266 
267  // Default to true for endTouch if not present
268  endTouch = (endTouch !== undefined) ? endTouch : true
269 
270  // Set a default speed if not specified
271  speed = (speed !== undefined) ? speed : units.gu(10)
272 
273  // Set a default iterations if not specified
274  var iterations = (iterations !== undefined) ? iterations : 10
275 
276  var distance = Math.sqrt(Math.pow(rootTo.x - rootFrom.x, 2) + Math.pow(rootTo.Y - rootFrom.y, 2))
277  var totalTime = (distance / speed) * 1000 /* converting speed to pixels/ms */
278 
279  var timeStep = totalTime / iterations
280  var diffX = (rootTo.x - rootFrom.x) / iterations
281  var diffY = (rootTo.y - rootFrom.y) / iterations
282  if (beginTouch) {
283  fakeDateTime.currentTimeMs += timeStep
284 
285  var event = touchEvent(item)
286  event.press(0 /* touchId */, rootFrom.x, rootFrom.y)
287  event.commit()
288  }
289  for (var i = 0; i < iterations; ++i) {
290  fakeDateTime.currentTimeMs += timeStep
291  if (i === iterations - 1) {
292  // Avoid any rounding errors by making the last move be at precisely
293  // the point specified
294  wait(iterations / speed)
295  var event = touchEvent(item)
296  event.move(0 /* touchId */, rootTo.x, rootTo.y)
297  event.commit()
298  } else {
299  wait(iterations / speed)
300  var event = touchEvent(item)
301  event.move(0 /* touchId */, rootFrom.x + (i + 1) * diffX, rootFrom.y + (i + 1) * diffY)
302  event.commit()
303  }
304  }
305  if (endTouch) {
306  fakeDateTime.currentTimeMs += timeStep
307  var event = touchEvent(item)
308  event.release(0 /* touchId */, rootTo.x, rootTo.y)
309  event.commit()
310  }
311  }
312 
313  // perform a drag in the given direction until the given condition is true
314  // The condition is a function to be evaluated after every step
315  function touchDragUntil(item, startX, startY, stepX, stepY, condition) {
316  multiTouchDragUntil([0], item, startX, startY, stepX, stepY, condition);
317  }
318 
319  function multiTouchDragUntil(touchIds, item, startX, startY, stepX, stepY, condition) {
320  var root = fetchRootItem(item);
321  var pos = item.mapToItem(root, startX, startY);
322 
323  // convert step to scene coords
324  {
325  var stepStart = item.mapToItem(root, 0, 0);
326  var stepEnd = item.mapToItem(root, stepX, stepY);
327  }
328  stepX = stepEnd.x - stepStart.x;
329  stepY = stepEnd.y - stepStart.y;
330 
331  var event = touchEvent(item)
332  for (var i = 0; i < touchIds.length; i++) {
333  event.press(touchIds[i], pos.x, pos.y)
334  }
335  event.commit()
336 
337  // we have to stop at some point
338  var maxSteps = 100;
339  var stepsDone = 0;
340 
341  while (!condition() && stepsDone < maxSteps) {
342  wait(25);
343  fakeDateTime.currentTimeMs += 25;
344 
345  pos.x += stepX;
346  pos.y += stepY;
347 
348  event = touchEvent(item);
349  for (i = 0; i < touchIds.length; i++) {
350  event.move(touchIds[i], pos.x, pos.y);
351  }
352  event.commit();
353 
354  stepsDone += 1;
355  }
356 
357  event = touchEvent(item)
358  for (i = 0; i < touchIds.length; i++) {
359  event.release(touchIds[i], pos.x, pos.y)
360  }
361  event.commit()
362  }
363 
364  function touchMove(item, tox, toy) { multiTouchMove(0, item, tox, toy); }
365 
366  function multiTouchMove(touchId, item, tox, toy) {
367  if (typeof touchId !== "number") touchId = 0;
368  var root = fetchRootItem(item)
369  var rootPoint = item.mapToItem(root, tox, toy)
370 
371  var event = touchEvent(item);
372  event.move(touchId, rootPoint.x, rootPoint.y);
373  event.commit();
374  }
375 
376  function touchPinch(item, x1Start, y1Start, x1End, y1End, x2Start, y2Start, x2End, y2End) {
377  // Make sure the item is rendered
378  waitForRendering(item);
379 
380  var event1 = touchEvent(item);
381  // first finger
382  event1.press(0, x1Start, y1Start);
383  event1.commit();
384  // second finger
385  event1.move(0, x1Start, y1Start);
386  event1.press(1, x2Start, y2Start);
387  event1.commit();
388 
389  // pinch
390  for (var i = 0.0; i < 1.0; i += 0.02) {
391  event1.move(0, x1Start + (x1End - x1Start) * i, y1Start + (y1End - y1Start) * i);
392  event1.move(1, x2Start + (x2End - x2Start) * i, y2Start + (y2End - y2Start) * i);
393  event1.commit();
394  }
395 
396  // release
397  event1.release(0, x1End, y1End);
398  event1.release(1, x2End, y2End);
399  event1.commit();
400  }
401 
402  function fetchRootItem(item) {
403  if (item.parent)
404  return fetchRootItem(item.parent)
405  else
406  return item
407  }
408 
409  function touchPress(item, x, y) { multiTouchPress(0, item, x, y, []); }
410 
411  /*! \brief Release a touch point
412 
413  \param touchId The touchId to be pressed
414  \param item The item
415  \param x The x coordinate of the press, defaults to horizontal center
416  \param y The y coordinate of the press, defaults to vertical center
417  \param stationaryPoints An array of touchIds which are "already touched"
418  */
419  function multiTouchPress(touchId, item, x, y, stationaryPoints) {
420  if (typeof touchId !== "number") touchId = 0;
421  if (typeof x !== "number") x = item.width / 2;
422  if (typeof y !== "number") y = item.height / 2;
423  if (typeof stationaryPoints !== "object") stationaryPoints = []
424  var root = fetchRootItem(item)
425  var rootPoint = item.mapToItem(root, x, y)
426 
427  var event = touchEvent(item)
428  event.press(touchId, rootPoint.x, rootPoint.y)
429  for (var i = 0; i < stationaryPoints.length; i++) {
430  event.stationary(stationaryPoints[i]);
431  }
432  event.commit()
433  }
434 
435  function touchRelease(item, x, y) { multiTouchRelease(0, item, x, y, []); }
436 
437  /*! \brief Release a touch point
438 
439  \param touchId The touchId to be released
440  \param item The item
441  \param x The x coordinate of the release, defaults to horizontal center
442  \param y The y coordinate of the release, defaults to vertical center
443  \param stationaryPoints An array of touchIds which are "still touched"
444  */
445  function multiTouchRelease(touchId, item, x, y, stationaryPoints) {
446  if (typeof touchId !== "number") touchId = 0;
447  if (typeof x !== "number") x = item.width / 2;
448  if (typeof y !== "number") y = item.height / 2;
449  if (typeof stationaryPoints !== "object") stationaryPoints = []
450  var root = fetchRootItem(item)
451  var rootPoint = item.mapToItem(root, x, y)
452 
453  var event = touchEvent(item)
454  event.release(touchId, rootPoint.x, rootPoint.y)
455  for (var i = 0; i < stationaryPoints.length; i++) {
456  event.stationary(stationaryPoints[i]);
457  }
458  event.commit()
459  }
460 
461  /*! \brief Tap the item with a touch event.
462 
463  \param item The item to be tapped
464  \param x The x coordinate of the tap, defaults to horizontal center
465  \param y The y coordinate of the tap, defaults to vertical center
466  */
467  function tap(item, x, y) {
468  multiTouchTap([0], item, x, y);
469  }
470 
471  function multiTouchTap(touchIds, item, x, y) {
472  if (typeof touchIds !== "object") touchIds = [0];
473  if (typeof x !== "number") x = item.width / 2;
474  if (typeof y !== "number") y = item.height / 2;
475 
476  var root = fetchRootItem(item)
477  var rootPoint = item.mapToItem(root, x, y)
478 
479  var event = touchEvent(item)
480  for (var i = 0; i < touchIds.length; i++) {
481  event.press(touchIds[i], rootPoint.x, rootPoint.y)
482  }
483  event.commit()
484 
485  event = touchEvent(item)
486  for (i = 0; i < touchIds.length; i++) {
487  event.release(touchIds[i], rootPoint.x, rootPoint.y)
488  }
489  event.commit()
490  }
491 
492 
493  Component.onCompleted: {
494  var rootItem = parent;
495  while (rootItem.parent != undefined) {
496  rootItem = rootItem.parent;
497  }
498  removeTimeConstraintsFromDirectionalDragAreas(rootItem);
499  }
500 
501  /*
502  In qmltests, sequences of touch events are sent all at once, unlike in "real life".
503  Also qmltests might run really slowly, e.g. when run from inside virtual machines.
504  Thus to remove a variable that qmltests cannot really control, namely time, this
505  function removes all constraints from DirectionalDragAreas that are sensible to
506  elapsed time.
507 
508  This effectively makes DirectionalDragAreas easier to fool.
509  */
510  function removeTimeConstraintsFromDirectionalDragAreas(item) {
511 
512  // use duck-typing to identify a DirectionalDragArea
513  if (item.removeTimeConstraints != undefined) {
514  item.removeTimeConstraints();
515  } else {
516  for (var i in item.children) {
517  removeTimeConstraintsFromDirectionalDragAreas(item.children[i]);
518  }
519  }
520  }
521 
522  // TODO This function can be removed altogether once we use Qt 5.5 which has the same feature
523  function waitForRendering(item, timeout) {
524  if (timeout === undefined)
525  timeout = 5000;
526  if (!item)
527  qtest_fail("No item given to waitForRendering", 1);
528  return qtest_results.waitForRendering(item, timeout);
529  }
530 
531  /*
532  Wait until any transition animation has finished for the given StateGroup or Item
533  */
534  function waitUntilTransitionsEnd(stateGroup) {
535  var transitions = stateGroup.transitions;
536  for (var i = 0; i < transitions.length; ++i) {
537  var transition = transitions[i];
538  tryCompare(transition, "running", false, 2000);
539  }
540  }
541 }