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  while (i < 5 && !item.atYEnd) {
243  touchFlick(item, x, y, x, toY);
244  tryCompare(item, "moving", false);
245  ++i;
246  }
247  tryCompare(item, "atYEnd", true);
248  }
249 
250  function touchEvent(item) {
251  return UT.Util.touchEvent(item)
252  }
253 
254  // speed is in pixels/second
255  function touchFlick(item, x, y, toX, toY, beginTouch, endTouch, speed, iterations) {
256  // Make sure the item is rendered
257  waitForRendering(item);
258 
259  var root = fetchRootItem(item);
260  var rootFrom = item.mapToItem(root, x, y);
261  var rootTo = item.mapToItem(root, toX, toY);
262 
263  // Default to true for beginTouch if not present
264  beginTouch = (beginTouch !== undefined) ? beginTouch : true
265 
266  // Default to true for endTouch if not present
267  endTouch = (endTouch !== undefined) ? endTouch : true
268 
269  // Set a default speed if not specified
270  speed = (speed !== undefined) ? speed : units.gu(10)
271 
272  // Set a default iterations if not specified
273  var iterations = (iterations !== undefined) ? iterations : 10
274 
275  var distance = Math.sqrt(Math.pow(rootTo.x - rootFrom.x, 2) + Math.pow(rootTo.Y - rootFrom.y, 2))
276  var totalTime = (distance / speed) * 1000 /* converting speed to pixels/ms */
277 
278  var timeStep = totalTime / iterations
279  var diffX = (rootTo.x - rootFrom.x) / iterations
280  var diffY = (rootTo.y - rootFrom.y) / iterations
281  if (beginTouch) {
282  fakeDateTime.currentTimeMs += timeStep
283 
284  var event = touchEvent(item)
285  event.press(0 /* touchId */, rootFrom.x, rootFrom.y)
286  event.commit()
287  }
288  for (var i = 0; i < iterations; ++i) {
289  fakeDateTime.currentTimeMs += timeStep
290  if (i === iterations - 1) {
291  // Avoid any rounding errors by making the last move be at precisely
292  // the point specified
293  wait(iterations / speed)
294  var event = touchEvent(item)
295  event.move(0 /* touchId */, rootTo.x, rootTo.y)
296  event.commit()
297  } else {
298  wait(iterations / speed)
299  var event = touchEvent(item)
300  event.move(0 /* touchId */, rootFrom.x + (i + 1) * diffX, rootFrom.y + (i + 1) * diffY)
301  event.commit()
302  }
303  }
304  if (endTouch) {
305  fakeDateTime.currentTimeMs += timeStep
306  var event = touchEvent(item)
307  event.release(0 /* touchId */, rootTo.x, rootTo.y)
308  event.commit()
309  }
310  }
311 
312  // perform a drag in the given direction until the given condition is true
313  // The condition is a function to be evaluated after every step
314  function touchDragUntil(item, startX, startY, stepX, stepY, condition) {
315 
316  var root = fetchRootItem(item);
317  var pos = item.mapToItem(root, startX, startY);
318 
319  // convert step to scene coords
320  {
321  var stepStart = item.mapToItem(root, 0, 0);
322  var stepEnd = item.mapToItem(root, stepX, stepY);
323  }
324  stepX = stepEnd.x - stepStart.x;
325  stepY = stepEnd.y - stepStart.y;
326 
327  var event = touchEvent(item)
328  event.press(0 /* touchId */, pos.x, pos.y)
329  event.commit()
330 
331  // we have to stop at some point
332  var maxSteps = 100;
333  var stepsDone = 0;
334 
335  while (!condition() && stepsDone < maxSteps) {
336  wait(25);
337  fakeDateTime.currentTimeMs += 25;
338 
339  pos.x += stepX;
340  pos.y += stepY;
341 
342  event = touchEvent(item);
343  event.move(0 /* touchId */, pos.x, pos.y);
344  event.commit();
345 
346  stepsDone += 1;
347  }
348 
349  event = touchEvent(item)
350  event.release(0 /* touchId */, pos.x, pos.y)
351  event.commit()
352  }
353 
354  function touchMove(item, tox, toy) {
355  var root = fetchRootItem(item)
356  var rootPoint = item.mapToItem(root, tox, toy)
357 
358  var event = touchEvent(item);
359  event.move(0 /* touchId */, rootPoint.x, rootPoint.y);
360  event.commit();
361  }
362 
363  function touchPinch(item, x1Start, y1Start, x1End, y1End, x2Start, y2Start, x2End, y2End) {
364  // Make sure the item is rendered
365  waitForRendering(item);
366 
367  var event1 = touchEvent(item);
368  // first finger
369  event1.press(0, x1Start, y1Start);
370  event1.commit();
371  // second finger
372  event1.move(0, x1Start, y1Start);
373  event1.press(1, x2Start, y2Start);
374  event1.commit();
375 
376  // pinch
377  for (var i = 0.0; i < 1.0; i += 0.02) {
378  event1.move(0, x1Start + (x1End - x1Start) * i, y1Start + (y1End - y1Start) * i);
379  event1.move(1, x2Start + (x2End - x2Start) * i, y2Start + (y2End - y2Start) * i);
380  event1.commit();
381  }
382 
383  // release
384  event1.release(0, x1End, y1End);
385  event1.release(1, x2End, y2End);
386  event1.commit();
387  }
388 
389  function fetchRootItem(item) {
390  if (item.parent)
391  return fetchRootItem(item.parent)
392  else
393  return item
394  }
395 
396  function touchPress(item, x, y) {
397  var root = fetchRootItem(item)
398  var rootPoint = item.mapToItem(root, x, y)
399 
400  var event = touchEvent(item)
401  event.press(0 /* touchId */, rootPoint.x, rootPoint.y)
402  event.commit()
403  }
404 
405  function touchRelease(item, x, y) {
406  var root = fetchRootItem(item)
407  var rootPoint = item.mapToItem(root, x, y)
408 
409  var event = touchEvent(item)
410  event.release(0 /* touchId */, rootPoint.x, rootPoint.y)
411  event.commit()
412  }
413 
414  /*! \brief Tap the item with a touch event.
415 
416  \param item The item to be tapped
417  \param x The x coordinate of the tap, defaults to horizontal center
418  \param y The y coordinate of the tap, defaults to vertical center
419  */
420  function tap(item, x, y) {
421  if (typeof x !== "number") x = item.width / 2;
422  if (typeof y !== "number") y = item.height / 2;
423 
424  var root = fetchRootItem(item)
425  var rootPoint = item.mapToItem(root, x, y)
426 
427  var event = touchEvent(item)
428  event.press(0 /* touchId */, rootPoint.x, rootPoint.y)
429  event.commit()
430 
431  event = touchEvent(item)
432  event.release(0 /* touchId */, rootPoint.x, rootPoint.y)
433  event.commit()
434  }
435 
436  Component.onCompleted: {
437  var rootItem = parent;
438  while (rootItem.parent != undefined) {
439  rootItem = rootItem.parent;
440  }
441  removeTimeConstraintsFromDirectionalDragAreas(rootItem);
442  }
443 
444  /*
445  In qmltests, sequences of touch events are sent all at once, unlike in "real life".
446  Also qmltests might run really slowly, e.g. when run from inside virtual machines.
447  Thus to remove a variable that qmltests cannot really control, namely time, this
448  function removes all constraints from DirectionalDragAreas that are sensible to
449  elapsed time.
450 
451  This effectively makes DirectionalDragAreas easier to fool.
452  */
453  function removeTimeConstraintsFromDirectionalDragAreas(item) {
454 
455  // use duck-typing to identify a DirectionalDragArea
456  if (item.removeTimeConstraints != undefined) {
457  item.removeTimeConstraints();
458  } else {
459  for (var i in item.children) {
460  removeTimeConstraintsFromDirectionalDragAreas(item.children[i]);
461  }
462  }
463  }
464 
465  // TODO This function can be removed altogether once we use Qt 5.5 which has the same feature
466  function waitForRendering(item, timeout) {
467  if (timeout === undefined)
468  timeout = 5000;
469  if (!item)
470  qtest_fail("No item given to waitForRendering", 1);
471  return qtest_results.waitForRendering(item, timeout);
472  }
473 
474  /*
475  Wait until any transition animation has finished for the given StateGroup or Item
476  */
477  function waitUntilTransitionsEnd(stateGroup) {
478  var transitions = stateGroup.transitions;
479  for (var i = 0; i < transitions.length; ++i) {
480  var transition = transitions[i];
481  tryCompare(transition, "running", false, 2000);
482  }
483  }
484 }