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