@@ -0,0 +1,328 @@
1+/*
2+ * Copyright 2014-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 Unity.Test 0.1 as UT
20+import ".."
21+import "../../../qml/Components"
22+import "../../../qml/Stages"
23+import Ubuntu.Components 1.3
24+import Ubuntu.Components.ListItems 1.3 as ListItem
25+import Unity.Application 0.1
26+
27+Rectangle {
28+ color: "red"
29+ id: root
30+ width: fakeShell.shortestDimension + controls.width
31+ height: fakeShell.longestDimension
32+
33+ property QtObject fakeApplication: null
34+
35+ Item {
36+ id: fakeShell
37+
38+ readonly property real shortestDimension: units.gu(100)
39+ readonly property real longestDimension: units.gu(70)
40+
41+ width: landscape ? longestDimension : shortestDimension
42+ height: landscape ? shortestDimension : longestDimension
43+
44+ x: landscape ? (height - width) / 2 : 0
45+ y: landscape ? (width - height) / 2 : 0
46+
47+ property bool landscape: orientationAngle == 90 || orientationAngle == 270
48+ property int orientationAngle: shellOrientationAngleSelector.value
49+
50+ rotation: orientationAngle
51+
52+ Loader {
53+ id: spreadDelegateLoader
54+ property int requestedX: units.gu(1)
55+ property int requestedY: units.gu(1)
56+ property int requestedWidth: units.gu(50)
57+ property int requestedHeight: units.gu(40)
58+ x: requestedX
59+ y: requestedY
60+ width: item ? item.implicitWidth : 0
61+ height: item ? item.implicitHeight : 0
62+
63+ active: false
64+ property bool itemDestroyed: false
65+ sourceComponent: DecoratedWindow {
66+ anchors.fill: parent
67+ application: fakeApplication
68+ requestedWidth: spreadDelegateLoader.requestedWidth
69+ requestedHeight: spreadDelegateLoader.requestedHeight
70+ surface: fakeApplication && fakeApplication.surfaceList.count > 0 ? fakeApplication.surfaceList.get(0) : null
71+
72+ Component.onDestruction: {
73+ spreadDelegateLoader.itemDestroyed = true;
74+ }
75+ Component.onCompleted: {
76+ spreadDelegateLoader.itemDestroyed = false;
77+ }
78+ }
79+
80+ Rectangle { anchors.fill: parent; color: "green"; opacity: 1 }
81+ }
82+ WindowResizeArea {
83+ target: spreadDelegateLoader
84+ borderThickness: units.gu(2)
85+ }
86+ }
87+
88+
89+ Rectangle {
90+ id: controls
91+ color: "white"
92+ width: units.gu(30)
93+ anchors {
94+ top: parent.top
95+ bottom: parent.bottom
96+ right: parent.right
97+ }
98+
99+ Column {
100+ anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
101+ spacing: units.gu(1)
102+ Button {
103+ id: loadWithWeatherApp
104+ text: "Load with ubuntu-weather-app"
105+ onClicked: { testCase.restartWithApp("ubuntu-weather-app"); }
106+ }
107+ Button {
108+ id: loadWithGalleryApp
109+ text: "Load with gallery-app"
110+ onClicked: { testCase.restartWithApp("gallery-app"); }
111+ }
112+ Row {
113+ anchors { left: parent.left; right: parent.right }
114+ CheckBox { id: swipeToCloseCheckbox; checked: false; }
115+ Label { text: "swipeToCloseEnabled" }
116+ }
117+ Button {
118+ text: "toggle has decoration"
119+ onClicked: spreadDelegateLoader.item.hasDecoration = !spreadDelegateLoader.item.hasDecoration
120+ }
121+ Button {
122+ text: "show/hide decoration"
123+ onClicked: spreadDelegateLoader.item.showDecoration = !spreadDelegateLoader.item.showDecoration
124+ }
125+ Button {
126+ text: "toggle scaleToPreview"
127+ onClicked: spreadDelegateLoader.item.scaleToPreview = !spreadDelegateLoader.item.scaleToPreview
128+ }
129+
130+ Row {
131+ anchors { left: parent.left; right: parent.right }
132+ CheckBox { id: closeableCheckbox; checked: false }
133+ Label { text: "closeable" }
134+ }
135+ ListItem.ItemSelector {
136+ id: shellOrientationAngleSelector
137+ anchors { left: parent.left; right: parent.right }
138+ text: "shellOrientationAngle"
139+ model: ["0", "90", "180", "270"]
140+ property int value: selectedIndex * 90
141+ }
142+ Button {
143+ text: "matchShellOrientation()"
144+ onClicked: { spreadDelegateLoader.item.matchShellOrientation(); }
145+ }
146+ Button {
147+ text: "animateToShellOrientation()"
148+ onClicked: { spreadDelegateLoader.item.animateToShellOrientation(); }
149+ }
150+ }
151+ }
152+
153+ UT.UnityTestCase {
154+ id: testCase
155+ name: "SpreadDelegate"
156+ when: windowShown
157+
158+ SignalSpy {
159+ id: spyClosedSignal
160+ target: spreadDelegateLoader.item
161+ signalName: "closed"
162+ }
163+
164+ property var dragArea
165+ property Item spreadDelegate: spreadDelegateLoader.item
166+
167+ function init() {
168+ }
169+
170+ function cleanup() {
171+ unloadSpreadDelegate();
172+ spyClosedSignal.clear();
173+ shellOrientationAngleSelector.selectedIndex = 0;
174+ root.fakeApplication = null;
175+ killApps();
176+ }
177+
178+ function restartWithApp(appId) {
179+ if (spreadDelegateLoader.active) {
180+ unloadSpreadDelegate();
181+ }
182+ if (root.fakeApplication) {
183+ ApplicationManager.stopApplication(root.fakeApplication.appId);
184+ }
185+
186+ root.fakeApplication = ApplicationManager.startApplication(appId);
187+ spreadDelegateLoader.active = true;
188+ tryCompare(spreadDelegateLoader, "status", Loader.Ready);
189+
190+ dragArea = findInvisibleChild(spreadDelegate, "dragArea");
191+ dragArea.__dateTime = fakeDateTime;
192+ }
193+
194+ function unloadSpreadDelegate() {
195+ spreadDelegateLoader.active = false;
196+ tryCompare(spreadDelegateLoader, "status", Loader.Null);
197+ tryCompare(spreadDelegateLoader, "item", null);
198+ // Loader.status might be Loader.Null and Loader.item might be null but the Loader
199+ // item might still be alive. So if we set Loader.active back to true
200+ // again right now we will get the very same Shell instance back. So no reload
201+ // actually took place. Likely because Loader waits until the next event loop
202+ // iteration to do its work. So to ensure the reload, we will wait until the
203+ // Shell instance gets destroyed.
204+ tryCompare(spreadDelegateLoader, "itemDestroyed", true);
205+
206+ }
207+
208+ function test_swipeToClose_data() {
209+ return [
210+ {tag: "swipeToClose=true closeable=true -> appWindow moves away",
211+ swipeToClose: true, closeable: true },
212+
213+ {tag: "swipeToClose=true closeable=false -> appWindow bounces back",
214+ swipeToClose: true, closeable: false },
215+
216+ {tag: "swipeToClose=false -> appWindow stays put",
217+ swipeToClose: false, closeable: true },
218+ ]
219+ }
220+
221+ function test_swipeToClose(data) {
222+ loadWithGalleryApp.clicked();
223+ var displacedAppWindowWithShadow = findChild(spreadDelegateLoader.item, "displacedAppWindowWithShadow");
224+
225+ verify(displacedAppWindowWithShadow.y === 0);
226+
227+ swipeToCloseCheckbox.checked = data.swipeToClose;
228+ closeableCheckbox.checked = data.closeable;
229+
230+ var dragDistance = spreadDelegateLoader.item.height * 0.8;
231+ var touchX = spreadDelegateLoader.item.width / 2;
232+ var fromY = spreadDelegateLoader.item.height * 0.9;
233+ var toY = fromY - dragDistance;
234+ touchFlick(spreadDelegateLoader.item,
235+ touchX /* fromX */, fromY, touchX /* toX */, toY,
236+ true /* beginTouch */, false /* endTouch */, dragArea.minSpeedToClose * 1.1 /* speed */);
237+
238+ if (data.swipeToClose) {
239+ verify(displacedAppWindowWithShadow.y < 0);
240+ var threshold = findChild(spreadDelegateLoader.item, "dragArea").threshold
241+ if (data.closeable) {
242+ // Verify that the delegate started moving exactly "threshold" after the finger movement
243+ // and did not jump up to the finger, but lags the threshold behind
244+ compare(Math.abs(Math.abs(displacedAppWindowWithShadow.y) - dragDistance), threshold);
245+ } else {
246+ verify(Math.abs(Math.abs(displacedAppWindowWithShadow.y) - dragDistance) > threshold);
247+ }
248+
249+ touchRelease(spreadDelegateLoader.item, touchX, toY - units.gu(1));
250+
251+ waitForCloseAnimationToFinish();
252+
253+ if (data.closeable) {
254+ verify(spyClosedSignal.count === 1);
255+ } else {
256+ verify(spyClosedSignal.count === 0);
257+ tryCompare(displacedAppWindowWithShadow, "y", 0);
258+ }
259+
260+ } else {
261+ verify(displacedAppWindowWithShadow.y === 0);
262+
263+ touchRelease(spreadDelegateLoader.item, touchX, toY);
264+ }
265+ }
266+
267+ function test_loadingLandscapeOnlyAppWhenShellInPortrait() {
268+ loadWithWeatherApp.clicked();
269+
270+ var appWindow = findChild(spreadDelegate, "appWindow");
271+ verify(appWindow);
272+
273+ // It must have landscape dimensions as it does not support portrait
274+ tryCompare(appWindow, "width", fakeShell.height);
275+ tryCompare(appWindow, "height", fakeShell.width - spreadDelegate.maximizedAppTopMargin);
276+ }
277+
278+ function test_keepsSceneTransformationWhenShellRotates_data() {
279+ return [
280+ {tag: "0", selectedIndex: 0},
281+ {tag: "90", selectedIndex: 1},
282+ {tag: "180", selectedIndex: 2},
283+ {tag: "270", selectedIndex: 3}
284+ ];
285+ }
286+ function test_keepsSceneTransformationWhenShellRotates(data) {
287+ loadWithGalleryApp.clicked();
288+
289+ var appWindowWithShadow = findChild(spreadDelegate, "appWindowWithShadow");
290+ verify(appWindowWithShadow);
291+
292+ // Wait until it reaches the state we are interested on.
293+ // It begins with "startingUp"
294+ tryCompare(appWindowWithShadow, "state", "keepSceneRotation");
295+
296+ shellOrientationAngleSelector.selectedIndex = data.selectedIndex;
297+
298+ // must keep same aspect ratio
299+ compare(appWindowWithShadow.width, fakeShell.shortestDimension);
300+ compare(appWindowWithShadow.height, fakeShell.longestDimension);
301+
302+
303+ // and scene transformation must be the identity (ie, no rotation or translation)
304+ var pointInDelegateCoords = appWindowWithShadow.mapToItem(root, 0, 0);
305+ compare(pointInDelegateCoords.x, 0);
306+ compare(pointInDelegateCoords.y, 0);
307+
308+ pointInDelegateCoords = appWindowWithShadow.mapToItem(root,
309+ fakeShell.shortestDimension, fakeShell.longestDimension);
310+ compare(pointInDelegateCoords.x, fakeShell.shortestDimension);
311+ compare(pointInDelegateCoords.y, fakeShell.longestDimension);
312+ }
313+
314+ function waitForCloseAnimationToFinish() {
315+ var closeAnimation = findInvisibleChild(spreadDelegateLoader.item, "closeAnimation");
316+ wait(closeAnimation.duration * 1.5);
317+ tryCompare(closeAnimation, "running", false);
318+ }
319+
320+ function test_showHighLight() {
321+ loadWithGalleryApp.clicked();
322+ var highlightRect = findChild(spreadDelegateLoader.item, "selectionHighlight")
323+ tryCompare(highlightRect, "visible", false)
324+ spreadDelegateLoader.item.highlightShown = true;
325+ tryCompare(highlightRect, "visible", true)
326+ }
327+ }
328+}