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 
317  var root = fetchRootItem(item);
318  var pos = item.mapToItem(root, startX, startY);
319 
320  // convert step to scene coords
321  {
322  var stepStart = item.mapToItem(root, 0, 0);
323  var stepEnd = item.mapToItem(root, stepX, stepY);
324  }
325  stepX = stepEnd.x - stepStart.x;
326  stepY = stepEnd.y - stepStart.y;
327 
328  var event = touchEvent(item)
329  event.press(0 /* touchId */, pos.x, pos.y)
330  event.commit()
331 
332  // we have to stop at some point
333  var maxSteps = 100;
334  var stepsDone = 0;
335 
336  while (!condition() && stepsDone < maxSteps) {
337  wait(25);
338  fakeDateTime.currentTimeMs += 25;
339 
340  pos.x += stepX;
341  pos.y += stepY;
342 
343  event = touchEvent(item);
344  event.move(0 /* touchId */, pos.x, pos.y);
345  event.commit();
346 
347  stepsDone += 1;
348  }
349 
350  event = touchEvent(item)
351  event.release(0 /* touchId */, pos.x, pos.y)
352  event.commit()
353  }
354 
355  function touchMove(item, tox, toy) {
356  var root = fetchRootItem(item)
357  var rootPoint = item.mapToItem(root, tox, toy)
358 
359  var event = touchEvent(item);
360  event.move(0 /* touchId */, rootPoint.x, rootPoint.y);
361  event.commit();
362  }
363 
364  function touchPinch(item, x1Start, y1Start, x1End, y1End, x2Start, y2Start, x2End, y2End) {
365  // Make sure the item is rendered
366  waitForRendering(item);
367 
368  var event1 = touchEvent(item);
369  // first finger
370  event1.press(0, x1Start, y1Start);
371  event1.commit();
372  // second finger
373  event1.move(0, x1Start, y1Start);
374  event1.press(1, x2Start, y2Start);
375  event1.commit();
376 
377  // pinch
378  for (var i = 0.0; i < 1.0; i += 0.02) {
379  event1.move(0, x1Start + (x1End - x1Start) * i, y1Start + (y1End - y1Start) * i);
380  event1.move(1, x2Start + (x2End - x2Start) * i, y2Start + (y2End - y2Start) * i);
381  event1.commit();
382  }
383 
384  // release
385  event1.release(0, x1End, y1End);
386  event1.release(1, x2End, y2End);
387  event1.commit();
388  }
389 
390  function fetchRootItem(item) {
391  if (item.parent)
392  return fetchRootItem(item.parent)
393  else
394  return item
395  }
396 
397  function touchPress(item, x, y) {
398  var root = fetchRootItem(item)
399  var rootPoint = item.mapToItem(root, x, y)
400 
401  var event = touchEvent(item)
402  event.press(0 /* touchId */, rootPoint.x, rootPoint.y)
403  event.commit()
404  }
405 
406  function touchRelease(item, x, y) {
407  var root = fetchRootItem(item)
408  var rootPoint = item.mapToItem(root, x, y)
409 
410  var event = touchEvent(item)
411  event.release(0 /* touchId */, rootPoint.x, rootPoint.y)
412  event.commit()
413  }
414 
415  /*! \brief Tap the item with a touch event.
416 
417  \param item The item to be tapped
418  \param x The x coordinate of the tap, defaults to horizontal center
419  \param y The y coordinate of the tap, defaults to vertical center
420  */
421  function tap(item, x, y) {
422  if (typeof x !== "number") x = item.width / 2;
423  if (typeof y !== "number") y = item.height / 2;
424 
425  var root = fetchRootItem(item)
426  var rootPoint = item.mapToItem(root, x, y)
427 
428  var event = touchEvent(item)
429  event.press(0 /* touchId */, rootPoint.x, rootPoint.y)
430  event.commit()
431 
432  event = touchEvent(item)
433  event.release(0 /* touchId */, rootPoint.x, rootPoint.y)
434  event.commit()
435  }
436 
437  Component.onCompleted: {
438  var rootItem = parent;
439  while (rootItem.parent != undefined) {
440  rootItem = rootItem.parent;
441  }
442  removeTimeConstraintsFromDirectionalDragAreas(rootItem);
443  }
444 
445  /*
446  In qmltests, sequences of touch events are sent all at once, unlike in "real life".
447  Also qmltests might run really slowly, e.g. when run from inside virtual machines.
448  Thus to remove a variable that qmltests cannot really control, namely time, this
449  function removes all constraints from DirectionalDragAreas that are sensible to
450  elapsed time.
451 
452  This effectively makes DirectionalDragAreas easier to fool.
453  */
454  function removeTimeConstraintsFromDirectionalDragAreas(item) {
455 
456  // use duck-typing to identify a DirectionalDragArea
457  if (item.removeTimeConstraints != undefined) {
458  item.removeTimeConstraints();
459  } else {
460  for (var i in item.children) {
461  removeTimeConstraintsFromDirectionalDragAreas(item.children[i]);
462  }
463  }
464  }
465 
466  // TODO This function can be removed altogether once we use Qt 5.5 which has the same feature
467  function waitForRendering(item, timeout) {
468  if (timeout === undefined)
469  timeout = 5000;
470  if (!item)
471  qtest_fail("No item given to waitForRendering", 1);
472  return qtest_results.waitForRendering(item, timeout);
473  }
474 
475  /*
476  Wait until any transition animation has finished for the given StateGroup or Item
477  */
478  function waitUntilTransitionsEnd(stateGroup) {
479  var transitions = stateGroup.transitions;
480  for (var i = 0; i < transitions.length; ++i) {
481  var transition = transitions[i];
482  tryCompare(transition, "running", false, 2000);
483  }
484  }
485 }