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.0
18 import QtTest 1.0
19 import Ubuntu.Components 0.1
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 touchPinch(item, x1Start, y1Start, x1End, y1End, x2Start, y2Start, x2End, y2End) {
355  // Make sure the item is rendered
356  waitForRendering(item);
357 
358  var event1 = touchEvent(item);
359  // first finger
360  event1.press(0, x1Start, y1Start);
361  event1.commit();
362  // second finger
363  event1.move(0, x1Start, y1Start);
364  event1.press(1, x2Start, y2Start);
365  event1.commit();
366 
367  // pinch
368  for (var i = 0.0; i < 1.0; i += 0.02) {
369  event1.move(0, x1Start + (x1End - x1Start) * i, y1Start + (y1End - y1Start) * i);
370  event1.move(1, x2Start + (x2End - x2Start) * i, y2Start + (y2End - y2Start) * i);
371  event1.commit();
372  }
373 
374  // release
375  event1.release(0, x1End, y1End);
376  event1.release(1, x2End, y2End);
377  event1.commit();
378  }
379 
380  function fetchRootItem(item) {
381  if (item.parent)
382  return fetchRootItem(item.parent)
383  else
384  return item
385  }
386 
387  function touchPress(item, x, y) {
388  var root = fetchRootItem(item)
389  var rootPoint = item.mapToItem(root, x, y)
390 
391  var event = touchEvent(item)
392  event.press(0 /* touchId */, rootPoint.x, rootPoint.y)
393  event.commit()
394  }
395 
396  function touchRelease(item, x, y) {
397  var root = fetchRootItem(item)
398  var rootPoint = item.mapToItem(root, x, y)
399 
400  var event = touchEvent(item)
401  event.release(0 /* touchId */, rootPoint.x, rootPoint.y)
402  event.commit()
403  }
404 
405  /*! \brief Tap the item with a touch event.
406 
407  \param item The item to be tapped
408  \param x The x coordinate of the tap, defaults to horizontal center
409  \param y The y coordinate of the tap, defaults to vertical center
410  */
411  function tap(item, x, y) {
412  if (typeof x !== "number") x = item.width / 2;
413  if (typeof y !== "number") y = item.height / 2;
414 
415  var root = fetchRootItem(item)
416  var rootPoint = item.mapToItem(root, x, y)
417 
418  var event = touchEvent(item)
419  event.press(0 /* touchId */, rootPoint.x, rootPoint.y)
420  event.commit()
421 
422  event = touchEvent(item)
423  event.release(0 /* touchId */, rootPoint.x, rootPoint.y)
424  event.commit()
425  }
426 
427  Component.onCompleted: {
428  var rootItem = parent;
429  while (rootItem.parent != undefined) {
430  rootItem = rootItem.parent;
431  }
432  removeTimeConstraintsFromDirectionalDragAreas(rootItem);
433  }
434 
435  /*
436  In qmltests, sequences of touch events are sent all at once, unlike in "real life".
437  Also qmltests might run really slowly, e.g. when run from inside virtual machines.
438  Thus to remove a variable that qmltests cannot really control, namely time, this
439  function removes all constraints from DirectionalDragAreas that are sensible to
440  elapsed time.
441 
442  This effectively makes DirectionalDragAreas easier to fool.
443  */
444  function removeTimeConstraintsFromDirectionalDragAreas(item) {
445 
446  // use duck-typing to identify a DirectionalDragArea
447  if (item.removeTimeConstraints != undefined) {
448  item.removeTimeConstraints();
449  } else {
450  for (var i in item.children) {
451  removeTimeConstraintsFromDirectionalDragAreas(item.children[i]);
452  }
453  }
454  }
455 
456  // TODO This function can be removed altogether once we use Qt 5.5 which has the same feature
457  function waitForRendering(item, timeout) {
458  if (timeout === undefined)
459  timeout = 5000;
460  if (!item)
461  qtest_fail("No item given to waitForRendering", 1);
462  return qtest_results.waitForRendering(item, timeout);
463  }
464 
465  /*
466  Wait until any transition animation has finished for the given StateGroup or Item
467  */
468  function waitUntilTransitionsEnd(stateGroup) {
469  var transitions = stateGroup.transitions;
470  for (var i = 0; i < transitions.length; ++i) {
471  var transition = transitions[i];
472  tryCompare(transition, "running", false, 2000);
473  }
474  }
475 }