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