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