@@ -0,0 +1,1078 @@
1+/*
2+ * Copyright (C) 2014 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 QtQuick.Layouts 1.1
19+import QtTest 1.0
20+import Ubuntu.Components 1.1
21+import Ubuntu.Components.ListItems 1.0 as ListItem
22+import Unity.Application 0.1
23+import Unity.Test 0.1
24+import LightDM 0.1 as LightDM
25+
26+import "../../qml"
27+
28+Rectangle {
29+ id: root
30+ color: "grey"
31+ width: units.gu(160) + controls.width
32+ height: units.gu(100)
33+
34+ QtObject {
35+ id: applicationArguments
36+
37+ function hasGeometry() {
38+ return false;
39+ }
40+
41+ function width() {
42+ return 0;
43+ }
44+
45+ function height() {
46+ return 0;
47+ }
48+ property string deviceName: "mako"
49+ }
50+
51+ QtObject {
52+ id: mockOrientationLock
53+ property int savedOrientation
54+ }
55+
56+ QtObject {
57+ id: mockUsageModeSettings
58+ property string usageMode: usageModeSelector.model[usageModeSelector.selectedIndex]
59+ }
60+
61+ property int physicalOrientation0
62+ property int physicalOrientation90
63+ property int physicalOrientation180
64+ property int physicalOrientation270
65+ property real primaryOrientationAngle
66+
67+ state: applicationArguments.deviceName
68+ states: [
69+ State {
70+ name: "mako"
71+ PropertyChanges {
72+ target: orientedShellLoader
73+ width: units.gu(40)
74+ height: units.gu(71)
75+ }
76+ PropertyChanges {
77+ target: root
78+ physicalOrientation0: Qt.PortraitOrientation
79+ physicalOrientation90: Qt.InvertedLandscapeOrientation
80+ physicalOrientation180: Qt.InvertedPortraitOrientation
81+ physicalOrientation270: Qt.LandscapeOrientation
82+ primaryOrientationAngle: 0
83+ }
84+ },
85+ State {
86+ name: "manta"
87+ PropertyChanges {
88+ target: orientedShellLoader
89+ width: units.gu(160)
90+ height: units.gu(100)
91+ }
92+ PropertyChanges {
93+ target: root
94+ physicalOrientation90: Qt.PortraitOrientation
95+ physicalOrientation180: Qt.InvertedLandscapeOrientation
96+ physicalOrientation270: Qt.InvertedPortraitOrientation
97+ physicalOrientation0: Qt.LandscapeOrientation
98+ primaryOrientationAngle: 0
99+ }
100+ },
101+ State {
102+ name: "flo"
103+ PropertyChanges {
104+ target: orientedShellLoader
105+ width: units.gu(62)
106+ height: units.gu(100)
107+ }
108+ PropertyChanges {
109+ target: root
110+ physicalOrientation270: Qt.PortraitOrientation
111+ physicalOrientation0: Qt.InvertedLandscapeOrientation
112+ physicalOrientation90: Qt.InvertedPortraitOrientation
113+ physicalOrientation180: Qt.LandscapeOrientation
114+ primaryOrientationAngle: 90
115+ }
116+ }
117+ ]
118+
119+ Loader {
120+ id: orientedShellLoader
121+
122+ x: ((root.width - controls.width) - width) / 2
123+ y: (root.height - height) / 2
124+
125+ focus: true
126+
127+ property bool itemDestroyed: false
128+ sourceComponent: Component {
129+ OrientedShell {
130+ anchors.fill: parent
131+ usageModeSettings: mockUsageModeSettings
132+ physicalOrientation: root.physicalOrientation0
133+ orientationLocked: orientationLockedCheckBox.checked
134+ orientationLock: mockOrientationLock
135+ Component.onDestruction: {
136+ orientedShellLoader.itemDestroyed = true;
137+ }
138+ }
139+ }
140+ }
141+
142+ function orientationsToStr(orientations) {
143+ if (orientations === Qt.PrimaryOrientation) {
144+ return "Primary";
145+ } else {
146+ var str = "";
147+ if (orientations & Qt.PortraitOrientation) {
148+ str += " Portrait";
149+ }
150+ if (orientations & Qt.InvertedPortraitOrientation) {
151+ str += " InvertedPortrait";
152+ }
153+ if (orientations & Qt.LandscapeOrientation) {
154+ str += " Landscape";
155+ }
156+ if (orientations & Qt.InvertedLandscapeOrientation) {
157+ str += " InvertedLandscape";
158+ }
159+ return str;
160+ }
161+ }
162+
163+ Rectangle {
164+ id: controls
165+ color: "darkgrey"
166+ width: units.gu(30)
167+ anchors {
168+ top: parent.top
169+ bottom: parent.bottom
170+ right: parent.right
171+ }
172+
173+ Column {
174+ anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
175+ spacing: units.gu(1)
176+ Button {
177+ text: "Show Greeter"
178+ activeFocusOnPress: false
179+ onClicked: {
180+ if (orientedShellLoader.status !== Loader.Ready)
181+ return;
182+
183+ LightDM.Greeter.showGreeter();
184+ }
185+ }
186+ Label {
187+ text: "Physical Orientation:"
188+ }
189+ Button {
190+ id: rotate0Button
191+ text: root.orientationsToStr(root.physicalOrientation0) + " (0)"
192+ activeFocusOnPress: false
193+ onClicked: {
194+ orientedShellLoader.item.physicalOrientation = root.physicalOrientation0;
195+ }
196+ }
197+ Button {
198+ id: rotate90Button
199+ text: root.orientationsToStr(root.physicalOrientation90) + " (90)"
200+ activeFocusOnPress: false
201+ onClicked: {
202+ orientedShellLoader.item.physicalOrientation = root.physicalOrientation90;
203+ }
204+ }
205+ Button {
206+ id: rotate180Button
207+ text: root.orientationsToStr(root.physicalOrientation180) + " (180)"
208+ activeFocusOnPress: false
209+ onClicked: {
210+ orientedShellLoader.item.physicalOrientation = root.physicalOrientation180;
211+ }
212+ }
213+ Button {
214+ id: rotate270Button
215+ text: root.orientationsToStr(root.physicalOrientation270) + " (270)"
216+ activeFocusOnPress: false
217+ onClicked: {
218+ orientedShellLoader.item.physicalOrientation = root.physicalOrientation270;
219+ }
220+ }
221+ RowLayout {
222+ Layout.fillWidth: true
223+ CheckBox {
224+ id: orientationLockedCheckBox
225+ checked: false
226+ activeFocusOnPress: false
227+ }
228+ Label {
229+ text: "Orientation Locked"
230+ anchors.verticalCenter: parent.verticalCenter
231+ }
232+ }
233+ Button {
234+ text: "Power dialog"
235+ activeFocusOnPress: false
236+ onClicked: { testCase.showPowerDialog(); }
237+ }
238+ ListItem.ItemSelector {
239+ id: deviceNameSelector
240+ anchors { left: parent.left; right: parent.right }
241+ activeFocusOnPress: false
242+ text: "Device Name"
243+ model: ["mako", "manta", "flo"]
244+ onSelectedIndexChanged: {
245+ testCase.tearDown();
246+ applicationArguments.deviceName = model[selectedIndex];
247+ orientedShellLoader.active = true;
248+ }
249+ }
250+ ListItem.ItemSelector {
251+ id: usageModeSelector
252+ anchors { left: parent.left; right: parent.right }
253+ activeFocusOnPress: false
254+ text: "Usage Mode"
255+ model: ["Staged", "Windowed", "Automatic"]
256+ }
257+ Button {
258+ text: "Switch fullscreen"
259+ activeFocusOnPress: false
260+ onClicked: {
261+ var app = ApplicationManager.findApplication(ApplicationManager.focusedApplicationId);
262+ app.fullscreen = !app.fullscreen;
263+ }
264+ }
265+ }
266+ }
267+
268+ UnityTestCase {
269+ id: testCase
270+ name: "OrientedShell"
271+ when: windowShown
272+
273+ property Item orientedShell: orientedShellLoader.status === Loader.Ready ? orientedShellLoader.item : null
274+ property Item shell
275+
276+ SignalSpy { id: signalSpy }
277+ SignalSpy { id: signalSpy2 }
278+
279+ Connections {
280+ id: spreadRepeaterConnections
281+ ignoreUnknownSignals : true
282+ property var itemAddedCallback: null
283+ onItemAdded: {
284+ if (itemAddedCallback) {
285+ itemAddedCallback(item);
286+ }
287+ }
288+ }
289+
290+ function init() {
291+ if (orientedShellLoader.active) {
292+ // happens for the very first test function as shell
293+ // is loaded by default
294+ tearDown();
295+ }
296+ }
297+
298+ function tearDown() {
299+ orientedShellLoader.itemDestroyed = false;
300+ orientedShellLoader.active = false;
301+
302+ tryCompare(orientedShellLoader, "status", Loader.Null);
303+ tryCompare(orientedShellLoader, "item", null);
304+ // Loader.status might be Loader.Null and Loader.item might be null but the Loader
305+ // actually took place. Likely because Loader waits until the next event loop
306+ // iteration to do its work. So to ensure the reload, we will wait until the
307+ // Shell instance gets destroyed.
308+ tryCompare(orientedShellLoader, "itemDestroyed", true);
309+
310+ // kill all (fake) running apps
311+ killApps();
312+
313+ spreadRepeaterConnections.target = null;
314+ spreadRepeaterConnections.itemAddedCallback = null;
315+ signalSpy.target = null;
316+ signalSpy.signalName = "";
317+
318+ LightDM.Greeter.authenticate(""); // reset greeter
319+ }
320+
321+ function cleanup() {
322+ tryCompare(shell, "enabled", true); // make sure greeter didn't leave us in disabled state
323+ shell = null;
324+
325+ tearDown();
326+ }
327+
328+ function test_appSupportingOnlyPrimaryOrientationMakesShellStayPut_data() {
329+ return [
330+ {tag: "mako", deviceName: "mako"},
331+ {tag: "manta", deviceName: "manta"},
332+ {tag: "flo", deviceName: "flo"}
333+ ];
334+ }
335+ function test_appSupportingOnlyPrimaryOrientationMakesShellStayPut(data) {
336+ loadShell(data.deviceName);
337+
338+ // unity8-dash supports only primary orientation and should be already running
339+ var dashAppWindow = findChild(shell, "appWindow_unity8-dash");
340+ verify(dashAppWindow);
341+ compare(ApplicationManager.focusedApplicationId, "unity8-dash");
342+ var dashApp = dashAppWindow.application
343+ verify(dashApp);
344+ compare(dashApp.rotatesWindowContents, false);
345+ compare(dashApp.supportedOrientations, Qt.PrimaryOrientation);
346+ compare(dashApp.stage, ApplicationInfoInterface.MainStage);
347+
348+ tryCompareFunction(function(){return dashApp.session.surface != null;}, true);
349+ verify(checkAppSurfaceOrientation(dashApp, root.primaryOrientationAngle));
350+
351+ compare(shell.transformRotationAngle, root.primaryOrientationAngle);
352+ rotateTo(90);
353+
354+ verify(checkAppSurfaceOrientation(dashApp, root.primaryOrientationAngle));
355+ compare(shell.transformRotationAngle, root.primaryOrientationAngle);
356+
357+ rotateTo(180);
358+
359+ verify(checkAppSurfaceOrientation(dashApp, root.primaryOrientationAngle));
360+ compare(shell.transformRotationAngle, root.primaryOrientationAngle);
361+
362+ rotateTo(270);
363+
364+ verify(checkAppSurfaceOrientation(dashApp, root.primaryOrientationAngle));
365+ compare(shell.transformRotationAngle, root.primaryOrientationAngle);
366+ }
367+
368+ function test_greeterRemainsInPrimaryOrientation_data() {
369+ return [
370+ {tag: "mako", deviceName: "mako"},
371+ {tag: "manta", deviceName: "manta"},
372+ {tag: "flo", deviceName: "flo"}
373+ ];
374+ }
375+ function test_greeterRemainsInPrimaryOrientation(data) {
376+ loadShell(data.deviceName);
377+
378+ var gmailApp = ApplicationManager.startApplication("gmail-webapp");
379+ verify(gmailApp);
380+
381+ // ensure the mock gmail-webapp is as we expect
382+ compare(gmailApp.rotatesWindowContents, false);
383+ compare(gmailApp.supportedOrientations, Qt.PortraitOrientation | Qt.LandscapeOrientation
384+ | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
385+ compare(gmailApp.stage, ApplicationInfoInterface.MainStage);
386+
387+ // wait until it's able to rotate
388+ tryCompare(shell, "orientationChangesEnabled", true);
389+
390+ compare(shell.transformRotationAngle, root.primaryOrientationAngle);
391+ rotateTo(90);
392+ tryCompare(shell, "transformRotationAngle", root.primaryOrientationAngle + 90);
393+
394+ showGreeter();
395+
396+ tryCompare(shell, "transformRotationAngle", root.primaryOrientationAngle);
397+ rotateTo(180);
398+ compare(shell.transformRotationAngle, root.primaryOrientationAngle);
399+ rotateTo(270);
400+ compare(shell.transformRotationAngle, root.primaryOrientationAngle);
401+ rotateTo(0);
402+ compare(shell.transformRotationAngle, root.primaryOrientationAngle);
403+ }
404+
405+ function test_appRotatesWindowContents_data() {
406+ return [
407+ {tag: "mako", deviceName: "mako", orientationAngleAfterRotation: 90},
408+ {tag: "manta", deviceName: "manta", orientationAngleAfterRotation: 90},
409+ {tag: "flo", deviceName: "flo", orientationAngleAfterRotation: 180}
410+ ];
411+ }
412+ function test_appRotatesWindowContents(data) {
413+ loadShell(data.deviceName);
414+ var cameraApp = ApplicationManager.startApplication("camera-app");
415+ verify(cameraApp);
416+
417+ // ensure the mock camera-app is as we expect
418+ compare(cameraApp.fullscreen, true);
419+ compare(cameraApp.rotatesWindowContents, true);
420+ compare(cameraApp.supportedOrientations, Qt.PortraitOrientation | Qt.LandscapeOrientation
421+ | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
422+
423+ waitUntilAppSurfaceShowsUp("camera-app")
424+
425+ var cameraSurface = cameraApp.session.surface;
426+ verify(cameraSurface);
427+
428+ var focusChangedSpy = signalSpy;
429+ focusChangedSpy.clear();
430+ focusChangedSpy.target = cameraSurface;
431+ focusChangedSpy.signalName = "activeFocusChanged";
432+ verify(focusChangedSpy.valid);
433+
434+ verify(cameraSurface.activeFocus);
435+
436+ tryCompare(shell, "orientationChangesEnabled", true);
437+
438+ var rotationStates = findInvisibleChild(orientedShell, "rotationStates");
439+ verify(rotationStates);
440+ var immediateTransition = null
441+ for (var i = 0; i < rotationStates.transitions.length && !immediateTransition; ++i) {
442+ var transition = rotationStates.transitions[i];
443+ if (transition.objectName == "immediateTransition") {
444+ immediateTransition = transition;
445+ }
446+ }
447+ verify(immediateTransition);
448+ var transitionSpy = signalSpy2;
449+ transitionSpy.clear();
450+ transitionSpy.target = immediateTransition;
451+ transitionSpy.signalName = "runningChanged";
452+ verify(transitionSpy.valid);
453+
454+ rotateTo(90);
455+
456+ tryCompare(cameraSurface, "orientationAngle", data.orientationAngleAfterRotation);
457+
458+ // the rotation should have been immediate
459+ // false -> true -> false
460+ compare(transitionSpy.count, 2);
461+
462+ // It should retain native dimensions regardless of its rotation/orientation
463+ compare(cameraSurface.width, orientedShell.width);
464+ compare(cameraSurface.height, orientedShell.height);
465+
466+ // Surface focus shouldn't have been touched because of the rotation
467+ compare(focusChangedSpy.count, 0);
468+ }
469+
470+ /*
471+ Preconditions:
472+ Shell orientation angle matches the screen one.
473+
474+ Use case:
475+ User switches to an app that has an orientation angle different from the
476+ shell one but that also happens to support the current shell orientation
477+ angle.
478+
479+ Expected outcome:
480+ The app should get rotated to match shell's orientation angle
481+ */
482+ function test_switchingToAppWithDifferentRotation_data() {
483+ return [
484+ {tag: "mako", deviceName: "mako", shellAngleAfterRotation: 90},
485+ {tag: "manta", deviceName: "manta", shellAngleAfterRotation: 90},
486+ {tag: "flo", deviceName: "flo", shellAngleAfterRotation: 180}
487+ ];
488+ }
489+ function test_switchingToAppWithDifferentRotation(data) {
490+ loadShell(data.deviceName);
491+ var gmailApp = ApplicationManager.startApplication("gmail-webapp");
492+ verify(gmailApp);
493+
494+ // ensure the mock gmail-webapp is as we expect
495+ compare(gmailApp.rotatesWindowContents, false);
496+ compare(gmailApp.supportedOrientations, Qt.PortraitOrientation | Qt.LandscapeOrientation
497+ | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
498+ compare(gmailApp.stage, ApplicationInfoInterface.MainStage);
499+
500+ waitUntilAppSurfaceShowsUp("gmail-webapp")
501+
502+ var musicApp = ApplicationManager.startApplication("music-app");
503+ verify(musicApp);
504+
505+ // ensure the mock music-app is as we expect
506+ compare(musicApp.rotatesWindowContents, false);
507+ compare(musicApp.supportedOrientations, Qt.PortraitOrientation | Qt.LandscapeOrientation
508+ | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
509+ compare(musicApp.stage, ApplicationInfoInterface.MainStage);
510+
511+ waitUntilAppSurfaceShowsUp("music-app")
512+ tryCompare(shell, "orientationChangesEnabled", true);
513+
514+ rotateTo(90);
515+ tryCompare(shell, "transformRotationAngle", data.shellAngleAfterRotation);
516+
517+ performEdgeSwipeToSwitchToPreviousApp();
518+
519+ tryCompare(shell, "mainAppWindowOrientationAngle", data.shellAngleAfterRotation);
520+ compare(shell.transformRotationAngle, data.shellAngleAfterRotation);
521+ }
522+
523+ /*
524+ Preconditions:
525+ - Device supports portrait, landscape and inverted-landscape
526+
527+ Steps:
528+ 1 - Launch app that supports all orientations
529+ 2 - Rotate device to inverted-landscape
530+ 3 - See that shell gets rotated to inverted-landscape accordingly
531+ 4 - Rotate device to inverted-portrait
532+
533+ Expected outcome:
534+ Shell stays at inverted-landscape
535+
536+ Actual outcome:
537+ Shell rotates to landscape
538+
539+ Comments:
540+ Rationale being that shell should be rotated to the closest supported orientation.
541+ In that case, landscape and inverted-landscape are both 90 degrees away from the physical
542+ orientation (Screen.orientation), so they are both equally good alternatives
543+ */
544+ function test_rotateToUnsupportedDeviceOrientation(data) {
545+ loadShell("mako");
546+ var twitterApp = ApplicationManager.startApplication("twitter-webapp");
547+ verify(twitterApp);
548+
549+ // ensure the mock twitter-webapp is as we expect
550+ compare(twitterApp.rotatesWindowContents, false);
551+ compare(twitterApp.supportedOrientations, Qt.PortraitOrientation | Qt.LandscapeOrientation
552+ | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
553+
554+ waitUntilAppSurfaceShowsUp("twitter-webapp")
555+
556+ rotateTo(data.rotationAngle);
557+ tryCompare(shell, "transformRotationAngle", data.rotationAngle);
558+
559+ rotateTo(180);
560+ tryCompare(shell, "transformRotationAngle", data.rotationAngle);
561+ }
562+ function test_rotateToUnsupportedDeviceOrientation_data() {
563+ return [
564+ {tag: "90", rotationAngle: 90},
565+ {tag: "270", rotationAngle: 270}
566+ ];
567+ }
568+
569+ function test_launchLandscapeOnlyAppFromPortrait() {
570+ loadShell("mako");
571+ var weatherApp = ApplicationManager.startApplication("ubuntu-weather-app");
572+ verify(weatherApp);
573+
574+ // ensure the mock app is as we expect
575+ compare(weatherApp.supportedOrientations, Qt.LandscapeOrientation | Qt.InvertedLandscapeOrientation);
576+
577+ waitUntilAppSurfaceShowsUp("ubuntu-weather-app");
578+
579+ var rotationStates = findInvisibleChild(orientedShell, "rotationStates");
580+ waitUntilTransitionsEnd(rotationStates);
581+
582+ tryCompareFunction(function (){return shell.transformRotationAngle === 90
583+ || shell.transformRotationAngle === 270;}, true);
584+ }
585+
586+ /*
587+ - launch an app that supports all orientations, such as twitter-webapp
588+ - wait a bit until that app is considered to have finished initializing and is thus
589+ ready to get resized/rotated
590+ - switch back to dash
591+ - rotate device to 90 degrees
592+ - Physical orientation is 90 but Shell orientation is kept at 0 because unity8-dash
593+ doesn't support such orientation
594+ - do a long right-edge drag to show the apps spread
595+ - tap on twitter-webapp
596+
597+ Shell will rotate to match the physical orientation.
598+
599+ This is a kind of tricky case as there are a few things happening at the same time:
600+ 1 - Stage switching from apps spread (phase 2) to showing the focused app (phase 0)
601+ 2 - orientation and aspect ratio (ie size) changes
602+
603+ This may trigger some corner case bugs. such as one were
604+ the greeter is not kept completely outside the shell, causing the black rect in Shell.qml
605+ have an opacity > 0.
606+ */
607+ function test_greeterStaysAwayAfterRotation() {
608+ loadShell("mako");
609+ var twitterApp = ApplicationManager.startApplication("twitter-webapp");
610+ verify(twitterApp);
611+
612+ // ensure the mock twitter-webapp is as we expect
613+ compare(twitterApp.rotatesWindowContents, false);
614+ compare(twitterApp.supportedOrientations, Qt.PortraitOrientation | Qt.LandscapeOrientation
615+ | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
616+
617+ waitUntilAppSurfaceShowsUp("twitter-webapp");
618+ waitUntilAppWindowCanRotate("twitter-webapp");
619+
620+ // go back to unity8-dash
621+ performEdgeSwipeToSwitchToPreviousApp();
622+
623+ rotateTo(90);
624+ wait(1); // spin the event loop to let all bindings do their thing
625+ // should not rotat as unity8-dash doesn't support it
626+ tryCompare(shell, "transformRotationAngle", 0);
627+
628+ performEdgeSwipeToShowAppSpread();
629+
630+ // wait until things have settled
631+ var greeter = findChild(shell, "greeter");
632+ tryCompare(greeter, "animating", false);
633+
634+ var twitterDelegate = findChild(shell, "appDelegate1");
635+ compare(twitterDelegate.application.appId, "twitter-webapp");
636+ twitterDelegate.clicked();
637+
638+ // now it should finally follow the physical orientation
639+ tryCompare(shell, "transformRotationAngle", 90);
640+
641+ // greeter should remaing completely hidden
642+ tryCompare(greeter, "shown", false);
643+ }
644+
645+ function test_appInSideStageDoesntRotateOnStartUp_data() {
646+ return [
647+ {tag: "manta", deviceName: "manta"},
648+ {tag: "flo", deviceName: "flo"}
649+ ];
650+ }
651+ function test_appInSideStageDoesntRotateOnStartUp(data) {
652+ loadShell(data.deviceName);
653+
654+ var twitterDelegate = null;
655+
656+ verify(spreadRepeaterConnections.target);
657+ spreadRepeaterConnections.itemAddedCallback = function(item) {
658+ verify(item.application.appId, "twitter-webapp");
659+ twitterDelegate = item;
660+ signalSpy.target = findInvisibleChild(item, "orientationChangeAnimation");
661+ verify(signalSpy.valid);
662+ }
663+
664+ signalSpy.clear();
665+ signalSpy.target = null;
666+ signalSpy.signalName = "runningChanged";
667+
668+ var twitterApp = ApplicationManager.startApplication("twitter-webapp");
669+ verify(twitterApp);
670+
671+ // ensure the mock twitter-webapp is as we expect
672+ compare(twitterApp.rotatesWindowContents, false);
673+ compare(twitterApp.supportedOrientations, Qt.PortraitOrientation | Qt.LandscapeOrientation
674+ | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
675+ compare(twitterApp.stage, ApplicationInfoInterface.SideStage);
676+
677+ // Wait until spreadRepeaterConnections hs caught the new SpreadDelegate and
678+ // set up the signalSpy target accordingly.
679+ tryCompareFunction(function(){ return signalSpy.target != null && signalSpy.valid; }, true);
680+
681+ tryCompare(twitterDelegate, "orientationChangesEnabled", true);
682+
683+ var appWindowWithShadow = findChild(twitterDelegate, "appWindowWithShadow");
684+ verify(appWindowWithShadow);
685+ tryCompare(appWindowWithShadow, "state", "keepSceneRotation");
686+
687+ // no reason for any rotation animation to have taken place
688+ compare(signalSpy.count, 0);
689+ }
690+
691+ function test_portraitOnlyAppInSideStage_data() {
692+ return [
693+ {tag: "manta", deviceName: "manta"},
694+ {tag: "flo", deviceName: "flo"}
695+ ];
696+ }
697+ function test_portraitOnlyAppInSideStage(data) {
698+ loadShell(data.deviceName);
699+
700+ var dialerDelegate = null;
701+ verify(spreadRepeaterConnections.target);
702+ spreadRepeaterConnections.itemAddedCallback = function(item) {
703+ dialerDelegate = item;
704+ verify(item.application.appId, "dialer-app");
705+ }
706+
707+ var dialerApp = ApplicationManager.startApplication("dialer-app");
708+ verify(dialerApp);
709+
710+ // ensure the mock dialer-app is as we expect
711+ compare(dialerApp.rotatesWindowContents, false);
712+ compare(dialerApp.supportedOrientations, Qt.PortraitOrientation | Qt.InvertedPortraitOrientation);
713+ compare(dialerApp.stage, ApplicationInfoInterface.SideStage);
714+
715+ tryCompareFunction(function(){ return dialerDelegate != null; }, true);
716+ tryCompare(dialerDelegate, "orientationChangesEnabled", true);
717+
718+ var appWindowWithShadow = findChild(dialerDelegate, "appWindowWithShadow");
719+ verify(appWindowWithShadow);
720+ tryCompare(appWindowWithShadow, "state", "keepSceneRotation");
721+
722+ // app must have portrait aspect ratio
723+ verify(appWindowWithShadow.width < appWindowWithShadow.height);
724+
725+ compare(appWindowWithShadow.rotation, 0);
726+
727+ verifyAppWindowWithinSpreadDelegateBoundaries(dialerDelegate);
728+
729+ // shell should remain in its primery orientation as the app in the main stage
730+ // is the one that dictates its orientation. In this case it's unity8-dash
731+ // which supports only primary orientation
732+ compare(shell.orientation, orientedShell.primaryOrientation);
733+ }
734+
735+ function test_sideStageAppsRemainPortraitInSpread_data() {
736+ return [
737+ {tag: "manta", deviceName: "manta"},
738+ {tag: "flo", deviceName: "flo"}
739+ ];
740+ }
741+ function test_sideStageAppsRemainPortraitInSpread(data) {
742+ loadShell(data.deviceName);
743+
744+
745+ ////
746+ // Launch dialer
747+ var dialerDelegate = null;
748+ verify(spreadRepeaterConnections.target);
749+ spreadRepeaterConnections.itemAddedCallback = function(item) {
750+ dialerDelegate = item;
751+ verify(item.application.appId, "dialer-app");
752+ }
753+
754+ var dialerApp = ApplicationManager.startApplication("dialer-app");
755+ verify(dialerApp);
756+
757+ // ensure the mock dialer-app is as we expect
758+ compare(dialerApp.rotatesWindowContents, false);
759+ compare(dialerApp.supportedOrientations, Qt.PortraitOrientation | Qt.InvertedPortraitOrientation);
760+ compare(dialerApp.stage, ApplicationInfoInterface.SideStage);
761+
762+ tryCompareFunction(function(){ return dialerDelegate != null; }, true);
763+ waitUntilAppDelegateIsFullyInit(dialerDelegate);
764+
765+
766+ ////
767+ // Launch twitter
768+ var twitterDelegate = null;
769+ spreadRepeaterConnections.itemAddedCallback = function(item) {
770+ twitterDelegate = item;
771+ verify(item.application.appId, "twitter-webapp");
772+ }
773+
774+ var twitterApp = ApplicationManager.startApplication("twitter-webapp");
775+ verify(twitterApp);
776+
777+ // ensure the mock twitter-webapp is as we expect
778+ compare(twitterApp.rotatesWindowContents, false);
779+ compare(twitterApp.supportedOrientations, Qt.PortraitOrientation | Qt.LandscapeOrientation
780+ | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
781+ compare(twitterApp.stage, ApplicationInfoInterface.SideStage);
782+
783+ tryCompareFunction(function(){ return twitterDelegate != null; }, true);
784+ waitUntilAppDelegateIsFullyInit(twitterDelegate);
785+
786+ ////
787+ // Edge swipe to show spread and check orientations
788+
789+ performEdgeSwipeToShowAppSpread();
790+
791+ {
792+ var appWindowWithShadow = findChild(dialerDelegate, "appWindowWithShadow");
793+ verify(appWindowWithShadow);
794+ tryCompare(appWindowWithShadow, "state", "keepSceneRotation");
795+ compare(appWindowWithShadow.width, dialerDelegate.width);
796+ compare(appWindowWithShadow.height, dialerDelegate.height);
797+ compare(appWindowWithShadow.rotation, 0);
798+ }
799+ {
800+ var appWindowWithShadow = findChild(twitterDelegate, "appWindowWithShadow");
801+ verify(appWindowWithShadow);
802+ tryCompare(appWindowWithShadow, "state", "keepSceneRotation");
803+ compare(appWindowWithShadow.width, twitterDelegate.width);
804+ compare(appWindowWithShadow.height, twitterDelegate.height);
805+ compare(appWindowWithShadow.rotation, 0);
806+ }
807+
808+ verifyAppWindowWithinSpreadDelegateBoundaries(dialerDelegate);
809+ verifyAppWindowWithinSpreadDelegateBoundaries(twitterDelegate);
810+ }
811+
812+ function test_launchedAppHasActiveFocus_data() {
813+ return [
814+ {tag: "mako", deviceName: "mako"},
815+ {tag: "manta", deviceName: "manta"},
816+ {tag: "flo", deviceName: "flo"}
817+ ];
818+ }
819+ function test_launchedAppHasActiveFocus(data) {
820+ loadShell(data.deviceName);
821+
822+ var gmailApp = ApplicationManager.startApplication("gmail-webapp");
823+ verify(gmailApp);
824+ waitUntilAppSurfaceShowsUp("gmail-webapp")
825+
826+ verify(gmailApp.session.surface);
827+
828+ tryCompare(gmailApp.session.surface, "activeFocus", true);
829+ }
830+
831+ function test_launchLandscapeOnlyAppOverPortraitOnlyDashThenSwitchToDash() {
832+ loadShell("mako");
833+
834+ // starts as portrait, as unity8-dash is portrait only
835+ tryCompare(shell, "transformRotationAngle", 0);
836+
837+ var weatherApp = ApplicationManager.startApplication("ubuntu-weather-app");
838+ verify(weatherApp);
839+
840+ // ensure the mock app is as we expect
841+ compare(weatherApp.supportedOrientations, Qt.LandscapeOrientation | Qt.InvertedLandscapeOrientation);
842+
843+ waitUntilAppSurfaceShowsUp("ubuntu-weather-app");
844+
845+ // should have rotated to landscape
846+ tryCompareFunction(function () { return shell.transformRotationAngle == 270
847+ || shell.transformRotationAngle == 90; }, true);
848+
849+ var rotationStates = findInvisibleChild(orientedShell, "rotationStates");
850+ waitUntilTransitionsEnd(rotationStates);
851+
852+ performLeftEdgeSwipeToSwitchToDash();
853+
854+ // Should be back to portrait
855+ tryCompare(shell, "transformRotationAngle", 0);
856+ }
857+
858+ // angle - rotation angle in degrees clockwise, relative to the primary orientation.
859+ function rotateTo(angle) {
860+ switch (angle) {
861+ case 0:
862+ rotate0Button.clicked();
863+ break;
864+ case 90:
865+ rotate90Button.clicked();
866+ break;
867+ case 180:
868+ rotate180Button.clicked();
869+ break;
870+ case 270:
871+ rotate270Button.clicked();
872+ break;
873+ default:
874+ verify(false);
875+ }
876+
877+ var rotationStates = findInvisibleChild(orientedShell, "rotationStates");
878+ waitUntilTransitionsEnd(rotationStates);
879+ }
880+
881+ function waitUntilAppDelegateIsFullyInit(spreadDelegate) {
882+ tryCompare(spreadDelegate, "orientationChangesEnabled", true);
883+
884+ var appWindowWithShadow = findChild(spreadDelegate, "appWindowWithShadow");
885+ verify(appWindowWithShadow);
886+ tryCompare(appWindowWithShadow, "state", "keepSceneRotation");
887+
888+ var appWindowStates = findInvisibleChild(appWindowWithShadow, "applicationWindowStateGroup");
889+ verify(appWindowStates);
890+ tryCompare(appWindowStates, "state", "surface");
891+ }
892+
893+ function waitUntilAppSurfaceShowsUp(appId) {
894+ var appWindow = findChild(shell, "appWindow_" + appId);
895+ verify(appWindow);
896+ var appWindowStates = findInvisibleChild(appWindow, "applicationWindowStateGroup");
897+ verify(appWindowStates);
898+ tryCompare(appWindowStates, "state", "surface");
899+ }
900+
901+ function waitUntilAppWindowCanRotate(appId) {
902+ var appWindow = findChild(shell, "appWindow_" + appId);
903+ verify(appWindow);
904+ tryCompare(appWindow, "orientationChangesEnabled", true);
905+ }
906+
907+ function waitUntilShellIsInOrientation(orientation) {
908+ tryCompare(shell, "orientation", orientation);
909+ var rotationStates = findInvisibleChild(orientedShell, "rotationStates");
910+ waitUntilTransitionsEnd(rotationStates);
911+ }
912+
913+ function performLeftEdgeSwipeToSwitchToDash() {
914+ var spreadView = findChild(shell, "spreadView");
915+ var swipeLength = spreadView.width * 0.7;
916+
917+ var touchStartX = 1;
918+ var touchStartY = shell.height / 2;
919+ touchFlick(shell,
920+ touchStartX, touchStartY,
921+ touchStartX + swipeLength, touchStartY);
922+
923+ tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
924+ }
925+
926+ function performEdgeSwipeToSwitchToPreviousApp() {
927+ // swipe just enough to ensure an app switch action.
928+ // If we swipe too much we will trigger the spread mode
929+ // and we don't want that.
930+ var spreadView = findChild(shell, "spreadView");
931+ var swipeLength = (spreadView.positionMarker1 * spreadView.width) * 0.5;
932+
933+ verify(ApplicationManager.count >= 2);
934+ var previousApp = ApplicationManager.get(1);
935+
936+ var touchStartX = shell.width - 1;
937+ var touchStartY = shell.height / 2;
938+ touchFlick(shell,
939+ touchStartX, touchStartY,
940+ touchStartX - swipeLength, touchStartY);
941+
942+ // ensure the app switch animation has ended
943+ tryCompare(spreadView, "shiftedContentX", 0);
944+
945+ compare(ApplicationManager.get(0).appId, previousApp.appId);
946+ }
947+
948+ function performEdgeSwipeToShowAppSpread() {
949+ var touchStartY = shell.height / 2;
950+ touchFlick(shell,
951+ shell.width - 1, touchStartY,
952+ 0, touchStartY);
953+
954+ var spreadView = findChild(shell, "spreadView");
955+ verify(spreadView);
956+ tryCompare(spreadView, "phase", 2);
957+ tryCompare(spreadView, "flicking", false);
958+ tryCompare(spreadView, "moving", false);
959+ }
960+
961+ function showPowerDialog() {
962+ var dialogs = findChild(orientedShell, "dialogs");
963+ var dialogsPrivate = findInvisibleChild(dialogs, "dialogsPrivate");
964+ dialogsPrivate.showPowerDialog();
965+ }
966+
967+ function verifyAppWindowWithinSpreadDelegateBoundaries(spreadDelegate) {
968+ var appWindowWithShadow = findChild(spreadDelegate, "appWindowWithShadow");
969+ verify(appWindowWithShadow);
970+
971+ var windowInDelegateCoords = appWindowWithShadow.mapToItem(spreadDelegate, 0, 0,
972+ appWindowWithShadow.width, appWindowWithShadow.height);
973+
974+ compare(windowInDelegateCoords.x, 0);
975+ compare(windowInDelegateCoords.y, 0);
976+ compare(windowInDelegateCoords.width, spreadDelegate.width);
977+ compare(windowInDelegateCoords.height, spreadDelegate.height);
978+ }
979+
980+ function swipeAwayGreeter() {
981+ var greeter = findChild(shell, "greeter");
982+ tryCompare(greeter, "fullyShown", true);
983+
984+ var touchX = shell.width - (shell.edgeSize / 2);
985+ var touchY = shell.height / 2;
986+ touchFlick(shell, touchX, touchY, shell.width * 0.1, touchY);
987+
988+ // wait until the animation has finished
989+ tryCompare(greeter, "shown", false);
990+ waitForRendering(greeter);
991+ }
992+
993+ function showGreeter() {
994+ LightDM.Greeter.showGreeter();
995+ // wait until the animation has finished
996+ var greeter = findChild(shell, "greeter");
997+ tryCompare(greeter, "fullyShown", true);
998+ }
999+
1000+ function killApps() {
1001+ while (ApplicationManager.count > 1) {
1002+ var appIndex = ApplicationManager.get(0).appId == "unity8-dash" ? 1 : 0
1003+ ApplicationManager.stopApplication(ApplicationManager.get(appIndex).appId);
1004+ }
1005+ compare(ApplicationManager.count, 1)
1006+ }
1007+
1008+ function loadShell(deviceName) {
1009+ applicationArguments.deviceName = deviceName;
1010+
1011+ // reload our test subject to get it in a fresh state once again
1012+ orientedShellLoader.active = true;
1013+
1014+ tryCompare(orientedShellLoader, "status", Loader.Ready);
1015+ removeTimeConstraintsFromDirectionalDragAreas(orientedShellLoader.item);
1016+
1017+ shell = findChild(orientedShell, "shell");
1018+
1019+ tryCompare(shell, "enabled", true); // enabled by greeter when ready
1020+
1021+ waitUntilShellIsInOrientation(root.physicalOrientation0);
1022+
1023+ swipeAwayGreeter();
1024+
1025+ var spreadRepeater = findChild(shell, "spreadRepeater");
1026+ if (spreadRepeater) {
1027+ spreadRepeaterConnections.target = spreadRepeater;
1028+ }
1029+ }
1030+
1031+ // expectedAngle is in orientedShell's coordinate system
1032+ function checkAppSurfaceOrientation(app, expectedAngle) {
1033+ var surface = app.session.surface;
1034+ if (!surface) {
1035+ console.warn("no surface");
1036+ return false;
1037+ }
1038+
1039+ if (!itemIsChildOfOrientedShell(surface)) {
1040+ console.warn("surface not a child of OrientedShell");
1041+ return false;
1042+ }
1043+
1044+ var topMargin = 0.;
1045+ if (!app.fullscreen) {
1046+ var appsDisplayLoader = findChild(shell, "applicationsDisplayLoader");
1047+ verify(appsDisplayLoader);
1048+ verify(appsDisplayLoader.item);
1049+ verify(appsDisplayLoader.item.maximizedAppTopMargin !== undefined);
1050+ topMargin = appsDisplayLoader.item.maximizedAppTopMargin;
1051+ }
1052+
1053+ var point = surface.mapToItem(orientedShell, 0, 0);
1054+
1055+ switch (expectedAngle) {
1056+ case 0:
1057+ return point.x === 0. && point.y === topMargin;
1058+ case 90:
1059+ return point.x === orientedShell.width - topMargin && point.y === 0;
1060+ case 180:
1061+ return point.x === orientedShell.width && point.y === orientedShell.height - topMargin;
1062+ default: // 270
1063+ return point.x === topMargin && point.y === orientedShell.height;
1064+ }
1065+ }
1066+
1067+ function itemIsChildOfOrientedShell(item) {
1068+ var parent = item.parent;
1069+ var found = false;
1070+ while (parent && !found) {
1071+ found = parent === orientedShell;
1072+ parent = parent.parent;
1073+ }
1074+ return found;
1075+ }
1076+ }
1077+
1078+}