on xenial help sign in
Refactor the greeter code to be more compartmented and isolatable.

This will help make it an optional piece of the shell when we allow a split greeter/shell. My driving goals were: - Remove as much logic from Shell.qml as possible. Right now, there is so much Greeter logic spread all throughout Shell.qml in different bits. For example, the Lockscreen widget sitting there instead of in qml/Greeter/. - Clean up how we load the two different views (tablet & phone). Right now we have some widgets always present but sometimes hidden (Lockscreen) and Loaders within Loaders (LoginList inside GreeterContent). I wanted just two top-level view widgets that could be separately loaded depending on which mode we are in. - Toward that end, I wanted to consolidate all the Greeter logic out of Shell and out of those two view classes to avoid duplication. So I made the main Greeter entry point widget handle all the login logic and interactions with LightDM. It controls the views, who always ask it to do things on their behalf. Classic MVC stuff. - I separated out some of the widgetry from the previous Greeter class into a CoverPage class that both NarrowView and WideView use. This class now holds the drag handle for sliding the greeter away and the infographic. - Now that each piece of the greeter has a much more brightly defined boundary, testing them in isolation is much easier. I've added tst_WideView, tst_NarrowView, and tst_Greeter files. I've moved existing tests around a bit and added some more. But it should be much more clear where a prospective test belongs now (logic tests in Greeter, visual tests in the respective View). - I also moved some tests we had in qmluitests/ that were purely testing the LightDM plugin into their own plugin test file. Approved by: Daniel d'Andrada

commita720b0b601881903d346b8c15aa440aa6f894723 authorMichael Terry <michael.terry@canonical.com> dateTue 24 Feb 2015, 10:15 (12 years ago) parentf84db1b3b68792
29 files changed+2948−1662
cmake/modules/QmlTest.cmake+1−1 qml/Components/DelayedLockscreen.qml+3−1 qml/Components/PassphraseLockscreen.qml+2−3 qml/Components/PinLockscreen.qml+3−4 qml/Greeter/CoverPage.qml+216 qml/Greeter/Greeter.qml+319−147 qml/Greeter/GreeterContent.qml−163 qml/Greeter/Infographics.qml+1 qml/Greeter/LoginList.qml+67−85 qml/Greeter/NarrowView.qml+170 qml/Greeter/WideView.qml+134 qml/Shell.qml+67−353 tests/autopilot/unity8/shell/emulators/greeter.py+19−3 tests/autopilot/unity8/shell/emulators/main_window.py−6 tests/autopilot/unity8/shell/tests/__init__.py+2−3 tests/autopilot/unity8/shell/tests/test_lock_screen.py+8−12 tests/plugins/LightDM/CMakeLists.txt+19−4 tests/plugins/LightDM/usersmodel.cpp+84 tests/qmltests/CMakeLists.txt+4−3 tests/qmltests/Greeter/TestView.qml+99 tests/qmltests/Greeter/tst_Greeter.qml+472 tests/qmltests/Greeter/tst_MultiGreeter.qml−475 tests/qmltests/Greeter/tst_NarrowView.qml+522 tests/qmltests/Greeter/tst_SingleGreeter.qml−244 tests/qmltests/Greeter/tst_WideView.qml+519 tests/qmltests/Tutorial/tst_Tutorial.qml+5−4 tests/qmltests/tst_Shell.qml+65−43 tests/qmltests/tst_ShellWithPin.qml+97−48 tests/qmltests/tst_TabletShell.qml+50−60
cmake/modules/QmlTest.cmake+1−1
@@ -28,7 +28,7 @@ endif()
2828
2929set(qmlscene_exe ${CMAKE_BINARY_DIR}/tests/uqmlscene/uqmlscene)
3030
31−set(test_env UNITY_TESTING=1)
31+set(test_env LC_ALL=C UNITY_TESTING=1)
3232
3333macro(add_manual_qml_test SUBPATH COMPONENT_NAME)
3434 set(options NO_ADD_TEST NO_TARGETS)
qml/Components/DelayedLockscreen.qml+3−1
@@ -20,11 +20,13 @@ import "../Components"
2020
2121Item {
2222 id: root
23− anchors.fill: parent
2423
2524 property int delayMinutes
2625 property bool alphaNumeric
2726
27+ signal entered(string passphrase) // unused
28+ signal cancel() // unused
29+
2830 function clear(playAnimation) {}
2931
3032 Column {
qml/Components/PassphraseLockscreen.qml+2−3
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013 Canonical, Ltd.
2+ * Copyright (C) 2013,2014,2015 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -20,8 +20,7 @@ import "../Components"
2020
2121Item {
2222 id: root
23− anchors.top: parent.top
24− anchors.topMargin: units.gu(4)
23+ y: units.gu(4)
2524 height: shakeContainer.height
2625
2726 property string infoText
qml/Components/PinLockscreen.qml+3−4
@@ -21,9 +21,7 @@ import "../Components"
2121
2222Column {
2323 id: root
24− anchors.top: parent.top
25− anchors.topMargin: units.gu(4)
26− anchors.horizontalCenter: parent.horizontalCenter
24+ y: units.gu(4)
2725 spacing: units.gu(2)
2826
2927 property string infoText
@@ -123,6 +121,7 @@ Column {
123121 objectName: "backspaceIcon"
124122 anchors { right: parent.right; top: parent.top; bottom: parent.bottom }
125123 width: height
124+ enabled: root.entryEnabled
126125
127126 Icon {
128127 anchors.fill: parent
@@ -130,7 +129,7 @@ Column {
130129 color: "#f3f3e7"
131130 }
132131
133− opacity: (pinentryField.text.length && !pinentryField.incorrectOverride) > 0 ? 1 : 0
132+ opacity: (pinentryField.text.length > 0 && !pinentryField.incorrectOverride) ? 1 : 0
134133
135134 Behavior on opacity {
136135 UbuntuNumberAnimation {}
qml/Greeter/CoverPage.qmlnew file+216
@@ -0,0 +1,216 @@
1+/*
2+ * Copyright (C) 2013,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.3
18+import Ubuntu.Components 1.1
19+import Ubuntu.Gestures 0.1
20+import "../Components"
21+
22+Showable {
23+ id: root
24+
25+ property real dragHandleLeftMargin
26+ property real launcherOffset
27+ property alias background: greeterBackground.source
28+ property real backgroundTopMargin
29+ property var infographicModel
30+ property bool draggable: true
31+
32+ property alias infographics: infographics
33+
34+ readonly property real showProgress: MathUtils.clamp((width - Math.abs(x)) / width, 0, 1)
35+
36+ signal tease()
37+
38+ function hideRight() {
39+ d.forceRightOnNextHideAnimation = true;
40+ hide();
41+ }
42+
43+ QtObject {
44+ id: d
45+ property bool forceRightOnNextHideAnimation: false
46+ }
47+
48+ prepareToHide: function () {
49+ hideTranslation.from = root.x + translation.x
50+ hideTranslation.to = root.x > 0 || d.forceRightOnNextHideAnimation ? root.width : -root.width;
51+ d.forceRightOnNextHideAnimation = false;
52+ }
53+
54+ // We don't directly bind "x" because that's owned by the DragHandle. So
55+ // instead, we can get a little extra horizontal push by using transforms.
56+ transform: Translate { id: translation; x: root.draggable ? launcherOffset : 0 }
57+
58+ MouseArea { anchors.fill: parent; }
59+
60+ Rectangle {
61+ // In case background fails to load
62+ id: backgroundBackup
63+ anchors.fill: parent
64+ color: "black"
65+ }
66+
67+ CrossFadeImage {
68+ id: greeterBackground
69+ objectName: "greeterBackground"
70+ anchors {
71+ fill: parent
72+ topMargin: root.backgroundTopMargin
73+ }
74+ fillMode: Image.PreserveAspectCrop
75+ // Limit how much memory we'll reserve for this image
76+ sourceSize.height: height
77+ sourceSize.width: width
78+ }
79+
80+ Rectangle {
81+ anchors.fill: parent
82+ color: "black"
83+ opacity: 0.4
84+ }
85+
86+ Infographics {
87+ id: infographics
88+ objectName: "infographics"
89+ height: parent.height
90+ model: root.infographicModel
91+ clip: true // clip large data bubbles
92+
93+ anchors {
94+ verticalCenter: parent.verticalCenter
95+ left: parent.left
96+ right: parent.right
97+ }
98+ }
99+
100+ Label {
101+ id: swipeHint
102+ property real baseOpacity: 0.5
103+ opacity: 0.0
104+ anchors.horizontalCenter: parent.horizontalCenter
105+ anchors.bottom: parent.bottom
106+ anchors.bottomMargin: units.gu(5)
107+ text: "《 " + i18n.tr("Unlock") + " 》"
108+ color: "white"
109+ font.weight: Font.Light
110+
111+ SequentialAnimation on opacity {
112+ id: showLabelAnimation
113+ running: false
114+ loops: 2
115+
116+ StandardAnimation {
117+ from: 0.0
118+ to: swipeHint.baseOpacity
119+ duration: UbuntuAnimation.SleepyDuration
120+ }
121+ PauseAnimation { duration: UbuntuAnimation.BriskDuration }
122+ StandardAnimation {
123+ from: swipeHint.baseOpacity
124+ to: 0.0
125+ duration: UbuntuAnimation.SleepyDuration
126+ }
127+ }
128+ }
129+
130+ DragHandle {
131+ id: dragHandle
132+ anchors.fill: parent
133+ anchors.leftMargin: root.dragHandleLeftMargin
134+ enabled: root.draggable
135+ direction: Direction.Horizontal
136+
137+ onDraggingChanged: {
138+ if (dragging) {
139+ root.tease();
140+ showLabelAnimation.start();
141+ }
142+ }
143+ }
144+
145+ // right side shadow
146+ Image {
147+ anchors.left: parent.right
148+ anchors.top: parent.top
149+ anchors.bottom: parent.bottom
150+ fillMode: Image.Tile
151+ source: "../graphics/dropshadow_right.png"
152+ }
153+
154+ // left side shadow
155+ Image {
156+ anchors.right: parent.left
157+ anchors.top: parent.top
158+ anchors.bottom: parent.bottom
159+ fillMode: Image.Tile
160+ source: "../graphics/dropshadow_left.png"
161+ }
162+
163+ Binding {
164+ id: positionLock
165+
166+ property bool enabled: false
167+ onEnabledChanged: {
168+ if (enabled === __enabled) {
169+ return;
170+ }
171+
172+ if (enabled) {
173+ if (root.x > 0) {
174+ value = Qt.binding(function() { return root.width; })
175+ } else {
176+ value = Qt.binding(function() { return -root.width; })
177+ }
178+ }
179+
180+ __enabled = enabled;
181+ }
182+
183+ property bool __enabled: false
184+
185+ target: root
186+ when: __enabled
187+ property: "x"
188+ }
189+
190+ hideAnimation: SequentialAnimation {
191+ id: hideAnimation
192+ objectName: "hideAnimation"
193+ property var target // unused, here to silence Showable warning
194+ StandardAnimation {
195+ id: hideTranslation
196+ property: "x"
197+ target: root
198+ }
199+ PropertyAction { target: root; property: "visible"; value: false }
200+ PropertyAction { target: positionLock; property: "enabled"; value: true }
201+ }
202+
203+ showAnimation: SequentialAnimation {
204+ id: showAnimation
205+ objectName: "showAnimation"
206+ property var target // unused, here to silence Showable warning
207+ PropertyAction { target: root; property: "visible"; value: true }
208+ PropertyAction { target: positionLock; property: "enabled"; value: false }
209+ StandardAnimation {
210+ property: "x"
211+ target: root
212+ to: 0
213+ duration: UbuntuAnimation.FastDuration
214+ }
215+ }
216+}
qml/Greeter/Greeter.qml+319−147
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013 Canonical, Ltd.
2+ * Copyright (C) 2013,2014,2015 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -14,66 +14,185 @@
1414 * along with this program. If not, see <http://www.gnu.org/licenses/>.
1515 */
1616
17−import QtQuick 2.0
18−import Ubuntu.Components 0.1
19−import Ubuntu.Gestures 0.1
17+import QtQuick 2.3
18+import AccountsService 0.1
2019import LightDM 0.1 as LightDM
20+import Ubuntu.Components 1.1
21+import Ubuntu.SystemImage 0.1
22+import Unity.Connectivity 0.1
23+import Unity.Launcher 0.1
2124import "../Components"
2225
2326Showable {
24− id: greeter
25− enabled: shown
26− created: greeterContentLoader.status == Loader.Ready && greeterContentLoader.item.ready
27+ id: root
28+ created: loader.status == Loader.Ready
2729
2830 property real dragHandleLeftMargin: 0
29− property alias dragging: dragHandle.dragging
3031
3132 property url background
3233
33− // so that it can be replaced in tests with a mock object
34− property var inputMethod: Qt.inputMethod
34+ // How far to offset the top greeter layer during a launcher left-drag
35+ property real launcherOffset
3536
36− prepareToHide: function () {
37− hideTranslation.to = greeter.x > 0 || d.forceRightOnNextHideAnimation ? greeter.width : -greeter.width;
38− d.forceRightOnNextHideAnimation = false;
37+ readonly property bool active: shown || hasLockedApp
38+ readonly property bool fullyShown: loader.item ? loader.item.fullyShown : false
39+
40+ // True when the greeter is waiting for PAM or other setup process
41+ readonly property alias waiting: d.waiting
42+
43+ property string lockedApp: ""
44+ readonly property bool hasLockedApp: lockedApp !== ""
45+
46+ property bool forcedUnlock
47+ readonly property bool locked: LightDM.Greeter.active && !LightDM.Greeter.authenticated && !forcedUnlock
48+
49+ property bool tabletMode
50+ property url viewSource // only used for testing
51+
52+ property int maxFailedLogins: -1 // disabled by default for now, will enable via settings in future
53+ property int failedLoginsDelayAttempts: 7 // number of failed logins
54+ property int failedLoginsDelayMinutes: 5 // minutes of forced waiting
55+
56+ signal tease()
57+ signal sessionStarted()
58+ signal emergencyCall()
59+
60+ function forceShow() {
61+ showNow();
62+ loader.item.reset();
3963 }
4064
41− QtObject {
42− id: d
43− property bool forceRightOnNextHideAnimation: false
65+ function notifyAppFocused(appId) {
66+ if (!active) {
67+ return;
68+ }
69+
70+ if (hasLockedApp) {
71+ if (appId === lockedApp) {
72+ hide(); // show locked app
73+ } else {
74+ show();
75+ d.startUnlock(false /* toTheRight */);
76+ }
77+ } else if (appId !== "unity8-dash") { // dash isn't started by user
78+ d.startUnlock(false /* toTheRight */);
79+ }
4480 }
4581
46− property bool loadContent: required
82+ function notifyAboutToFocusApp(appId) {
83+ if (!active) {
84+ return;
85+ }
4786
48− // 1 when fully shown and 0 when fully hidden
49− property real showProgress: visible ? MathUtils.clamp((width - Math.abs(x)) / width, 0, 1) : 0
87+ // A hint that we're about to focus an app. This way we can look
88+ // a little more responsive, rather than waiting for the above
89+ // notifyAppFocused call. We also need this in case we have a locked
90+ // app, in order to show lockscreen instead of new app.
91+ d.startUnlock(false /* toTheRight */);
92+ }
5093
51− property alias model: greeterContentLoader.model
52− property bool locked: true
94+ // This is a just a glorified notifyAboutToFocusApp(), but it does one
95+ // other thing: it hides any cover pages to the RIGHT, because the user
96+ // just came from a launcher drag starting on the left.
97+ // It also returns a boolean value, indicating whether there was a visual
98+ // change or not (the shell only wants to hide the launcher if there was
99+ // a change).
100+ function notifyShowingDashFromDrag() {
101+ if (!active) {
102+ return false;
103+ }
53104
54− readonly property bool narrowMode: !multiUser && height > width
55− readonly property bool multiUser: LightDM.Users.count > 1
105+ return d.startUnlock(true /* toTheRight */);
106+ }
56107
57− readonly property int currentIndex: greeterContentLoader.currentIndex
108+ QtObject {
109+ id: d
58110
59− signal selected(int uid)
60− signal unlocked(int uid)
61− signal tapped()
111+ readonly property bool multiUser: LightDM.Users.count > 1
112+ property int currentIndex
113+ property int delayMinutes
114+ property bool waiting
115+
116+ // We want 'launcherOffset' to animate down to zero. But not to animate
117+ // while being dragged. So ideally we change this only when the user
118+ // lets go and launcherOffset drops to zero. But we need to wait for
119+ // the behavior to be enabled first. So we cache the last known good
120+ // launcherOffset value to cover us during that brief gap between
121+ // release and the behavior turning on.
122+ property real lastKnownPositiveOffset // set in a launcherOffsetChanged below
123+ property real launcherOffsetProxy: (shown && !launcherOffsetProxyBehavior.enabled) ? lastKnownPositiveOffset : 0
124+ Behavior on launcherOffsetProxy {
125+ id: launcherOffsetProxyBehavior
126+ enabled: launcherOffset === 0
127+ UbuntuNumberAnimation {}
128+ }
129+
130+ function selectUser(uid, reset) {
131+ d.waiting = true;
132+ if (reset) {
133+ loader.item.reset();
134+ }
135+ currentIndex = uid;
136+ var user = LightDM.Users.data(uid, LightDM.UserRoles.NameRole);
137+ AccountsService.user = user;
138+ LauncherModel.setUser(user);
139+ LightDM.Greeter.authenticate(user); // always resets auth state
140+ }
141+
142+ function login() {
143+ enabled = false;
144+ if (LightDM.Greeter.startSessionSync()) {
145+ sessionStarted();
146+ loader.item.notifyAuthenticationSucceeded();
147+ } else {
148+ loader.item.notifyAuthenticationFailed();
149+ }
150+ enabled = true;
151+ }
62152
63− function hideRight() {
64− d.forceRightOnNextHideAnimation = true;
65− hide();
153+ function startUnlock(toTheRight) {
154+ if (loader.item) {
155+ return loader.item.tryToUnlock(toTheRight);
156+ } else {
157+ return false;
158+ }
159+ }
66160 }
67161
68− function tryToUnlock() {
69− if (created) {
70− greeterContentLoader.item.tryToUnlock()
162+ onLauncherOffsetChanged: {
163+ if (launcherOffset > 0) {
164+ d.lastKnownPositiveOffset = launcherOffset;
71165 }
72166 }
73167
74− function reset() {
75− if (created) {
76− greeterContentLoader.item.reset()
168+ onForcedUnlockChanged: {
169+ if (forcedUnlock && shown) {
170+ // pretend we were just authenticated
171+ loader.item.notifyAuthenticationSucceeded();
172+ }
173+ }
174+
175+ onRequiredChanged: {
176+ if (required) {
177+ d.waiting = true;
178+ lockedApp = "";
179+ }
180+ }
181+
182+ Component.onCompleted: {
183+ Connectivity.unlockAllModems();
184+ }
185+
186+ Timer {
187+ id: forcedDelayTimer
188+ interval: 1000 * 60
189+ onTriggered: {
190+ if (d.delayMinutes > 0) {
191+ d.delayMinutes -= 1;
192+ if (d.delayMinutes > 0) {
193+ start(); // go again
194+ }
195+ }
77196 }
78197 }
79198
@@ -82,152 +201,205 @@ Showable {
82201 MouseArea { anchors.fill: parent }
83202
84203 Loader {
85− id: greeterContentLoader
86− objectName: "greeterContentLoader"
204+ id: loader
205+ objectName: "loader"
206+
87207 anchors.fill: parent
88− property var model: LightDM.Users
89− property int currentIndex: 0
90− property var infographicModel: LightDM.Infographic
91− readonly property int backgroundTopMargin: -greeter.y
92208
93− source: loadContent ? "GreeterContent.qml" : ""
209+ active: root.required
210+ source: root.viewSource.toString() ? root.viewSource :
211+ (d.multiUser || root.tabletMode) ? "WideView.qml" : "NarrowView.qml"
94212
95213 onLoaded: {
96− selected(currentIndex);
214+ root.lockedApp = "";
215+ root.forceActiveFocus();
216+ d.selectUser(d.currentIndex, true);
217+ LightDM.Infographic.readyForDataChange();
97218 }
98219
99220 Connections {
100− target: greeterContentLoader.item
101−
221+ target: loader.item
102222 onSelected: {
103− greeter.selected(uid);
104− greeterContentLoader.currentIndex = uid;
223+ d.selectUser(index, true);
224+ }
225+ onResponded: {
226+ if (root.locked) {
227+ LightDM.Greeter.respond(response);
228+ } else {
229+ if (LightDM.Greeter.active && !LightDM.Greeter.authenticated) { // could happen if forcedUnlock
230+ d.login();
231+ }
232+ loader.item.hide();
233+ }
234+ }
235+ onTease: root.tease()
236+ onEmergencyCall: root.emergencyCall()
237+ onRequiredChanged: {
238+ if (!loader.item.required) {
239+ root.hide();
240+ }
105241 }
106− onUnlocked: greeter.unlocked(uid);
107242 }
243+
108244 Binding {
109− target: greeterContentLoader.item
110− property: "inputMethod"
111− value: greeter.inputMethod
245+ target: loader.item
246+ property: "backgroundTopMargin"
247+ value: -root.y
112248 }
113− }
114249
115− DragHandle {
116− id: dragHandle
117− anchors.fill: parent
118− anchors.leftMargin: greeter.dragHandleLeftMargin
119− enabled: (greeter.narrowMode || !greeter.locked) && greeter.enabled && greeter.shown
120− direction: Direction.Horizontal
250+ Binding {
251+ target: loader.item
252+ property: "launcherOffset"
253+ value: d.launcherOffsetProxy
254+ }
121255
122− onTapped: {
123− greeter.tapped();
124− showLabelAnimation.start();
256+ Binding {
257+ target: loader.item
258+ property: "dragHandleLeftMargin"
259+ value: root.dragHandleLeftMargin
125260 }
126261
127− onDraggingChanged: {
128− if (dragging) {
129− showLabelAnimation.start();
130− }
262+ Binding {
263+ target: loader.item
264+ property: "delayMinutes"
265+ value: d.delayMinutes
266+ }
267+
268+ Binding {
269+ target: loader.item
270+ property: "background"
271+ value: root.background
272+ }
273+
274+ Binding {
275+ target: loader.item
276+ property: "locked"
277+ value: root.locked
278+ }
279+
280+ Binding {
281+ target: loader.item
282+ property: "alphanumeric"
283+ value: AccountsService.passwordDisplayHint === AccountsService.Keyboard
284+ }
285+
286+ Binding {
287+ target: loader.item
288+ property: "currentIndex"
289+ value: d.currentIndex
290+ }
291+
292+ Binding {
293+ target: loader.item
294+ property: "userModel"
295+ value: LightDM.Users
296+ }
297+
298+ Binding {
299+ target: loader.item
300+ property: "infographicModel"
301+ value: LightDM.Infographic
131302 }
132303 }
133304
134− Label {
135− id: swipeHint
136− property real baseOpacity: 0.5
137− opacity: 0.0
138− anchors.horizontalCenter: parent.horizontalCenter
139− anchors.bottom: parent.bottom
140− anchors.bottomMargin: units.gu(5)
141− text: "《 " + i18n.tr("Unlock") + " 》"
142− color: "white"
143− font.weight: Font.Light
144−
145− SequentialAnimation on opacity {
146− id: showLabelAnimation
147− running: false
148− loops: 2
149−
150− StandardAnimation {
151− from: 0.0
152− to: swipeHint.baseOpacity
153− duration: UbuntuAnimation.SleepyDuration
305+ Connections {
306+ target: LightDM.Greeter
307+
308+ onShowGreeter: root.forceShow()
309+
310+ onHideGreeter: {
311+ d.login();
312+ loader.item.hide();
313+ }
314+
315+ onShowMessage: {
316+ if (!LightDM.Greeter.active) {
317+ return; // could happen if hideGreeter() comes in before we prompt
318+ }
319+
320+ // inefficient, but we only rarely deal with messages
321+ var html = text.replace(/&/g, "&amp;")
322+ .replace(/</g, "&lt;")
323+ .replace(/>/g, "&gt;")
324+ .replace(/\n/g, "<br>");
325+ if (isError) {
326+ html = "<font color=\"#df382c\">" + html + "</font>";
154327 }
155− PauseAnimation { duration: UbuntuAnimation.BriskDuration }
156− StandardAnimation {
157− from: swipeHint.baseOpacity
158− to: 0.0
159− duration: UbuntuAnimation.SleepyDuration
328+
329+ loader.item.showMessage(html);
330+ }
331+
332+ onShowPrompt: {
333+ d.waiting = false;
334+
335+ if (!LightDM.Greeter.active) {
336+ return; // could happen if hideGreeter() comes in before we prompt
160337 }
338+
339+ loader.item.showPrompt(text, isSecret, isDefaultPrompt);
161340 }
162− }
163341
164− // right side shadow
165− Image {
166− anchors.left: parent.right
167− anchors.top: parent.top
168− anchors.bottom: parent.bottom
169− fillMode: Image.Tile
170− source: "../graphics/dropshadow_right.png"
171− }
342+ onAuthenticationComplete: {
343+ d.waiting = false;
172344
173− // left side shadow
174− Image {
175− anchors.right: parent.left
176− anchors.top: parent.top
177− anchors.bottom: parent.bottom
178− fillMode: Image.Tile
179− source: "../graphics/dropshadow_left.png"
180− }
345+ if (LightDM.Greeter.authenticated) {
346+ AccountsService.failedLogins = 0;
347+ d.login();
348+ if (!LightDM.Greeter.promptless) {
349+ loader.item.hide();
350+ }
351+ } else {
352+ if (!LightDM.Greeter.promptless) {
353+ AccountsService.failedLogins++;
354+ }
181355
182− Binding {
183− id: positionLock
356+ // Check if we should initiate a factory reset
357+ if (maxFailedLogins >= 2) { // require at least a warning
358+ if (AccountsService.failedLogins === maxFailedLogins - 1) {
359+ loader.item.showLastChance();
360+ } else if (AccountsService.failedLogins >= maxFailedLogins) {
361+ SystemImage.factoryReset(); // Ouch!
362+ }
363+ }
184364
185− property bool enabled: false
186− onEnabledChanged: {
187− if (enabled === __enabled) {
188− return;
189− }
365+ // Check if we should initiate a forced login delay
366+ if (failedLoginsDelayAttempts > 0 && AccountsService.failedLogins % failedLoginsDelayAttempts == 0) {
367+ d.delayMinutes = failedLoginsDelayMinutes;
368+ forcedDelayTimer.start();
369+ }
190370
191− if (enabled) {
192− if (greeter.x > 0) {
193− value = Qt.binding(function() { return greeter.width; })
194− } else {
195− value = Qt.binding(function() { return -greeter.width; })
371+ loader.item.notifyAuthenticationFailed();
372+ if (!LightDM.Greeter.promptless) {
373+ d.selectUser(d.currentIndex, false);
196374 }
197375 }
198−
199− __enabled = enabled;
200376 }
201377
202− property bool __enabled: false
378+ onRequestAuthenticationUser: {
379+ // Find index for requested user, if it exists
380+ for (var i = 0; i < LightDM.Users.count; i++) {
381+ if (user === LightDM.Users.data(i, LightDM.UserRoles.NameRole)) {
382+ d.selectUser(i, true);
383+ return;
384+ }
385+ }
386+ }
387+ }
203388
204− target: greeter
205− when: __enabled
206− property: "x"
389+ Binding {
390+ target: LightDM.Greeter
391+ property: "active"
392+ value: root.active
207393 }
208394
209− hideAnimation: SequentialAnimation {
210− id: hideAnimation
211− objectName: "hideAnimation"
212− StandardAnimation {
213− id: hideTranslation
214− property: "x"
215− target: greeter
216− }
217− PropertyAction { target: greeter; property: "visible"; value: false }
218− PropertyAction { target: positionLock; property: "enabled"; value: true }
395+ Binding {
396+ target: LightDM.Infographic
397+ property: "username"
398+ value: AccountsService.statsWelcomeScreen ? LightDM.Users.data(d.currentIndex, LightDM.UserRoles.NameRole) : ""
219399 }
220400
221− showAnimation: SequentialAnimation {
222− id: showAnimation
223− objectName: "showAnimation"
224− PropertyAction { target: greeter; property: "visible"; value: true }
225− PropertyAction { target: positionLock; property: "enabled"; value: false }
226− StandardAnimation {
227− property: "x"
228− target: greeter
229− to: 0
230− duration: UbuntuAnimation.FastDuration
231− }
401+ Connections {
402+ target: i18n
403+ onLanguageChanged: LightDM.Infographic.readyForDataChange()
232404 }
233405}
qml/Greeter/GreeterContent.qmldeleted−163
@@ -1,163 +0,0 @@
1−/*
2− * Copyright (C) 2013 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 AccountsService 0.1
19−import Ubuntu.Components 0.1
20−import LightDM 0.1 as LightDM
21−import "../Components"
22−
23−Item {
24− id: root
25− anchors.fill: parent
26−
27− property var inputMethod
28−
29− property bool ready: background.source == "" || background.status == Image.Ready || background.status == Image.Error
30−
31− signal selected(int uid)
32− signal unlocked(int uid)
33−
34− function tryToUnlock() {
35− if (loginLoader.item) {
36− loginLoader.item.tryToUnlock()
37− }
38− }
39−
40− function reset() {
41− if (loginLoader.item) {
42− loginLoader.item.reset()
43− }
44− }
45−
46− Rectangle {
47− // In case background fails to load
48− id: backgroundBackup
49− anchors.fill: parent
50− color: "black"
51− }
52−
53− CrossFadeImage {
54− id: background
55− objectName: "greeterBackground"
56− anchors {
57− fill: parent
58− topMargin: backgroundTopMargin
59− }
60− fillMode: Image.PreserveAspectCrop
61− // Limit how much memory we'll reserve for this image
62− sourceSize.height: height
63− sourceSize.width: width
64− source: greeter.background
65− }
66−
67− Rectangle {
68− anchors.fill: parent
69− color: "black"
70− opacity: 0.4
71− }
72−
73− Loader {
74− id: loginLoader
75− objectName: "loginLoader"
76− anchors {
77− left: parent.left
78− leftMargin: Math.min(parent.width * 0.16, units.gu(20))
79− top: parent.top
80− }
81− width: units.gu(29)
82− height: inputMethod && inputMethod.visible ? parent.height - inputMethod.keyboardRectangle.height
83− : parent.height
84− Behavior on height { UbuntuNumberAnimation {} }
85−
86− // TODO: Once we have a system API for determining which mode we are
87− // in, tablet/phone/desktop, that should be used instead of narrowMode.
88− source: greeter.narrowMode ? "" : "LoginList.qml"
89−
90− onLoaded: {
91− item.currentIndex = greeterContentLoader.currentIndex;
92− }
93−
94− Binding {
95− target: loginLoader.item
96− property: "model"
97− value: greeterContentLoader.model
98− }
99−
100− Connections {
101− target: loginLoader.item
102−
103− onSelected: {
104− root.selected(uid);
105− }
106−
107− onUnlocked: {
108− root.unlocked(uid);
109− }
110−
111− onCurrentIndexChanged: {
112− if (greeterContentLoader.currentIndex !== loginLoader.item.currentIndex) {
113− greeterContentLoader.currentIndex = loginLoader.item.currentIndex;
114− }
115− }
116− }
117− }
118−
119− Infographics {
120− id: infographics
121− objectName: "infographics"
122− height: narrowMode ? parent.height : 0.75 * parent.height
123− model: greeterContentLoader.infographicModel
124− clip: true // clip large data bubbles
125−
126− property string selectedUser
127− property string infographicUser: AccountsService.statsWelcomeScreen ? selectedUser : ""
128− onInfographicUserChanged: greeterContentLoader.infographicModel.username = infographicUser
129−
130− Component.onCompleted: {
131− selectedUser = greeterContentLoader.model.data(greeterContentLoader.currentIndex, LightDM.UserRoles.NameRole)
132− greeterContentLoader.infographicModel.username = infographicUser
133− greeterContentLoader.infographicModel.readyForDataChange()
134− }
135−
136− Connections {
137− target: root
138− onSelected: infographics.selectedUser = greeterContentLoader.model.data(uid, LightDM.UserRoles.NameRole)
139− }
140−
141− Connections {
142− target: i18n
143− onLanguageChanged: greeterContentLoader.infographicModel.readyForDataChange()
144− }
145−
146− anchors {
147− verticalCenter: parent.verticalCenter
148− left: narrowMode ? root.left : loginLoader.right
149− right: root.right
150− }
151− }
152−
153− Clock {
154− id: clock
155− visible: narrowMode
156−
157− anchors {
158− top: parent.top
159− topMargin: units.gu(2)
160− horizontalCenter: parent.horizontalCenter
161− }
162− }
163−}
qml/Greeter/Infographics.qml+1
@@ -38,6 +38,7 @@ Item {
3838
3939 Connections {
4040 target: model
41+ ignoreUnknownSignals: model === undefined
4142
4243 onDataAboutToAppear: startHideAnimation() // hide "no data" label
4344 onDataAppeared: startShowAnimation()
qml/Greeter/LoginList.qml+67−85
@@ -14,44 +14,76 @@
1414 * along with this program. If not, see <http://www.gnu.org/licenses/>.
1515 */
1616
17−import QtQuick 2.0
18−import Ubuntu.Components 0.1
19−import LightDM 0.1 as LightDM
17+import QtQuick 2.3
18+import Ubuntu.Components 1.1
2019import "../Components"
2120
2221Item {
2322 id: root
2423
25− property alias userList: userList
2624 property alias model: userList.model
27− property alias currentIndex: userList.currentIndex
25+ property int currentIndex
26+ property bool locked
2827
2928 readonly property int numAboveBelow: 4
3029 readonly property int cellHeight: units.gu(5)
3130 readonly property int highlightedHeight: units.gu(10)
3231 readonly property int moveDuration: 200
32+ readonly property string currentUser: userList.currentItem.username
3333 property bool wasPrompted: false
3434
35− signal selected(int uid)
36− signal unlocked(int uid)
35+ signal selected(int index)
36+ signal responded(string response)
3737
3838 function tryToUnlock() {
39− if (LightDM.Greeter.promptless) {
40− if (LightDM.Greeter.authenticated) {
41− root.unlocked(userList.currentIndex)
39+ if (wasPrompted) {
40+ passwordInput.forceActiveFocus();
41+ } else {
42+ if (root.locked) {
43+ root.selected(currentIndex);
4244 } else {
43− root.resetAuthentication()
45+ root.responded("");
4446 }
47+ }
48+ }
49+
50+ function showMessage(html) {
51+ if (infoLabel.text === "") {
52+ infoLabel.text = html;
4553 } else {
46− passwordInput.forceActiveFocus()
54+ infoLabel.text += "<br>" + html;
55+ }
56+ }
57+
58+ function showPrompt(text, isSecret, isDefaultPrompt) {
59+ passwordInput.text = "";
60+ passwordInput.promptText = text;
61+ passwordInput.enabled = true;
62+ passwordInput.echoMode = isSecret ? TextInput.Password : TextInput.Normal;
63+ if (wasPrompted) // stay in text field if second prompt
64+ passwordInput.focus = true;
65+ wasPrompted = true;
66+ }
67+
68+ function showError() {
69+ wrongPasswordAnimation.start();
70+ root.resetAuthentication();
71+ if (wasPrompted) {
72+ passwordInput.focus = true;
4773 }
4874 }
4975
5076 function reset() {
51− root.resetAuthentication()
77+ root.resetAuthentication();
5278 }
5379
54− Keys.onEscapePressed: root.resetAuthentication()
80+ Keys.onEscapePressed: {
81+ selected(currentIndex);
82+ }
83+
84+ onCurrentIndexChanged: {
85+ userList.currentIndex = currentIndex;
86+ }
5587
5688 Rectangle {
5789 id: highlightItem
@@ -81,26 +113,24 @@ Item {
81113 flickDeceleration: 10000
82114
83115 readonly property bool movingInternally: moveTimer.running || userList.moving
84−
85− onCurrentIndexChanged: {
86− if (LightDM.Greeter.authenticationUser != userList.model.data(currentIndex, LightDM.UserRoles.NameRole)) {
87− root.resetAuthentication();
88− }
89− }
90−
91116 onMovingInternallyChanged: {
92− // Only emit the selected signal once we stop moving to avoid frequent background changes
93117 if (!movingInternally) {
94− root.selected(userList.currentIndex);
118+ root.selected(currentIndex);
95119 }
96120 }
97121
122+ onCurrentIndexChanged: {
123+ root.resetAuthentication();
124+ moveTimer.start();
125+ }
126+
98127 delegate: Item {
99128 width: parent.width
100129 height: root.cellHeight
101130
102131 readonly property bool belowHighlight: (userList.currentIndex < 0 && index > 0) || (userList.currentIndex >= 0 && index > userList.currentIndex)
103132 readonly property int belowOffset: root.highlightedHeight - root.cellHeight
133+ readonly property string username: name
104134
105135 opacity: {
106136 // The goal here is to make names less and less opaque as they
@@ -148,11 +178,8 @@ Item {
148178 topMargin: parent.belowHighlight ? parent.belowOffset : 0
149179 }
150180 height: parent.height
151− enabled: userList.currentIndex !== index
152− onClicked: {
153− moveTimer.start();
154− userList.currentIndex = index;
155− }
181+ enabled: userList.currentIndex !== index && parent.opacity > 0
182+ onClicked: root.selected(index)
156183
157184 Behavior on anchors.topMargin { NumberAnimation { duration: root.moveDuration; easing.type: Easing.InOutQuad; } }
158185 }
@@ -204,6 +231,11 @@ Item {
204231 width: parent.width - anchors.margins * 2
205232 opacity: userList.movingInternally ? 0 : 1
206233
234+ property string promptText
235+ placeholderText: root.wasPrompted ? promptText
236+ : (root.locked ? i18n.tr("Retry")
237+ : i18n.tr("Tap to unlock"))
238+
207239 Behavior on opacity {
208240 NumberAnimation { duration: 100 }
209241 }
@@ -213,9 +245,11 @@ Item {
213245 return;
214246 root.focus = true; // so that it can handle Escape presses for us
215247 enabled = false;
216− LightDM.Greeter.respond(text);
248+ root.responded(text);
249+ }
250+ Keys.onEscapePressed: {
251+ root.selected(currentIndex);
217252 }
218− Keys.onEscapePressed: root.resetAuthentication()
219253
220254 Image {
221255 anchors {
@@ -223,7 +257,7 @@ Item {
223257 rightMargin: units.gu(2)
224258 verticalCenter: parent.verticalCenter
225259 }
226− visible: LightDM.Greeter.promptless
260+ visible: !root.wasPrompted
227261 source: "graphics/icon_arrow.png"
228262 }
229263
@@ -247,7 +281,7 @@ Item {
247281 id: passwordMouseArea
248282 objectName: "passwordMouseArea"
249283 anchors.fill: passwordInput
250− enabled: LightDM.Greeter.promptless
284+ enabled: !root.wasPrompted
251285 onClicked: root.tryToUnlock()
252286 }
253287
@@ -256,62 +290,10 @@ Item {
256290 return;
257291 }
258292 infoLabel.text = "";
259− passwordInput.placeholderText = "";
293+ passwordInput.promptText = "";
260294 passwordInput.text = "";
261295 passwordInput.focus = false;
262296 passwordInput.enabled = true;
263297 root.wasPrompted = false;
264− LightDM.Greeter.authenticate(userList.model.data(currentIndex, LightDM.UserRoles.NameRole));
265− }
266−
267− Connections {
268− target: LightDM.Greeter
269−
270− onShowMessage: {
271− // inefficient, but we only rarely deal with messages
272− var html = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\n/g, "<br>")
273− if (isError)
274− html = "<font color=\"#df382c\">" + html + "</font>"
275− if (infoLabel.text == "")
276− infoLabel.text = html
277− else
278− infoLabel.text = infoLabel.text + "<br>" + html
279− }
280−
281− onShowPrompt: {
282− passwordInput.text = "";
283− passwordInput.placeholderText = text;
284− passwordInput.enabled = true;
285− passwordInput.echoMode = isSecret ? TextInput.Password : TextInput.Normal;
286− if (root.wasPrompted) // stay in text field if second prompt
287− passwordInput.focus = true;
288− root.wasPrompted = true;
289− }
290−
291− onAuthenticationComplete: {
292− if (LightDM.Greeter.promptless) {
293− passwordInput.placeholderText = LightDM.Greeter.authenticated ? "Tap to unlock" : "Retry";
294− return;
295− }
296− if (LightDM.Greeter.authenticated) {
297− root.unlocked(userList.currentIndex);
298− } else {
299− wrongPasswordAnimation.start();
300− root.resetAuthentication();
301− passwordInput.focus = true;
302− }
303− passwordInput.text = "";
304− }
305−
306− onRequestAuthenticationUser: {
307− // Find index for requested user, if it exists
308− for (var i = 0; i < userList.model.count; i++) {
309− if (user == userList.model.data(i, LightDM.UserRoles.NameRole)) {
310− moveTimer.start();
311− userList.currentIndex = i;
312− return;
313− }
314− }
315− }
316298 }
317299}
qml/Greeter/NarrowView.qmlnew file+170
@@ -0,0 +1,170 @@
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.3
18+import Ubuntu.Components 1.1
19+import "../Components"
20+
21+FocusScope {
22+ id: root
23+
24+ property alias dragHandleLeftMargin: coverPage.dragHandleLeftMargin
25+ property alias launcherOffset: coverPage.launcherOffset
26+ property int currentIndex // unused
27+ property alias delayMinutes: lockscreen.delayMinutes
28+ property alias backgroundTopMargin: coverPage.backgroundTopMargin
29+ property url background
30+ property bool locked
31+ property bool alphanumeric
32+ property var userModel // unused
33+ property alias infographicModel: coverPage.infographicModel
34+ readonly property bool fullyShown: coverPage.showProgress === 1 || lockscreen.shown
35+ readonly property bool required: coverPage.required || lockscreen.required
36+
37+ signal selected(int index) // unused
38+ signal responded(string response)
39+ signal tease()
40+ signal emergencyCall()
41+
42+ function showMessage(html) {
43+ // TODO
44+ }
45+
46+ function showPrompt(text, isSecret, isDefaultPrompt) {
47+ lockscreen.promptText = isDefaultPrompt ? "" : text.toLowerCase();
48+ lockscreen.maybeShow();
49+ }
50+
51+ function showLastChance() {
52+ var title = lockscreen.alphaNumeric ?
53+ i18n.tr("Sorry, incorrect passphrase.") :
54+ i18n.tr("Sorry, incorrect passcode.");
55+ var text = i18n.tr("This will be your last attempt.") + " " +
56+ (lockscreen.alphaNumeric ?
57+ i18n.tr("If passphrase is entered incorrectly, your phone will conduct a factory reset and all personal data will be deleted.") :
58+ i18n.tr("If passcode is entered incorrectly, your phone will conduct a factory reset and all personal data will be deleted."));
59+ lockscreen.showInfoPopup(title, text);
60+ }
61+
62+ function hide() {
63+ lockscreen.hide();
64+ coverPage.hide();
65+ }
66+
67+ function notifyAuthenticationSucceeded() {
68+ lockscreen.hide();
69+ }
70+
71+ function notifyAuthenticationFailed() {
72+ lockscreen.clear(true);
73+ }
74+
75+ function reset() {
76+ coverPage.show();
77+ }
78+
79+ function tryToUnlock(toTheRight) {
80+ var coverChanged = coverPage.shown;
81+ lockscreen.maybeShow();
82+ if (toTheRight) {
83+ coverPage.hideRight();
84+ } else {
85+ coverPage.hide();
86+ }
87+ return coverChanged;
88+ }
89+
90+ onLockedChanged: {
91+ if (locked) {
92+ lockscreen.maybeShow();
93+ } else {
94+ lockscreen.hide();
95+ }
96+ }
97+
98+ Lockscreen {
99+ id: lockscreen
100+ objectName: "lockscreen"
101+
102+ shown: false
103+ showAnimation: StandardAnimation { property: "opacity"; to: 1 }
104+ hideAnimation: StandardAnimation { property: "opacity"; to: 0 }
105+ anchors.fill: parent
106+ visible: required
107+ enabled: !coverPage.shown
108+ background: root.background
109+ darkenBackground: 0.4
110+ alphaNumeric: root.alphanumeric
111+ minPinLength: 4
112+ maxPinLength: 4
113+
114+ property string promptText
115+ infoText: promptText !== "" ? i18n.tr("Enter %1").arg(promptText) :
116+ alphaNumeric ? i18n.tr("Enter passphrase") :
117+ i18n.tr("Enter passcode")
118+ errorText: promptText !== "" ? i18n.tr("Sorry, incorrect %1").arg(promptText) :
119+ alphaNumeric ? i18n.tr("Sorry, incorrect passphrase") + "\n" +
120+ i18n.tr("Please re-enter") :
121+ i18n.tr("Sorry, incorrect passcode")
122+
123+ onEntered: root.responded(passphrase)
124+ onCancel: coverPage.show()
125+ onEmergencyCall: root.emergencyCall()
126+
127+ function maybeShow() {
128+ if (root.locked && !shown) {
129+ showNow();
130+ }
131+ }
132+ }
133+
134+ Rectangle {
135+ anchors.fill: parent
136+ color: "black"
137+ opacity: coverPage.showProgress * 0.8
138+ }
139+
140+ CoverPage {
141+ id: coverPage
142+ objectName: "coverPage"
143+ height: parent.height
144+ width: parent.width
145+ background: root.background
146+ onTease: root.tease()
147+
148+ onShowProgressChanged: {
149+ if (showProgress === 1) {
150+ lockscreen.reset();
151+ }
152+
153+ if (showProgress === 0) {
154+ if (root.locked) {
155+ lockscreen.clear(false); // to reset focus if necessary
156+ } else {
157+ root.responded("");
158+ }
159+ }
160+ }
161+
162+ Clock {
163+ anchors {
164+ top: parent.top
165+ topMargin: units.gu(2)
166+ horizontalCenter: parent.horizontalCenter
167+ }
168+ }
169+ }
170+}
qml/Greeter/WideView.qmlnew file+134
@@ -0,0 +1,134 @@
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.3
18+import Ubuntu.Components 1.1
19+
20+FocusScope {
21+ id: root
22+
23+ property alias dragHandleLeftMargin: coverPage.dragHandleLeftMargin
24+ property alias launcherOffset: coverPage.launcherOffset
25+ property alias currentIndex: loginList.currentIndex
26+ property int delayMinutes // TODO
27+ property alias backgroundTopMargin: coverPage.backgroundTopMargin
28+ property alias background: coverPage.background
29+ property bool locked
30+ property bool alphanumeric // unused
31+ property alias userModel: loginList.model
32+ property alias infographicModel: coverPage.infographicModel
33+ readonly property bool fullyShown: coverPage.showProgress === 1
34+ readonly property bool required: coverPage.required
35+
36+ // so that it can be replaced in tests with a mock object
37+ property var inputMethod: Qt.inputMethod
38+
39+ signal selected(int index)
40+ signal responded(string response)
41+ signal tease()
42+ signal emergencyCall() // unused
43+
44+ function showMessage(html) {
45+ loginList.showMessage(html);
46+ }
47+
48+ function showPrompt(text, isSecret, isDefaultPrompt) {
49+ loginList.showPrompt(text, isSecret, isDefaultPrompt);
50+ }
51+
52+ function showLastChance() {
53+ // TODO
54+ }
55+
56+ function hide() {
57+ coverPage.hide();
58+ }
59+
60+ function notifyAuthenticationSucceeded() {
61+ // Nothing needed
62+ }
63+
64+ function notifyAuthenticationFailed() {
65+ loginList.showError();
66+ }
67+
68+ function reset() {
69+ loginList.reset();
70+ }
71+
72+ function tryToUnlock(toTheRight) {
73+ if (root.locked) {
74+ coverPage.show();
75+ loginList.tryToUnlock();
76+ return false;
77+ } else {
78+ var coverChanged = coverPage.shown;
79+ if (toTheRight) {
80+ coverPage.hideRight();
81+ } else {
82+ coverPage.hide();
83+ }
84+ return coverChanged;
85+ }
86+ }
87+
88+ Rectangle {
89+ anchors.fill: parent
90+ color: "black"
91+ opacity: coverPage.showProgress * 0.8
92+ }
93+
94+ CoverPage {
95+ id: coverPage
96+ objectName: "coverPage"
97+ height: parent.height
98+ width: parent.width
99+ draggable: !root.locked
100+
101+ infographics {
102+ height: 0.75 * parent.height
103+ anchors.leftMargin: loginList.x + loginList.width
104+ }
105+
106+ onTease: root.tease()
107+
108+ onShowProgressChanged: {
109+ if (showProgress === 0 && !root.locked) {
110+ root.responded("");
111+ }
112+ }
113+
114+ LoginList {
115+ id: loginList
116+ objectName: "loginList"
117+
118+ anchors {
119+ left: parent.left
120+ leftMargin: Math.min(parent.width * 0.16, units.gu(20))
121+ top: parent.top
122+ }
123+ width: units.gu(29)
124+ height: inputMethod && inputMethod.visible ? parent.height - inputMethod.keyboardRectangle.height
125+ : parent.height
126+ Behavior on height { UbuntuNumberAnimation {} }
127+
128+ locked: root.locked
129+
130+ onSelected: root.selected(index)
131+ onResponded: root.responded(response)
132+ }
133+ }
134+}
qml/Shell.qml+67−353
@@ -22,9 +22,7 @@ import Unity.Application 0.1
2222import Ubuntu.Components 0.1
2323import Ubuntu.Components.Popups 1.0
2424import Ubuntu.Gestures 0.1
25−import Ubuntu.SystemImage 0.1
2625import Ubuntu.Telephony 0.1 as Telephony
27−import Unity.Connectivity 0.1
2826import Unity.Launcher 0.1
2927import Utils 0.1
3028import LightDM 0.1 as LightDM
@@ -46,9 +44,9 @@ import Unity.Indicators 0.1 as Indicators
4644Item {
4745 id: shell
4846
49− // Disable everything so that user can't swipe greeter or launcher until
50− // we get first prompt/authenticate, which will re-enable the shell.
51− enabled: false
47+ // Disable everything while greeter is waiting, so that the user can't swipe
48+ // the greeter or launcher until we know whether the session is locked.
49+ enabled: !greeter.waiting
5250
5351 // this is only here to select the width / height of the window if not running fullscreen
5452 property bool tablet: false
@@ -61,18 +59,9 @@ Item {
6159 : gsImageTester.status == Image.Ready ? gsImageTester.source : defaultBackground
6260 readonly property real panelHeight: panel.panelHeight
6361
64− readonly property bool locked: LightDM.Greeter.active && !LightDM.Greeter.authenticated && !forcedUnlock
65− readonly property alias hasLockedApp: greeter.hasLockedApp
66− readonly property bool forcedUnlock: tutorial.running
67− onForcedUnlockChanged: if (forcedUnlock) lockscreen.hide()
68−
6962 property bool sideStageEnabled: shell.width >= units.gu(100)
7063 readonly property string focusedApplicationId: ApplicationManager.focusedApplicationId
7164
72− property int maxFailedLogins: -1 // disabled by default for now, will enable via settings in future
73− property int failedLoginsDelayAttempts: 7 // number of failed logins
74− property int failedLoginsDelayMinutes: 5 // minutes of forced waiting
75−
7665 property int orientation
7766 readonly property int deviceOrientationAngle: Screen.angleBetween(Screen.primaryOrientation, Screen.orientation)
7867 onDeviceOrientationAngleChanged: {
@@ -99,7 +88,7 @@ Item {
9988 }
10089
10190 function startLockedApp(app) {
102− if (shell.locked) {
91+ if (greeter.locked) {
10392 greeter.lockedApp = app;
10493 }
10594 shell.activateApplication(app);
@@ -117,6 +106,7 @@ Item {
117106
118107 GSettings {
119108 id: backgroundSettings
109+ objectName: "backgroundSettings"
120110 schema.id: "org.gnome.desktop.background"
121111 }
122112
@@ -222,53 +212,32 @@ Item {
222212
223213 Connections {
224214 target: ApplicationManager
225− onFocusRequested: {
226− if (greeter.narrowMode) {
227− if (appId === "dialer-app" && callManager.hasCalls && shell.locked) {
228− // If we are in the middle of a call, make dialer lockedApp and show it.
229− // This can happen if user backs out of dialer back to greeter, then
230− // launches dialer again.
231− greeter.lockedApp = appId;
232− }
233− if (greeter.hasLockedApp) {
234− if (appId === greeter.lockedApp) {
235− lockscreen.hide() // show locked app
236− } else {
237− greeter.startUnlock() // show lockscreen if necessary
238− }
239− }
240− greeter.hide();
241− } else {
242− if (LightDM.Greeter.active) {
243− greeter.startUnlock()
244− }
245− }
246− }
247215
216+ // This signal is also fired when we try to focus the current app
217+ // again. We rely on this!
248218 onFocusedApplicationIdChanged: {
249− if (greeter.hasLockedApp && greeter.lockedApp !== ApplicationManager.focusedApplicationId) {
250− greeter.startUnlock()
251− }
252− panel.indicators.hide();
253− }
254−
255− onApplicationAdded: {
256− if (appId != "unity8-dash") {
257− if (greeter.shown) {
258− greeter.startUnlock();
259− }
219+ var appId = ApplicationManager.focusedApplicationId;
260220
221+ if (tutorial.running && appId != "unity8-dash") {
261222 // If this happens on first boot, we may be in edge
262223 // tutorial or wizard while receiving a call. But a call
263224 // is more important than wizard so just bail out of those.
264− if (tutorial.running) {
265− tutorial.finish();
266− wizard.hide();
267− }
225+ tutorial.finish();
226+ wizard.hide();
268227 }
269− if (greeter.narrowMode && greeter.hasLockedApp && appId === greeter.lockedApp) {
270− lockscreen.hide() // show locked app
228+
229+ if (appId === "dialer-app" && callManager.hasCalls && greeter.locked) {
230+ // If we are in the middle of a call, make dialer lockedApp and show it.
231+ // This can happen if user backs out of dialer back to greeter, then
232+ // launches dialer again.
233+ greeter.lockedApp = appId;
271234 }
235+ greeter.notifyAppFocused(appId);
236+
237+ panel.indicators.hide();
238+ }
239+
240+ onApplicationAdded: {
272241 launcher.hide();
273242 }
274243 }
@@ -292,7 +261,6 @@ Item {
292261
293262 property bool interactive: tutorial.spreadEnabled
294263 && !greeter.shown
295− && !lockscreen.shown
296264 && panel.indicators.fullyClosed
297265 && launcher.progress == 0
298266 && !notifications.useModal
@@ -328,7 +296,7 @@ Item {
328296 Binding {
329297 target: applicationsDisplayLoader.item
330298 property: "inverseProgress"
331− value: launcher.progress
299+ value: greeter.locked ? 0 : launcher.progress
332300 }
333301 Binding {
334302 target: applicationsDisplayLoader.item
@@ -379,299 +347,50 @@ Item {
379347 }
380348 }
381349
382− Lockscreen {
383− id: lockscreen
384− objectName: "lockscreen"
350+ Greeter {
351+ id: greeter
352+ objectName: "greeter"
385353
386354 hides: [launcher, panel.indicators]
387− shown: false
388− enabled: true
389− showAnimation: StandardAnimation { property: "opacity"; to: 1 }
390− hideAnimation: StandardAnimation { property: "opacity"; to: 0 }
391− y: panel.panelHeight
392− visible: required
393− width: parent.width
394− height: parent.height - panel.panelHeight
355+ tabletMode: shell.sideStageEnabled
356+ launcherOffset: launcher.progress
357+ forcedUnlock: tutorial.running
395358 background: shell.background
396− darkenBackground: 0.4
397− alphaNumeric: AccountsService.passwordDisplayHint === AccountsService.Keyboard
398− minPinLength: 4
399− maxPinLength: 4
400−
401− property string promptText
402− infoText: promptText !== "" ? i18n.tr("Enter %1").arg(promptText) :
403− alphaNumeric ? i18n.tr("Enter passphrase") :
404− i18n.tr("Enter passcode")
405− errorText: promptText !== "" ? i18n.tr("Sorry, incorrect %1").arg(promptText) :
406− alphaNumeric ? i18n.tr("Sorry, incorrect passphrase") + "\n" +
407− i18n.tr("Please re-enter") :
408− i18n.tr("Sorry, incorrect passcode")
409−
410− // FIXME: We *should* show emergency dialer if there is a SIM present,
411− // regardless of whether the side stage is enabled. But right now,
412− // the assumption is that narrow screens are phones which have SIMs
413− // and wider screens are tablets which don't. When we do allow this
414− // on devices with a side stage and a SIM, work should be done to
415− // ensure that the main stage is disabled while the dialer is present
416− // in the side stage. See the FIXME in the stage loader in this file.
417− showEmergencyCallButton: !shell.sideStageEnabled
418−
419− onEntered: LightDM.Greeter.respond(passphrase);
420− onCancel: greeter.show()
421− onEmergencyCall: startLockedApp("dialer-app")
422−
423− onShownChanged: if (shown) greeter.lockedApp = ""
424−
425− function maybeShow() {
426− if (!shell.forcedUnlock && !shown) {
427− showNow();
428− }
429− }
430−
431− Timer {
432− id: forcedDelayTimer
433− interval: 1000 * 60
434− onTriggered: {
435− if (lockscreen.delayMinutes > 0) {
436− lockscreen.delayMinutes -= 1
437− if (lockscreen.delayMinutes > 0) {
438− start() // go again
439− }
440− }
441− }
442− }
443−
444− Component.onCompleted: {
445− if (greeter.narrowMode) {
446− LightDM.Greeter.authenticate(LightDM.Users.data(0, LightDM.UserRoles.NameRole))
447− }
448− }
449− }
450−
451− Connections {
452− target: LightDM.Greeter
453359
454− onShowGreeter: greeter.show()
455− onHideGreeter: greeter.login()
456−
457− onShowPrompt: {
458− shell.enabled = true;
459− if (!LightDM.Greeter.active) {
460− return; // could happen if hideGreeter() comes in before we prompt
461− }
462− if (greeter.narrowMode) {
463− lockscreen.promptText = isDefaultPrompt ? "" : text.toLowerCase();
464− lockscreen.maybeShow();
465− }
466− }
467−
468− onPromptlessChanged: {
469− if (!LightDM.Greeter.active) {
470− return; // could happen if hideGreeter() comes in before we prompt
471− }
472− if (greeter.narrowMode) {
473− if (LightDM.Greeter.promptless && LightDM.Greeter.authenticated) {
474− lockscreen.hide()
475− } else {
476− lockscreen.reset();
477− lockscreen.maybeShow();
478− }
479− }
480− }
481−
482− onAuthenticationComplete: {
483− shell.enabled = true;
484− if (LightDM.Greeter.authenticated) {
485− AccountsService.failedLogins = 0
486− }
487− // Else only penalize user for a failed login if they actually were
488− // prompted for a password. We do this below after the promptless
489− // early exit.
490−
491− if (LightDM.Greeter.promptless) {
492− return;
493− }
494−
495− if (LightDM.Greeter.authenticated) {
496− greeter.login();
497− } else {
498− AccountsService.failedLogins++
499− if (maxFailedLogins >= 2) { // require at least a warning
500− if (AccountsService.failedLogins === maxFailedLogins - 1) {
501− var title = lockscreen.alphaNumeric ?
502− i18n.tr("Sorry, incorrect passphrase.") :
503− i18n.tr("Sorry, incorrect passcode.")
504− var text = i18n.tr("This will be your last attempt.") + " " +
505− (lockscreen.alphaNumeric ?
506− i18n.tr("If passphrase is entered incorrectly, your phone will conduct a factory reset and all personal data will be deleted.") :
507− i18n.tr("If passcode is entered incorrectly, your phone will conduct a factory reset and all personal data will be deleted."))
508− lockscreen.showInfoPopup(title, text)
509− } else if (AccountsService.failedLogins >= maxFailedLogins) {
510− SystemImage.factoryReset() // Ouch!
511− }
512− }
513− if (failedLoginsDelayAttempts > 0 && AccountsService.failedLogins % failedLoginsDelayAttempts == 0) {
514− lockscreen.delayMinutes = failedLoginsDelayMinutes
515− forcedDelayTimer.start()
516− }
517−
518− lockscreen.clear(true);
519− if (greeter.narrowMode) {
520− LightDM.Greeter.authenticate(LightDM.Users.data(0, LightDM.UserRoles.NameRole))
521− }
522− }
523− }
524− }
525−
526− Binding {
527− target: LightDM.Greeter
528− property: "active"
529− value: greeter.shown || lockscreen.shown || greeter.hasLockedApp
530− }
531−
532− Rectangle {
533360 anchors.fill: parent
534− color: "black"
535− opacity: greeterWrapper.showProgress * 0.8
536− }
537−
538− Item {
539− // Just a tiny wrapper to adjust greeter's x without messing with its own dragging
540− id: greeterWrapper
541− objectName: "greeterWrapper"
542− x: (greeter.narrowMode && greeter.showProgress > 0) ? launcher.progress : 0
543− y: panel.panelHeight
544− width: parent.width
545− height: parent.height - panel.panelHeight
361+ anchors.topMargin: panel.panelHeight
546362
547− Behavior on x {
548− enabled: !launcher.dashSwipe
549− StandardAnimation {}
550− }
363+ // avoid overlapping with Launcher's edge drag area
364+ // FIXME: Fix TouchRegistry & friends and remove this workaround
365+ // Issue involves launcher's DDA getting disabled on a long
366+ // left-edge drag
367+ dragHandleLeftMargin: launcher.available ? launcher.dragAreaWidth + 1 : 0
551368
552− property bool fullyShown: showProgress === 1.0
553− onFullyShownChanged: {
554− // Wait until the greeter is completely covering lockscreen before resetting it.
555− if (greeter.narrowMode && fullyShown && !LightDM.Greeter.authenticated) {
556− lockscreen.reset();
557− lockscreen.maybeShow();
558− }
369+ onSessionStarted: {
370+ launcher.hide();
559371 }
560372
561− readonly property real showProgress: MathUtils.clamp((1 - x/width) + greeter.showProgress - 1, 0, 1)
562− onShowProgressChanged: {
563− if (showProgress === 0) {
564− if ((LightDM.Greeter.promptless && LightDM.Greeter.authenticated) || shell.forcedUnlock) {
565− greeter.login()
566− } else if (greeter.narrowMode) {
567− lockscreen.clear(false) // to reset focus if necessary
568− }
373+ onTease: {
374+ if (!tutorial.running) {
375+ launcher.tease();
569376 }
570377 }
571378
572− Greeter {
573− id: greeter
574− objectName: "greeter"
575−
576− signal sessionStarted() // helpful for tests
577−
578− property string lockedApp: ""
579− property bool hasLockedApp: lockedApp !== ""
580−
581− hides: [launcher, panel.indicators]
582− loadContent: required || lockscreen.required // keeps content in memory for quick show()
583−
584− locked: shell.locked
585−
586− background: shell.background
587−
588− width: parent.width
589− height: parent.height
590−
591−
592− // avoid overlapping with Launcher's edge drag area
593− // FIXME: Fix TouchRegistry & friends and remove this workaround
594− // Issue involves launcher's DDA getting disabled on a long
595− // left-edge drag
596− dragHandleLeftMargin: launcher.available ? launcher.dragAreaWidth + 1 : 0
597−
598− function startUnlock() {
599− if (narrowMode) {
600− if (!LightDM.Greeter.authenticated) {
601− lockscreen.maybeShow()
602− }
603− hide()
604− } else {
605− show()
606− tryToUnlock()
607− }
608− }
609−
610− function login() {
611− enabled = false;
612− if (LightDM.Greeter.startSessionSync()) {
613− sessionStarted();
614− greeter.hide();
615− lockscreen.hide();
616− launcher.hide();
617− }
618− enabled = true;
619− }
620−
621− Timer {
622− // See powerConnection for why this is useful
623− id: showGreeterDelayed
624− interval: 1
625− onTriggered: {
626− greeter.showNow();
627− }
628− }
629−
630− onShownChanged: {
631− if (shown) {
632− // Disable everything so that user can't swipe greeter or
633− // launcher until we get the next prompt/authenticate, which
634− // will re-enable the shell.
635− shell.enabled = false;
636−
637− if (greeter.narrowMode) {
638− LightDM.Greeter.authenticate(LightDM.Users.data(0, LightDM.UserRoles.NameRole));
639− } else {
640− reset()
641− }
642− greeter.lockedApp = "";
643− greeter.forceActiveFocus();
644− }
645− }
646−
647− Component.onCompleted: {
648− Connectivity.unlockAllModems()
649− }
650−
651− onUnlocked: greeter.hide()
652− onSelected: {
653− // Update launcher items for new user
654− var user = LightDM.Users.data(uid, LightDM.UserRoles.NameRole);
655− AccountsService.user = user;
656− LauncherModel.setUser(user);
657− }
379+ onEmergencyCall: startLockedApp("dialer-app")
658380
659− onTapped: {
660− if (!tutorial.running) {
661− launcher.tease();
662− }
663− }
664− onDraggingChanged: {
665− if (dragging && !tutorial.running) {
666− launcher.tease();
667− }
381+ Timer {
382+ // See powerConnection for why this is useful
383+ id: showGreeterDelayed
384+ interval: 1
385+ onTriggered: {
386+ greeter.forceShow();
668387 }
388+ }
669389
670− Binding {
671− target: ApplicationManager
672− property: "suspended"
673− value: (greeter.shown && greeterWrapper.showProgress == 1) || lockscreen.shown
674− }
390+ Binding {
391+ target: ApplicationManager
392+ property: "suspended"
393+ value: greeter.shown
675394 }
676395 }
677396
@@ -680,7 +399,7 @@ Item {
680399 target: callManager
681400
682401 onHasCallsChanged: {
683− if (shell.locked && callManager.hasCalls && greeter.lockedApp !== "dialer-app") {
402+ if (greeter.locked && callManager.hasCalls && greeter.lockedApp !== "dialer-app") {
684403 // We just received an incoming call while locked. The
685404 // indicator will have already launched dialer-app for us, but
686405 // there is a race between "hasCalls" changing and the dialer
@@ -720,9 +439,7 @@ Item {
720439 return
721440 }
722441
723− if (LightDM.Greeter.active) {
724− greeter.startUnlock()
725− }
442+ greeter.notifyAboutToFocusApp("unity8-dash");
726443
727444 var animate = !LightDM.Greeter.active && !stages.shown
728445 dash.setCurrentScope(0, animate, false)
@@ -730,17 +447,11 @@ Item {
730447 }
731448
732449 function showDash() {
733− if (greeter.hasLockedApp || // just in case user gets here
734− (!greeter.narrowMode && shell.locked)) {
735− return
736− }
737−
738− if (greeter.shown) {
739− greeter.hideRight();
450+ if (greeter.notifyShowingDashFromDrag()) {
740451 launcher.fadeOut();
741452 }
742453
743− if (ApplicationManager.focusedApplicationId != "unity8-dash") {
454+ if (!greeter.locked && ApplicationManager.focusedApplicationId != "unity8-dash") {
744455 ApplicationManager.requestFocusApplication("unity8-dash")
745456 launcher.fadeOut();
746457 }
@@ -758,7 +469,9 @@ Item {
758469 anchors.fill: parent //because this draws indicator menus
759470 indicators {
760471 hides: [launcher]
761− available: tutorial.panelEnabled && (!shell.locked || AccountsService.enableIndicatorsWhileLocked) && !greeter.hasLockedApp
472+ available: tutorial.panelEnabled
473+ && (!greeter.locked || AccountsService.enableIndicatorsWhileLocked)
474+ && !greeter.hasLockedApp
762475 contentEnabled: tutorial.panelContentEnabled
763476 width: parent.width > units.gu(60) ? units.gu(40) : parent.width
764477
@@ -772,7 +485,7 @@ Item {
772485 }
773486 }
774487 callHint {
775− greeterShown: greeter.shown || lockscreen.shown
488+ greeterShown: greeter.shown
776489 }
777490
778491 property bool topmostApplicationIsFullscreen:
@@ -794,7 +507,9 @@ Item {
794507 anchors.bottom: parent.bottom
795508 width: parent.width
796509 dragAreaWidth: shell.edgeSize
797− available: tutorial.launcherEnabled && (!shell.locked || AccountsService.enableLauncherWhileLocked) && !greeter.hasLockedApp
510+ available: tutorial.launcherEnabled
511+ && (!greeter.locked || AccountsService.enableLauncherWhileLocked)
512+ && !greeter.hasLockedApp
798513 inverted: usageModeSettings.usageMode === "Staged"
799514 shadeBackground: !tutorial.running
800515
@@ -806,11 +521,10 @@ Item {
806521 }
807522 }
808523 onLauncherApplicationSelected: {
809− if (greeter.hasLockedApp) {
810− greeter.startUnlock()
811− }
812− if (!tutorial.running)
524+ if (!tutorial.running) {
525+ greeter.notifyAboutToFocusApp(appId);
813526 shell.activateApplication(appId)
527+ }
814528 }
815529 onShownChanged: {
816530 if (shown) {
tests/autopilot/unity8/shell/emulators/greeter.py+19−3
@@ -19,6 +19,7 @@
1919
2020import ubuntuuitoolkit
2121
22+from autopilot.utilities import sleep
2223from unity8.shell.emulators import UnityEmulatorBase
2324
2425
@@ -26,12 +27,27 @@ class Greeter(UnityEmulatorBase):
2627 """An emulator that understands the greeter screen."""
2728
2829 def wait_swiped_away(self):
29− self.showProgress.wait_for(0)
30+ # We have to be careful here, because coverPage can go away at any time
31+ # if there isn't a lockscreen behind it (it hides completely, then
32+ # the greeter disposes it). But if there *is* a lockscreen, then we
33+ # need a different check, by looking at its showProgress. So make our
34+ # own wait_for loop and check both possibilities.
35+ for i in range(10):
36+ if not self.required:
37+ return
38+ coverPage = self.select_single(objectName='coverPage')
39+ if coverPage.showProgress == 0:
40+ return
41+ sleep(1)
42+
43+ raise AssertionError("Greeter cover page still up after 10s")
44+
3045
3146 def swipe(self):
3247 """Swipe the greeter screen away."""
33− self.created.wait_for(True)
34− self.showProgress.wait_for(1)
48+ self.waiting.wait_for(False)
49+ coverPage = self.select_single(objectName='coverPage')
50+ coverPage.showProgress.wait_for(1)
3551
3652 rect = self.globalRect
3753 start_x = rect[0] + rect[2] - 3
tests/autopilot/unity8/shell/emulators/main_window.py−6
@@ -35,12 +35,6 @@ class QQuickView(emulators.UnityEmulatorBase):
3535 def get_greeter(self):
3636 return self.select_single(Greeter)
3737
38− def get_greeter_content_loader(self):
39− return self.wait_select_single(
40− "QQuickLoader",
41− objectName="greeterContentLoader"
42− )
43−
4438 def get_login_loader(self):
4539 return self.select_single("QQuickLoader", objectName="loginLoader")
4640
tests/autopilot/unity8/shell/tests/__init__.py+2−3
@@ -375,9 +375,8 @@ class UnityTestCase(AutopilotTestCase):
375375 self._proxy = None
376376
377377 def wait_for_unity(self):
378− greeter_content_loader = self.main_window.wait_select_single(
379− objectName='greeterContentLoader')
380− greeter_content_loader.progress.wait_for(1)
378+ greeter = self.main_window.wait_select_single(objectName='greeter')
379+ greeter.waiting.wait_for(False)
381380
382381 def get_dash(self):
383382 pid = process_helpers.get_job_pid('unity8-dash')
tests/autopilot/unity8/shell/tests/test_lock_screen.py+8−12
@@ -47,14 +47,13 @@ class TestLockscreen(UnityTestCase):
4747 self.launch_unity()
4848 greeter = self.main_window.get_greeter()
4949
50− if greeter.narrowMode:
50+ if not greeter.tabletMode:
5151 greeter.swipe()
5252 lockscreen = self._wait_for_lockscreen()
5353 self.main_window.enter_pin_code("1234")
54− self.assertThat(lockscreen.shown, Eventually(Equals(False)))
5554 else:
5655 self._enter_prompt_passphrase("1234\n")
57− self.assertThat(greeter.shown, Eventually(Equals(False)))
56+ self.assertThat(greeter.shown, Eventually(Equals(False)))
5857
5958 def test_can_unlock_passphrase_screen(self):
6059 """Must be able to unlock the passphrase entry screen."""
@@ -63,14 +62,13 @@ class TestLockscreen(UnityTestCase):
6362 self.launch_unity()
6463 greeter = self.main_window.get_greeter()
6564
66− if greeter.narrowMode:
65+ if not greeter.tabletMode:
6766 greeter.swipe()
6867 lockscreen = self._wait_for_lockscreen()
6968 self._enter_pin_passphrase("password")
70− self.assertThat(lockscreen.shown, Eventually(Equals(False)))
7169 else:
7270 self._enter_prompt_passphrase("password")
73− self.assertThat(greeter.shown, Eventually(Equals(False)))
71+ self.assertThat(greeter.shown, Eventually(Equals(False)))
7472
7573 def test_pin_screen_wrong_code(self):
7674 """Entering the wrong pin code must not dismiss the lock screen."""
@@ -78,18 +76,17 @@ class TestLockscreen(UnityTestCase):
7876 self.launch_unity()
7977 greeter = self.main_window.get_greeter()
8078
81− if greeter.narrowMode:
79+ if not greeter.tabletMode:
8280 greeter.swipe()
8381 lockscreen = self._wait_for_lockscreen()
8482 self.main_window.enter_pin_code("4321")
8583 pinentryField = self.main_window.get_pinentryField()
8684 self.assertThat(pinentryField.text, Eventually(Equals("")))
87− self.assertThat(lockscreen.shown, Eventually(Equals(True)))
8885 else:
8986 self._enter_prompt_passphrase("4231\n")
9087 prompt = self.main_window.get_greeter().get_prompt()
9188 self.assertThat(prompt.text, Eventually(Equals("")))
92− self.assertThat(greeter.shown, Eventually(Equals(True)))
89+ self.assertThat(greeter.shown, Eventually(Equals(True)))
9390
9491 def test_passphrase_screen_wrong_password(self):
9592 """Entering the wrong password must not dismiss the lock screen."""
@@ -97,18 +94,17 @@ class TestLockscreen(UnityTestCase):
9794 self.launch_unity()
9895 greeter = self.main_window.get_greeter()
9996
100− if greeter.narrowMode:
97+ if not greeter.tabletMode:
10198 greeter.swipe()
10299 lockscreen = self._wait_for_lockscreen()
103100 self._enter_pin_passphrase("foobar")
104101 pinentryField = self.main_window.get_pinentryField()
105102 self.assertThat(pinentryField.text, Eventually(Equals("")))
106− self.assertThat(lockscreen.shown, Eventually(Equals(True)))
107103 else:
108104 self._enter_prompt_passphrase("foobar")
109105 prompt = self.main_window.get_greeter().get_prompt()
110106 self.assertThat(prompt.text, Eventually(Equals("")))
111− self.assertThat(greeter.shown, Eventually(Equals(True)))
107+ self.assertThat(greeter.shown, Eventually(Equals(True)))
112108
113109 def _wait_for_lockscreen(self):
114110 """Wait for the lock screen to load, and return it."""
tests/plugins/LightDM/CMakeLists.txt+19−4
@@ -4,17 +4,32 @@ add_executable(GreeterDBusTestExec
44 )
55qt5_use_modules(GreeterDBusTestExec Core DBus Quick Test)
66
7+add_executable(GreeterUsersModelTestExec
8+ usersmodel.cpp
9+ ${CMAKE_SOURCE_DIR}/plugins/LightDM/UsersModel.cpp
10+ ${CMAKE_SOURCE_DIR}/plugins/Utils/unitysortfilterproxymodelqml.cpp
11+ )
12+qt5_use_modules(GreeterUsersModelTestExec Core Test)
13+
714include_directories(
815 ${CMAKE_CURRENT_BINARY_DIR}
916 ${CMAKE_SOURCE_DIR}/plugins/LightDM
17+ ${CMAKE_SOURCE_DIR}/plugins/Utils
1018 ${CMAKE_SOURCE_DIR}/tests/mocks/LightDM
11− ${LIBLIGHTDM_INCLUDE_DIRS}
1219 )
1320
14−add_dependencies(GreeterDBusTestExec MockLightDM)
15−target_link_libraries(GreeterDBusTestExec -L${CMAKE_BINARY_DIR}/tests/mocks/LightDM/liblightdm
16− -llightdm-qt5-2)
21+target_link_libraries(GreeterDBusTestExec
22+ -L${CMAKE_BINARY_DIR}/tests/mocks/LightDM/liblightdm
23+ -llightdm-qt5-2
24+ )
25+
26+target_link_libraries(GreeterUsersModelTestExec
27+ -L${CMAKE_BINARY_DIR}/tests/mocks/LightDM/liblightdm
28+ -llightdm-qt5-2
29+ )
1730
1831add_definitions(-DCURRENT_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}")
1932
2033add_binary_qml_test(GreeterDBus "${CMAKE_BINARY_DIR}/tests/mocks/LightDM/liblightdm" MockLightDM "QML2_IMPORT_PATH=${CMAKE_BINARY_DIR}/tests/mocks")
34+
35+add_binary_qml_test(GreeterUsersModel "${CMAKE_BINARY_DIR}/tests/mocks/LightDM/liblightdm" MockLightDM "LIBLIGHTDM_MOCK_MODE=full")
tests/plugins/LightDM/usersmodel.cppnew file+84
@@ -0,0 +1,84 @@
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+#include "UsersModel.h"
18+
19+#include <QLightDM/UsersModel>
20+#include <QtTest>
21+
22+class GreeterUsersModelTest : public QObject
23+{
24+ Q_OBJECT
25+
26+private:
27+ static int findName(QAbstractItemModel *model, const QString &name)
28+ {
29+ for (int i = 0; i < model->rowCount(QModelIndex()); i++) {
30+ if (model->data(model->index(i, 0), QLightDM::UsersModel::NameRole).toString() == name) {
31+ return i;
32+ }
33+ }
34+ return -1;
35+ }
36+
37+ static QString getStringValue(QAbstractItemModel *model, const QString &name, int role)
38+ {
39+ int i = findName(model, name);
40+ return model->data(model->index(i, 0), role).toString();
41+ }
42+
43+private Q_SLOTS:
44+
45+ void init()
46+ {
47+ model = new UsersModel();
48+ QVERIFY(model);
49+ sourceModel = new QLightDM::UsersModel();
50+ QVERIFY(sourceModel);
51+ }
52+
53+ void cleanup()
54+ {
55+ delete model;
56+ delete sourceModel;
57+ }
58+
59+ void testMangleColor()
60+ {
61+ QString background = getStringValue(sourceModel, "color-background", QLightDM::UsersModel::BackgroundPathRole);
62+ QVERIFY(background == "#dd4814");
63+
64+ background = getStringValue(model, "color-background", QLightDM::UsersModel::BackgroundPathRole);
65+ QVERIFY(background == "data:image/svg+xml,<svg><rect width='100%' height='100%' fill='#dd4814'/></svg>");
66+ }
67+
68+ void testMangleEmptyName()
69+ {
70+ QString name = getStringValue(sourceModel, "empty-name", QLightDM::UsersModel::RealNameRole);
71+ QVERIFY(name == "");
72+
73+ name = getStringValue(model, "empty-name", QLightDM::UsersModel::RealNameRole);
74+ QVERIFY(name == "empty-name");
75+ }
76+
77+private:
78+ UsersModel *model;
79+ QLightDM::UsersModel *sourceModel;
80+};
81+
82+QTEST_MAIN(GreeterUsersModelTest)
83+
84+#include "usersmodel.moc"
tests/qmltests/CMakeLists.txt+4−3
@@ -60,10 +60,11 @@ add_qml_test(Dash/ScopeSettings ScopeSettingList)
6060add_qml_test(Dash/ScopeSettings ScopeSettingNumber)
6161add_qml_test(Dash/ScopeSettings ScopeSettingString)
6262add_qml_test(Dash/ScopeSettings ScopeSettingsWidgetFactory)
63−add_qml_test(Greeter MultiGreeter)
64−add_qml_test(Greeter SingleGreeter)
65−add_qml_test(Greeter Infographics)
6663add_qml_test(Greeter Clock)
64+add_qml_test(Greeter Greeter)
65+add_qml_test(Greeter Infographics)
66+add_qml_test(Greeter NarrowView)
67+add_qml_test(Greeter WideView)
6768add_qml_test(Launcher Launcher)
6869add_qml_test(Notifications Notifications)
6970add_qml_test(Notifications VisualSnapDecisionsQueue)
tests/qmltests/Greeter/TestView.qmlnew file+99
@@ -0,0 +1,99 @@
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.3
18+import Ubuntu.Components 1.1
19+import "../Components"
20+
21+Item {
22+ objectName: "testView"
23+
24+ property real dragHandleLeftMargin
25+ property real launcherOffset
26+ property int currentIndex
27+ property int delayMinutes
28+ property real backgroundTopMargin
29+ property url background
30+ property bool locked
31+ property bool alphanumeric
32+ property var userModel
33+ property var infographicModel
34+ readonly property bool fullyShown: _fullyShown
35+ readonly property bool required: _required
36+
37+ property bool _fullyShown: true
38+ property bool _required: true
39+
40+ signal selected(int index)
41+ signal responded(string response)
42+ signal tease()
43+ signal emergencyCall()
44+
45+ signal _showMessageCalled(string html)
46+ signal _showPromptCalled(string text, bool isSecret, bool isDefaultPrompt)
47+ signal _showLastChanceCalled()
48+ signal _hideCalled()
49+ signal _notifyAuthenticationSucceededCalled()
50+ signal _notifyAuthenticationFailedCalled()
51+ signal _resetCalled()
52+ signal _tryToUnlockCalled(bool toTheRight)
53+
54+ function showMessage(html) {
55+ _showMessageCalled(html);
56+ }
57+
58+ function showPrompt(text, isSecret, isDefaultPrompt) {
59+ _showPromptCalled(text, isSecret, isDefaultPrompt);
60+ }
61+
62+ function showLastChance() {
63+ _showLastChanceCalled();
64+ }
65+
66+ function hide() {
67+ _hideCalled();
68+ _required = false;
69+ _fullyShown = false;
70+ }
71+
72+ function notifyAuthenticationSucceeded() {
73+ _notifyAuthenticationSucceededCalled();
74+ }
75+
76+ function notifyAuthenticationFailed() {
77+ _notifyAuthenticationFailedCalled();
78+ }
79+
80+ function reset() {
81+ _resetCalled();
82+ }
83+
84+ function tryToUnlock(toTheRight) {
85+ _tryToUnlockCalled(toTheRight);
86+ return true;
87+ }
88+
89+ Rectangle {
90+ anchors.fill: parent
91+ color: "black"
92+
93+ Label {
94+ text: "Fake view, nothing to see here"
95+ color: "white"
96+ anchors.centerIn: parent
97+ }
98+ }
99+}
tests/qmltests/Greeter/tst_Greeter.qmlnew file+472
@@ -0,0 +1,472 @@
1+/*
2+ * Copyright 2015 Canonical Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.0
18+import QtTest 1.0
19+import ".."
20+import "../../../qml/Greeter"
21+import Ubuntu.Components 0.1
22+import AccountsService 0.1
23+import LightDM 0.1 as LightDM
24+import Unity.Test 0.1 as UT
25+
26+Item {
27+ width: units.gu(120)
28+ height: units.gu(80)
29+
30+ property url defaultBackground: Qt.resolvedUrl("../../../qml/graphics/tablet_background.jpg")
31+
32+ Component.onCompleted: {
33+ // set the mock mode before loading
34+ LightDM.Greeter.mockMode = "full";
35+ LightDM.Users.mockMode = "full";
36+ loader.active = true;
37+ }
38+
39+ Loader {
40+ id: loader
41+
42+ active: false
43+ anchors.fill: parent
44+
45+ property bool itemDestroyed: false
46+ sourceComponent: Component {
47+ Greeter {
48+ width: loader.width
49+ height: loader.height
50+ background: defaultBackground
51+ viewSource: Qt.resolvedUrl("TestView.qml")
52+
53+ Component.onDestruction: {
54+ loader.itemDestroyed = true;
55+ }
56+ }
57+ }
58+ }
59+
60+ SignalSpy {
61+ id: teaseSpy
62+ target: loader.item
63+ signalName: "tease"
64+ }
65+
66+ SignalSpy {
67+ id: sessionStartedSpy
68+ target: loader.item
69+ signalName: "sessionStarted"
70+ }
71+
72+ SignalSpy {
73+ id: emergencyCallSpy
74+ target: loader.item
75+ signalName: "emergencyCall"
76+ }
77+
78+ UT.UnityTestCase {
79+ id: testCase
80+ name: "Greeter"
81+ when: windowShown
82+
83+ property Item greeter: loader.status === Loader.Ready ? loader.item : null
84+ property Item view
85+
86+ SignalSpy {
87+ id: viewShowMessageSpy
88+ target: testCase.view
89+ signalName: "_showMessageCalled"
90+ }
91+
92+ SignalSpy {
93+ id: viewShowPromptSpy
94+ target: testCase.view
95+ signalName: "_showPromptCalled"
96+ }
97+
98+ SignalSpy {
99+ id: viewShowLastChanceSpy
100+ target: testCase.view
101+ signalName: "_showLastChanceCalled"
102+ }
103+
104+ SignalSpy {
105+ id: viewHideSpy
106+ target: testCase.view
107+ signalName: "_hideCalled"
108+ }
109+
110+ SignalSpy {
111+ id: viewAuthenticationSucceededSpy
112+ target: testCase.view
113+ signalName: "_notifyAuthenticationSucceededCalled"
114+ }
115+
116+ SignalSpy {
117+ id: viewAuthenticationFailedSpy
118+ target: testCase.view
119+ signalName: "_notifyAuthenticationFailedCalled"
120+ }
121+
122+ SignalSpy {
123+ id: viewResetSpy
124+ target: testCase.view
125+ signalName: "_resetCalled"
126+ }
127+
128+ SignalSpy {
129+ id: viewTryToUnlockSpy
130+ target: testCase.view
131+ signalName: "_tryToUnlockCalled"
132+ }
133+
134+ function init() {
135+ teaseSpy.clear();
136+ sessionStartedSpy.clear();
137+ emergencyCallSpy.clear();
138+ viewShowMessageSpy.clear();
139+ viewShowPromptSpy.clear();
140+ viewShowLastChanceSpy.clear();
141+ viewHideSpy.clear();
142+ viewAuthenticationSucceededSpy.clear();
143+ viewAuthenticationFailedSpy.clear();
144+ viewResetSpy.clear();
145+ viewTryToUnlockSpy.clear();
146+ tryCompare(greeter, "waiting", false);
147+ view = findChild(greeter, "testView");
148+ verifySelected(LightDM.Users.data(0, LightDM.UserRoles.NameRole));
149+ }
150+
151+ function cleanup() {
152+ loader.itemDestroyed = false;
153+ loader.active = false;
154+ tryCompare(loader, "status", Loader.Null);
155+ tryCompare(loader, "item", null);
156+ tryCompare(loader, "itemDestroyed", true);
157+ loader.active = true;
158+ tryCompare(loader, "status", Loader.Ready);
159+ removeTimeConstraintsFromDirectionalDragAreas(loader.item);
160+ }
161+
162+ function getIndexOf(name) {
163+ for (var i = 0; i < LightDM.Users.count; i++) {
164+ if (name === LightDM.Users.data(i, LightDM.UserRoles.NameRole)) {
165+ return i;
166+ }
167+ }
168+ fail("Didn't find name")
169+ return -1;
170+ }
171+
172+ function selectUser(name) {
173+ var i = getIndexOf(name);
174+ view.selected(i);
175+ verifySelected(name);
176+ return i;
177+ }
178+
179+ function verifySelected(name) {
180+ var i = getIndexOf(name);
181+ compare(view.currentIndex, i);
182+ compare(AccountsService.user, name);
183+ compare(LightDM.Greeter.authenticationUser, name);
184+ }
185+
186+ function verifyLoggedIn() {
187+ tryCompare(sessionStartedSpy, "count", 1);
188+ verify(viewAuthenticationSucceededSpy.count > 0);
189+ compare(LightDM.Greeter.authenticated, true);
190+ compare(greeter.shown, false);
191+ }
192+
193+ function test_unlockPass() {
194+ selectUser("has-password");
195+ tryCompare(viewShowPromptSpy, "count", 1);
196+ compare(viewShowPromptSpy.signalArguments[0][0], "Password");
197+ compare(viewShowPromptSpy.signalArguments[0][1], true);
198+ compare(viewShowPromptSpy.signalArguments[0][2], true);
199+
200+ view.responded("password");
201+ verifyLoggedIn();
202+ }
203+
204+ function test_unlockFail() {
205+ selectUser("has-password");
206+ tryCompare(viewShowPromptSpy, "count", 1);
207+ compare(viewShowPromptSpy.signalArguments[0][0], "Password");
208+ compare(viewShowPromptSpy.signalArguments[0][1], true);
209+ compare(viewShowPromptSpy.signalArguments[0][2], true);
210+
211+ view.responded("wr0ng p4ssw0rd");
212+ tryCompare(viewAuthenticationFailedSpy, "count", 1);
213+
214+ tryCompare(viewShowPromptSpy, "count", 2);
215+ compare(viewShowPromptSpy.signalArguments[0][0], "Password");
216+ compare(viewShowPromptSpy.signalArguments[0][1], true);
217+ compare(viewShowPromptSpy.signalArguments[0][2], true);
218+ }
219+
220+ function test_promptless() {
221+ selectUser("no-password");
222+ tryCompare(viewAuthenticationSucceededSpy, "count", 1);
223+ compare(sessionStartedSpy.count, 1);
224+ compare(viewShowPromptSpy.count, 0);
225+ compare(viewHideSpy.count, 0);
226+ compare(view.locked, false);
227+ }
228+
229+ function test_twoFactorPass() {
230+ selectUser("two-factor");
231+ tryCompare(viewShowPromptSpy, "count", 1);
232+ compare(viewShowPromptSpy.signalArguments[0][0], "Password");
233+ compare(viewShowPromptSpy.signalArguments[0][1], true);
234+ compare(viewShowPromptSpy.signalArguments[0][2], true);
235+
236+ view.responded("password");
237+ tryCompare(viewShowPromptSpy, "count", 2);
238+ compare(viewShowPromptSpy.signalArguments[1][0], "otp");
239+ compare(viewShowPromptSpy.signalArguments[1][1], false);
240+ compare(viewShowPromptSpy.signalArguments[1][2], false);
241+
242+ view.responded("otp");
243+ verifyLoggedIn();
244+ }
245+
246+ function test_twoFactorFailOnFirst() {
247+ selectUser("two-factor");
248+ tryCompare(viewShowPromptSpy, "count", 1);
249+ compare(viewShowPromptSpy.signalArguments[0][0], "Password");
250+ compare(viewShowPromptSpy.signalArguments[0][1], true);
251+ compare(viewShowPromptSpy.signalArguments[0][2], true);
252+
253+ view.responded("wr0ng p4ssw0rd");
254+ tryCompare(viewAuthenticationFailedSpy, "count", 1);
255+
256+ tryCompare(viewShowPromptSpy, "count", 2);
257+ compare(viewShowPromptSpy.signalArguments[0][0], "Password");
258+ compare(viewShowPromptSpy.signalArguments[0][1], true);
259+ compare(viewShowPromptSpy.signalArguments[0][2], true);
260+ }
261+
262+ function test_twoFactorFailOnSecond() {
263+ selectUser("two-factor");
264+ tryCompare(viewShowPromptSpy, "count", 1);
265+ compare(viewShowPromptSpy.signalArguments[0][0], "Password");
266+ compare(viewShowPromptSpy.signalArguments[0][1], true);
267+ compare(viewShowPromptSpy.signalArguments[0][2], true);
268+
269+ view.responded("password");
270+ tryCompare(viewShowPromptSpy, "count", 2);
271+ compare(viewShowPromptSpy.signalArguments[1][0], "otp");
272+ compare(viewShowPromptSpy.signalArguments[1][1], false);
273+ compare(viewShowPromptSpy.signalArguments[1][2], false);
274+
275+ view.responded("wr0ng p4ssw0rd");
276+ tryCompare(viewAuthenticationFailedSpy, "count", 1);
277+
278+ tryCompare(viewShowPromptSpy, "count", 3);
279+ compare(viewShowPromptSpy.signalArguments[0][0], "Password");
280+ compare(viewShowPromptSpy.signalArguments[0][1], true);
281+ compare(viewShowPromptSpy.signalArguments[0][2], true);
282+ }
283+
284+ function test_htmlInfoPrompt() {
285+ selectUser("html-info-prompt");
286+ tryCompare(viewShowPromptSpy, "count", 1);
287+ compare(viewShowMessageSpy.count, 1);
288+ compare(viewShowMessageSpy.signalArguments[0][0], "&lt;b&gt;&amp;&lt;/b&gt;");
289+ }
290+
291+ function test_multiInfoPrompt() {
292+ selectUser("multi-info-prompt");
293+ tryCompare(viewShowPromptSpy, "count", 1);
294+ compare(viewShowMessageSpy.count, 3);
295+ compare(viewShowMessageSpy.signalArguments[0][0], "Welcome to Unity Greeter");
296+ compare(viewShowMessageSpy.signalArguments[1][0], "<font color=\"#df382c\">This is an error</font>");
297+ compare(viewShowMessageSpy.signalArguments[2][0], "You should have seen three messages");
298+ }
299+
300+ function test_waiting() {
301+ // Make sure we unset 'waiting' on prompt
302+ selectUser("has-password");
303+ compare(greeter.waiting, true);
304+ tryCompare(greeter, "waiting", false);
305+
306+ // Make sure we unset 'waiting' on authentication
307+ selectUser("no-password");
308+ compare(greeter.waiting, true);
309+ tryCompare(greeter, "waiting", false);
310+ }
311+
312+ function test_locked() {
313+ selectUser("has-password");
314+ compare(view.locked, true);
315+
316+ LightDM.Greeter.active = false;
317+ compare(view.locked, false);
318+ LightDM.Greeter.active = true;
319+
320+ greeter.forcedUnlock = true;
321+ compare(view.locked, false);
322+ greeter.forcedUnlock = false;
323+
324+ selectUser("no-password");
325+ tryCompare(view, "locked", false);
326+ selectUser("has-password");
327+ }
328+
329+ function test_fullyShown() {
330+ compare(greeter.fullyShown, true);
331+ view.hide();
332+ compare(greeter.fullyShown, false);
333+ }
334+
335+ function test_alphanumeric() {
336+ selectUser("has-password");
337+ compare(view.alphanumeric, true);
338+ selectUser("has-pin");
339+ compare(view.alphanumeric, false);
340+ }
341+
342+ function test_background() {
343+ greeter.background = "testing";
344+ compare(view.background, Qt.resolvedUrl("testing"));
345+ }
346+
347+ function test_notifyAboutToFocusApp() {
348+ greeter.notifyAboutToFocusApp("fake-app");
349+ compare(viewTryToUnlockSpy.count, 1);
350+ compare(viewTryToUnlockSpy.signalArguments[0][0], false);
351+ }
352+
353+ function test_notifyShowingDashFromDrag() {
354+ compare(greeter.notifyShowingDashFromDrag("fake-app"), true);
355+ compare(viewTryToUnlockSpy.count, 1);
356+ compare(viewTryToUnlockSpy.signalArguments[0][0], true);
357+ }
358+
359+ function test_dragHandleLeftMargin() {
360+ compare(view.dragHandleLeftMargin, 0);
361+ greeter.dragHandleLeftMargin = 5;
362+ compare(view.dragHandleLeftMargin, 5);
363+ }
364+
365+ function test_launcherOffset() {
366+ compare(view.launcherOffset, 0);
367+ greeter.launcherOffset = 5;
368+ tryCompare(view, "launcherOffset", 5);
369+ }
370+
371+ function test_laucherOffsetAnimation() {
372+ // Our logic for smoothing launcherOffset when it suddenly goes to
373+ // zero is a bit complicated. Let's just make sure it works here.
374+
375+ launcherOffsetWatcher.target = view;
376+
377+ // should follow immediately
378+ launcherOffsetWatcher.values = [];
379+ greeter.launcherOffset = 100;
380+ compare(view.launcherOffset, 100);
381+ compare(launcherOffsetWatcher.values.length, 1);
382+
383+ // should interpolate values until it reaches 0
384+ launcherOffsetWatcher.values = [];
385+ greeter.launcherOffset = 0;
386+ tryCompare(view, "launcherOffset", 0);
387+ verify(launcherOffsetWatcher.values.length > 1);
388+ for (var i = 0; i < launcherOffsetWatcher.values.length - 1; ++i) {
389+ verify(launcherOffsetWatcher.values[i] > 0.0);
390+ verify(launcherOffsetWatcher.values[i] < 100.0);
391+ }
392+ }
393+ Connections {
394+ id: launcherOffsetWatcher
395+ property var values: []
396+ onLauncherOffsetChanged: {
397+ values.push(target.launcherOffset);
398+ }
399+ }
400+
401+ function test_backgroundTopMargin() {
402+ compare(view.backgroundTopMargin, 0);
403+ greeter.y = 5;
404+ compare(view.backgroundTopMargin, -5);
405+ }
406+
407+ function test_differentPrompt() {
408+ selectUser("different-prompt");
409+ tryCompare(viewShowPromptSpy, "count", 1);
410+ compare(viewShowPromptSpy.signalArguments[0][0], "Secret word");
411+ compare(viewShowPromptSpy.signalArguments[0][1], true);
412+ compare(viewShowPromptSpy.signalArguments[0][2], false);
413+ }
414+
415+ function test_authError() {
416+ selectUser("auth-error");
417+ tryCompare(viewAuthenticationFailedSpy, "count", 1);
418+ compare(viewShowPromptSpy.count, 0);
419+ compare(view.locked, true);
420+ }
421+
422+ function test_statsWelcomeScreen() {
423+ // Test logic in greeter that turns statsWelcomeScreen setting into infographic changes
424+ selectUser("has-password");
425+ compare(LightDM.Infographic.username, "has-password");
426+ AccountsService.statsWelcomeScreen = false;
427+ compare(LightDM.Infographic.username, "");
428+ AccountsService.statsWelcomeScreen = true;
429+ compare(LightDM.Infographic.username, "has-password");
430+ }
431+
432+ function test_dbusRequestAuthenticationUser() {
433+ selectUser("no-password");
434+ LightDM.Greeter.requestAuthenticationUser("has-password");
435+ verifySelected("has-password");
436+ }
437+
438+ function test_dbusHideGreeter() {
439+ compare(view.required, true);
440+ LightDM.Greeter.hideGreeter();
441+ compare(view.required, false);
442+ compare(greeter.required, false);
443+ }
444+
445+ function test_dbusShowGreeterFromHiddenState() {
446+ greeter.hide();
447+ compare(greeter.required, false);
448+
449+ LightDM.Greeter.showGreeter();
450+ compare(greeter.required, true);
451+ compare(greeter.fullyShown, true);
452+ view = findChild(greeter, "testView");
453+ compare(view.required, true);
454+
455+ // Can't test some of the stuff called on 'view' here because
456+ // the view was torn down and created again. So the spies missed
457+ // the good stuff while it down. See next test for more.
458+ }
459+
460+ function test_dbusShowGreeterFromShownState() {
461+ selectUser("has-password");
462+ compare(viewResetSpy.count, 1);
463+ tryCompare(viewShowPromptSpy, "count", 1);
464+
465+ viewResetSpy.clear();
466+ viewShowPromptSpy.clear();
467+
468+ LightDM.Greeter.showGreeter();
469+ compare(viewResetSpy.count, 1);
470+ }
471+ }
472+}
tests/qmltests/Greeter/tst_MultiGreeter.qmldeleted−475
@@ -1,475 +0,0 @@
1−/*
2− * Copyright 2013 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 ".."
21−import "../../../qml/Greeter"
22−import Ubuntu.Components 0.1
23−import LightDM 0.1 as LightDM
24−import Unity.Test 0.1 as UT
25−
26−Item {
27− id: root
28− width: units.gu(140)
29− height: units.gu(80)
30−
31− QtObject {
32− id: fakeInputMethod
33− property bool visible: fakeKeyboard.visible
34− property var keyboardRectangle: QtObject {
35− property real x: fakeKeyboard.x
36− property real y: fakeKeyboard.y
37− property real width: fakeKeyboard.width
38− property real height: fakeKeyboard.height
39− }
40− }
41−
42− Binding {
43− target: LightDM.Greeter
44− property: "mockMode"
45− value: "full"
46− }
47− Binding {
48− target: LightDM.Users
49− property: "mockMode"
50− value: "full"
51− }
52−
53− Greeter {
54− id: greeter
55− width: units.gu(120)
56− height: root.height
57− inputMethod: fakeInputMethod
58− locked: !LightDM.Greeter.authenticated
59− }
60−
61− Component {
62− id: greeterComponent
63− Greeter {
64− SignalSpy {
65− objectName: "selectedSpy"
66− target: parent
67− signalName: "selected"
68− }
69− }
70− }
71−
72− Rectangle {
73− id: fakeKeyboard
74− color: "green"
75− opacity: 0.7
76− anchors.bottom: root.bottom
77− width: greeter.width
78− height: greeter.height * 0.6
79− visible: keyboardVisibleCheckbox.checked
80− Text {
81− text: "Keyboard Rectangle"
82− color: "yellow"
83− font.bold: true
84− fontSizeMode: Text.Fit
85− minimumPixelSize: 10; font.pixelSize: 200
86− verticalAlignment: Text.AlignVCenter
87− x: (parent.width - width) / 2
88− y: (parent.height - height) / 2
89− width: parent.width
90− height: parent.height
91− }
92− }
93−
94− Item {
95− anchors {
96− top: root.top
97− bottom: root.bottom
98− left: greeter.right
99− right: root.right
100− }
101− RowLayout {
102− Layout.fillWidth: true
103− CheckBox {
104− id: keyboardVisibleCheckbox
105− }
106− Label { text: "Keyboard Visible"; anchors.verticalCenter: parent.verticalCenter }
107− }
108− }
109−
110− SignalSpy {
111− id: unlockSpy
112− target: greeter
113− signalName: "unlocked"
114− }
115−
116− SignalSpy {
117− id: selectionSpy
118− target: greeter
119− signalName: "selected"
120− }
121−
122− SignalSpy {
123− id: tappedSpy
124− target: greeter
125− signalName: "tapped"
126− }
127−
128− UT.UnityTestCase {
129− name: "MultiGreeter"
130− when: windowShown
131−
132− function cleanup() {
133− keyboardVisibleCheckbox.checked = false;
134− }
135−
136− function select_index(i) {
137− // We could be anywhere in list; find target index to know which direction
138− var userlist = findChild(greeter, "userList")
139− if (userlist.currentIndex == i)
140− keyClick(Qt.Key_Escape) // Reset state if we're not moving
141− while (userlist.currentIndex != i) {
142− var next = userlist.currentIndex + 1
143− if (userlist.currentIndex > i) {
144− next = userlist.currentIndex - 1
145− }
146− var account = findChild(greeter, "username"+next)
147− mouseClick(account, 1, 1)
148− tryCompare(userlist, "currentIndex", next)
149− tryCompare(userlist, "movingInternally", false)
150− }
151− }
152−
153− function select_user(name) {
154− // We could be anywhere in list; find target index to know which direction
155− for (var i = 0; i < greeter.model.count; i++) {
156− if (greeter.model.data(i, LightDM.UserRoles.NameRole) == name) {
157− break
158− }
159− }
160− if (i == greeter.model.count) {
161− fail("Didn't find name")
162− return -1
163− }
164− select_index(i)
165− return i
166− }
167−
168− function test_properties() {
169− compare(greeter.multiUser, true)
170− compare(greeter.narrowMode, false)
171− }
172−
173− function test_cycle_data() {
174− var data = new Array()
175− for (var i = 0; i < greeter.model.count; i++) {
176− data[i] = {tag: greeter.model.data(i, LightDM.UserRoles.NameRole), uid: i }
177− }
178− return data
179− }
180−
181− function test_cycle(data) {
182− selectionSpy.clear();
183− var userList = findChild(greeter, "userList")
184− var waitForSignal = data.uid != 0 && userList.currentIndex != data.uid
185− select_index(data.uid)
186− tryCompare(userList, "currentIndex", data.uid)
187− tryCompare(greeter, "locked", data.tag !== "no-password")
188− if (waitForSignal) {
189− selectionSpy.wait()
190− tryCompare(selectionSpy, "count", 1)
191− }
192− }
193−
194− function test_unlock_password() {
195− select_user("no-password") // to guarantee a selected signal
196− unlockSpy.clear()
197− select_user("has-password")
198− var passwordInput = findChild(greeter, "passwordInput")
199− tryCompare(passwordInput, "opacity", 1)
200− mouseClick(passwordInput, 1, 1)
201− compare(unlockSpy.count, 0)
202− typeString("password")
203− keyClick(Qt.Key_Enter)
204− unlockSpy.wait()
205− }
206−
207− function test_unlock_wrong_password() {
208− select_user("no-password") // to guarantee a selected signal
209− unlockSpy.clear()
210− select_user("has-password")
211− wait(0) // spin event loop to start any pending animations
212− var passwordInput = findChild(greeter, "passwordInput")
213− tryCompare(passwordInput, "opacity", 1) // wait for opacity animation to be finished
214− mouseClick(passwordInput, 1, 1)
215− compare(unlockSpy.count, 0)
216− typeString("wr0ng p4ssw0rd")
217− keyClick(Qt.Key_Enter)
218− compare(unlockSpy.count, 0)
219− }
220−
221− function test_unlock_no_password() {
222− unlockSpy.clear()
223− select_user("no-password")
224− var passwordInput = findChild(greeter, "passwordInput")
225− tryCompare(passwordInput, "opacity", 1)
226− mouseClick(passwordInput, 1, 1)
227− unlockSpy.wait()
228− compare(unlockSpy.count, 1)
229− }
230−
231− function test_empty_name() {
232− for (var i = 0; i < greeter.model.count; i++) {
233− if (greeter.model.data(i, LightDM.UserRoles.NameRole) == "empty-name") {
234− compare(greeter.model.data(i, LightDM.UserRoles.RealNameRole), greeter.model.data(i, LightDM.UserRoles.NameRole))
235− return
236− }
237− }
238− fail("Didn't find empty-name")
239− }
240−
241− function test_auth_error() {
242− select_user("auth-error")
243− var passwordInput = findChild(greeter, "passwordInput")
244− tryCompare(passwordInput, "placeholderText", "Retry")
245− }
246−
247− function test_different_prompt() {
248− select_user("different-prompt")
249− var passwordInput = findChild(greeter, "passwordInput")
250− tryCompare(passwordInput, "placeholderText", "Secret word")
251− }
252−
253− function test_no_response() {
254− unlockSpy.clear()
255− select_user("no-response")
256− var passwordInput = findChild(greeter, "passwordInput")
257− tryCompare(passwordInput, "opacity", 1)
258− mouseClick(passwordInput, 1, 1)
259− compare(unlockSpy.count, 0)
260− typeString("password")
261− keyClick(Qt.Key_Enter)
262− tryCompare(passwordInput, "enabled", false)
263− keyClick(Qt.Key_Escape)
264− tryCompare(passwordInput, "enabled", true)
265− compare(unlockSpy.count, 0)
266− }
267−
268− function test_two_factor_correct() {
269− unlockSpy.clear()
270− select_user("two-factor")
271− var passwordInput = findChild(greeter, "passwordInput")
272− tryCompare(passwordInput, "opacity", 1)
273− tryCompare(passwordInput, "echoMode", TextInput.Password)
274− tryCompare(passwordInput, "placeholderText", "Password")
275− mouseClick(passwordInput, 1, 1)
276− compare(unlockSpy.count, 0)
277− typeString("password")
278− keyClick(Qt.Key_Enter)
279− tryCompare(passwordInput, "echoMode", TextInput.Normal)
280− tryCompare(passwordInput, "placeholderText", "otp")
281− tryCompare(passwordInput, "enabled", true)
282− typeString("otp")
283− keyClick(Qt.Key_Enter)
284− unlockSpy.wait()
285− }
286−
287− function test_two_factor_wrong1() {
288− unlockSpy.clear()
289− select_user("two-factor")
290− var passwordInput = findChild(greeter, "passwordInput")
291− tryCompare(passwordInput, "opacity", 1)
292− tryCompare(passwordInput, "placeholderText", "Password")
293− mouseClick(passwordInput, 1, 1)
294− compare(unlockSpy.count, 0)
295− typeString("wr0ng p4ssw0rd")
296− keyClick(Qt.Key_Enter)
297− tryCompare(passwordInput, "placeholderText", "Password")
298− tryCompare(passwordInput, "enabled", true)
299− compare(unlockSpy.count, 0)
300− }
301−
302− function test_two_factor_wrong2() {
303− unlockSpy.clear()
304− select_user("two-factor")
305− var passwordInput = findChild(greeter, "passwordInput")
306− tryCompare(passwordInput, "opacity", 1)
307− tryCompare(passwordInput, "placeholderText", "Password")
308− mouseClick(passwordInput, 1, 1)
309− compare(unlockSpy.count, 0)
310− typeString("password")
311− keyClick(Qt.Key_Enter)
312− tryCompare(passwordInput, "placeholderText", "otp")
313− tryCompare(passwordInput, "enabled", true)
314− typeString("wr0ng p4ssw0rd")
315− keyClick(Qt.Key_Enter)
316− tryCompare(passwordInput, "placeholderText", "Password")
317− tryCompare(passwordInput, "enabled", true)
318− compare(unlockSpy.count, 0)
319− }
320−
321− function test_unicode() {
322− var index = select_user("unicode")
323− var label = findChild(greeter, "username"+index)
324− tryCompare(label, "text", "가나다라마")
325− }
326−
327− function test_long_name() {
328− var index = select_user("long-name")
329− var label = findChild(greeter, "username"+index)
330− tryCompare(label, "truncated", true)
331− }
332−
333− function test_info_prompt() {
334− select_user("info-prompt")
335− var label = findChild(greeter, "infoLabel")
336− tryCompare(label, "text", "Welcome to Unity Greeter")
337− tryCompare(label, "opacity", 1)
338− tryCompare(label, "clip", true)
339− tryCompareFunction(function() {return label.contentWidth > label.width;}, false) // c.f. wide-info-prompt
340− var passwordInput = findChild(greeter, "passwordInput")
341− mouseClick(passwordInput, 1, 1)
342− keyClick(Qt.Key_Escape)
343− }
344−
345− function test_info_prompt_escape() {
346− select_user("info-prompt")
347− var passwordInput = findChild(greeter, "passwordInput")
348− mouseClick(passwordInput, 1, 1)
349− keyClick(Qt.Key_Escape)
350− var label = findChild(greeter, "infoLabel")
351− tryCompare(label, "text", "Welcome to Unity Greeter")
352− tryCompare(label, "opacity", 1)
353− }
354−
355− function test_wide_info_prompt() {
356− select_user("wide-info-prompt")
357− var label = findChild(greeter, "infoLabel")
358− tryCompare(label, "clip", true)
359− tryCompareFunction(function() {return label.contentWidth > label.width;}, true)
360− }
361−
362− function test_html_info_prompt() {
363− select_user("html-info-prompt")
364− var label = findChild(greeter, "infoLabel")
365− tryCompare(label, "text", "&lt;b&gt;&amp;&lt;/b&gt;")
366− }
367−
368− function test_long_info_prompt() {
369− select_user("long-info-prompt")
370− var label = findChild(greeter, "infoLabel")
371− tryCompare(label, "text", "Welcome to Unity Greeter<br><br>We like to annoy you with super ridiculously long messages.<br>Like this one<br><br>This is the last line of a multiple line message.")
372− tryCompare(label, "textFormat", Text.StyledText) // for parsing above correctly
373− tryCompare(label, "clip", true)
374− tryCompareFunction(function() {return label.contentWidth > label.width;}, true)
375− }
376−
377− function test_multi_info_prompt() {
378− select_user("multi-info-prompt")
379− var label = findChild(greeter, "infoLabel")
380− tryCompare(label, "text", "Welcome to Unity Greeter<br><font color=\"#df382c\">This is an error</font><br>You should have seen three messages")
381− tryCompare(label, "textFormat", Text.StyledText) // for parsing above correctly
382− }
383−
384− function test_bg_color() {
385− var index = select_user("color-background")
386− compare(greeter.model.data(index, LightDM.UserRoles.BackgroundPathRole), "data:image/svg+xml,<svg><rect width='100%' height='100%' fill='#dd4814'/></svg>")
387− }
388−
389− function test_bg_none() {
390− var index = select_user("no-background")
391− compare(greeter.model.data(index, LightDM.UserRoles.BackgroundPathRole), "")
392− }
393−
394− function test_tappedSignal_data() {
395− return [
396− {tag: "left", posX: units.gu(2)},
397− {tag: "right", posX: greeter.width - units.gu(2)}
398− ]
399− }
400−
401− function test_tappedSignal(data) {
402− select_user("no-password");
403− tappedSpy.clear();
404− tap(greeter, data.posX, greeter.height - units.gu(1))
405− tryCompare(tappedSpy, "count", 1)
406− }
407−
408− function test_teaseLockedUnlocked_data() {
409− return [
410− {tag: "unlocked", locked: false, narrow: false},
411− {tag: "locked", locked: true, narrow: false},
412− ];
413− }
414−
415− function test_teaseLockedUnlocked(data) {
416− tappedSpy.clear()
417− greeter.locked = data.locked;
418−
419− tap(greeter, greeter.width - units.gu(5), greeter.height - units.gu(1));
420−
421− if (!data.locked || data.narrow) {
422− tappedSpy.wait()
423− tryCompare(tappedSpy, "count", 1);
424− } else {
425− // waiting 100ms to make sure nothing happens
426− wait(100);
427− compare(tappedSpy.count, 0, "Greeter teasing not disabled even though it's locked.");
428− }
429−
430− // Reset value
431− greeter.locked = false;
432− }
433−
434− function test_dbus_set_active_entry() {
435− select_user("no-password") // to guarantee a selected signal
436− selectionSpy.clear()
437− LightDM.Greeter.requestAuthenticationUser("has-password")
438−
439− selectionSpy.wait()
440− tryCompare(selectionSpy, "count", 1)
441−
442− var userlist = findChild(greeter, "userList")
443− compare(greeter.model.data(userlist.currentIndex, LightDM.UserRoles.NameRole), "has-password")
444− }
445−
446− function test_initial_selected_signal() {
447− var greeterObj = greeterComponent.createObject(this)
448− var spy = findChild(greeterObj, "selectedSpy")
449− spy.wait()
450− tryCompare(spy, "count", 1)
451− greeterObj.destroy()
452− }
453−
454− function test_login_list_not_covered_by_keyboard() {
455− var loginList = findChild(greeter, "loginLoader").item;
456− compare(loginList.height, greeter.height);
457−
458− // when the vkb shows up, loginList is moved up to remain fully uncovered
459−
460− keyboardVisibleCheckbox.checked = true;
461−
462− tryCompare(loginList, "height", greeter.height - fakeInputMethod.keyboardRectangle.height);
463− tryCompareFunction( function() {
464− var loginListRect = loginList.mapToItem(greeter, 0, 0, loginList.width, loginList.height);
465− return loginListRect.y + loginListRect.height <= fakeInputMethod.keyboardRectangle.y;
466− }, true);
467−
468− // once the vkb goes away, loginList goes back to its full height
469−
470− keyboardVisibleCheckbox.checked = false;
471−
472− tryCompare(loginList, "height", greeter.height);
473− }
474− }
475−}
tests/qmltests/Greeter/tst_NarrowView.qmlnew file+522
@@ -0,0 +1,522 @@
1+/*
2+ * Copyright 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 QtTest 1.0
19+import ".."
20+import "../../../qml/Greeter"
21+import LightDM 0.1 as LightDM
22+import Ubuntu.Components 0.1
23+import Unity.Test 0.1 as UT
24+
25+Item {
26+ id: root
27+ width: units.gu(90)
28+ height: units.gu(80)
29+
30+ Row {
31+ anchors.fill: parent
32+ Loader {
33+ id: loader
34+ width: root.width - controls.width
35+ height: parent.height
36+
37+ property bool itemDestroyed: false
38+ sourceComponent: Component {
39+ NarrowView {
40+ background: Qt.resolvedUrl("../../../qml/graphics/phone_background.jpg")
41+ userModel: LightDM.Users
42+ infographicModel: LightDM.Infographic
43+
44+ launcherOffset: parseFloat(launcherOffsetField.text)
45+ currentIndex: parseInt(currentIndexField.text, 10)
46+ delayMinutes: parseInt(delayMinutesField.text, 10)
47+ backgroundTopMargin: parseFloat(backgroundTopMarginField.text)
48+ locked: lockedCheckBox.checked
49+ alphanumeric: alphanumericCheckBox.checked
50+
51+ Component.onDestruction: {
52+ loader.itemDestroyed = true
53+ }
54+
55+ onSelected: {
56+ currentIndexField.text = index;
57+ }
58+ }
59+ }
60+ }
61+
62+ Rectangle {
63+ id: controls
64+ color: "white"
65+ width: units.gu(40)
66+ height: parent.height
67+
68+ Column {
69+ anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
70+ spacing: units.gu(1)
71+
72+ Row {
73+ Button {
74+ text: "Show Last Chance"
75+ onClicked: loader.item.showLastChance()
76+ }
77+ }
78+ Row {
79+ Button {
80+ text: "Hide"
81+ onClicked: loader.item.hide()
82+ }
83+ }
84+ Row {
85+ Button {
86+ text: "Reset"
87+ onClicked: loader.item.reset()
88+ }
89+ }
90+ Row {
91+ Button {
92+ text: "Show Message"
93+ onClicked: loader.item.showMessage(messageField.text)
94+ }
95+ TextField {
96+ id: messageField
97+ width: units.gu(10)
98+ text: ""
99+ }
100+ }
101+ Row {
102+ Button {
103+ text: "Show Prompt"
104+ onClicked: loader.item.showPrompt(promptField.text, isSecretCheckBox.checked, isDefaultPromptCheckBox.checked)
105+ }
106+ TextField {
107+ id: promptField
108+ width: units.gu(10)
109+ text: ""
110+ }
111+ CheckBox {
112+ id: isSecretCheckBox
113+ }
114+ Label {
115+ text: "secret"
116+ }
117+ CheckBox {
118+ id: isDefaultPromptCheckBox
119+ }
120+ Label {
121+ text: "default"
122+ }
123+ }
124+ Row {
125+ Button {
126+ text: "Authenticated"
127+ onClicked: {
128+ if (successCheckBox.checked) {
129+ loader.item.notifyAuthenticationSucceeded();
130+ } else {
131+ loader.item.notifyAuthenticationFailed();
132+ }
133+ }
134+ }
135+ CheckBox {
136+ id: successCheckBox
137+ }
138+ Label {
139+ text: "success"
140+ }
141+ }
142+ Row {
143+ Button {
144+ text: "Try To Unlock"
145+ onClicked: loader.item.tryToUnlock(toTheRightCheckBox.checked)
146+ }
147+ CheckBox {
148+ id: toTheRightCheckBox
149+ }
150+ Label {
151+ text: "toTheRight"
152+ }
153+ }
154+ Row {
155+ TextField {
156+ id: launcherOffsetField
157+ width: units.gu(10)
158+ text: "0"
159+ }
160+ Label {
161+ text: "launcherOffset"
162+ }
163+ }
164+ Row {
165+ TextField {
166+ id: currentIndexField
167+ width: units.gu(10)
168+ text: "0"
169+ }
170+ Label {
171+ text: "currentIndex"
172+ }
173+ }
174+ Row {
175+ TextField {
176+ id: delayMinutesField
177+ width: units.gu(10)
178+ text: "0"
179+ }
180+ Label {
181+ text: "delayMinutes"
182+ }
183+ }
184+ Row {
185+ TextField {
186+ id: backgroundTopMarginField
187+ width: units.gu(10)
188+ text: "0"
189+ }
190+ Label {
191+ text: "backgroundTopMargin"
192+ }
193+ }
194+ Row {
195+ CheckBox {
196+ id: lockedCheckBox
197+ }
198+ Label {
199+ text: "locked"
200+ }
201+ }
202+ Row {
203+ CheckBox {
204+ id: alphanumericCheckBox
205+ }
206+ Label {
207+ text: "alphanumeric"
208+ }
209+ }
210+ Row {
211+ Label {
212+ text: "selected: " + selectedSpy.count
213+ }
214+ }
215+ Row {
216+ Label {
217+ text: "responded: " + respondedSpy.count
218+ }
219+ }
220+ Row {
221+ Label {
222+ text: "teased: " + teaseSpy.count
223+ }
224+ }
225+ Row {
226+ Label {
227+ text: "emergency: " + emergencySpy.count
228+ }
229+ }
230+ Row {
231+ Button {
232+ text: "Reload View"
233+ onClicked: {
234+ loader.active = false;
235+ loader.active = true;
236+ }
237+ }
238+ }
239+ }
240+ }
241+ }
242+
243+ Binding {
244+ target: LightDM.Infographic
245+ property: "username"
246+ value: "single"
247+ }
248+
249+ SignalSpy {
250+ id: selectedSpy
251+ target: loader.item
252+ signalName: "selected"
253+ }
254+
255+ SignalSpy {
256+ id: respondedSpy
257+ target: loader.item
258+ signalName: "responded"
259+ }
260+
261+ SignalSpy {
262+ id: teaseSpy
263+ target: loader.item
264+ signalName: "tease"
265+ }
266+
267+ SignalSpy {
268+ id: emergencySpy
269+ target: loader.item
270+ signalName: "emergencyCall"
271+ }
272+
273+ SignalSpy {
274+ id: infographicDataChangedSpy
275+ target: LightDM.Infographic
276+ signalName: "dataChanged"
277+ }
278+
279+ UT.UnityTestCase {
280+ name: "NarrowView"
281+ when: windowShown
282+
283+ property Item view: loader.status === Loader.Ready ? loader.item : null
284+
285+ function init() {
286+ view.currentIndex = 0; // break binding with text field
287+
288+ selectedSpy.clear();
289+ respondedSpy.clear();
290+ teaseSpy.clear();
291+ emergencySpy.clear();
292+ infographicDataChangedSpy.clear();
293+ }
294+
295+ function cleanup() {
296+ loader.itemDestroyed = false;
297+ loader.active = false;
298+ tryCompare(loader, "status", Loader.Null);
299+ tryCompare(loader, "item", null);
300+ tryCompare(loader, "itemDestroyed", true);
301+ loader.active = true;
302+ tryCompare(loader, "status", Loader.Ready);
303+ removeTimeConstraintsFromDirectionalDragAreas(loader.item);
304+ }
305+
306+ function swipeAwayCover(toTheRight) {
307+ if (toTheRight === undefined) {
308+ toTheRight = false;
309+ }
310+
311+ tryCompare(view, "fullyShown", true);
312+ var touchY = view.height / 2;
313+ if (toTheRight) {
314+ touchFlick(view, 0, touchY, view.width, touchY);
315+ } else {
316+ touchFlick(view, view.width, touchY, 0, touchY);
317+ }
318+ var coverPage = findChild(view, "coverPage");
319+ tryCompare(coverPage, "showProgress", 0);
320+ waitForRendering(view);
321+ }
322+
323+ function enterPin(pin) {
324+ for (var i = 0; i < pin.length; ++i) {
325+ var character = pin.charAt(i);
326+ var button = findChild(view, "pinPadButton" + character);
327+ tap(button);
328+ }
329+ }
330+
331+ function test_tease_data() {
332+ return [
333+ {tag: "left", x: 0, offset: 0, count: 1},
334+ {tag: "leftWithOffsetPass", x: 10, offset: 10, count: 1},
335+ {tag: "leftWithOffsetFail", x: 9, offset: 10, count: 0},
336+ {tag: "right", x: view.width, offset: 0, count: 1},
337+ ]
338+ }
339+ function test_tease(data) {
340+ view.dragHandleLeftMargin = data.offset;
341+ tap(view, data.x, 0);
342+ compare(teaseSpy.count, data.count);
343+ }
344+
345+ function test_respondedWithPin() {
346+ view.locked = true;
347+ swipeAwayCover();
348+ enterPin("1234");
349+ compare(respondedSpy.count, 1);
350+ compare(respondedSpy.signalArguments[0][0], "1234");
351+ }
352+
353+ function test_respondedWithPassphrase() {
354+ view.locked = true;
355+ view.alphanumeric = true;
356+ swipeAwayCover();
357+ typeString("test");
358+ keyClick(Qt.Key_Enter);
359+ compare(respondedSpy.count, 1);
360+ compare(respondedSpy.signalArguments[0][0], "test");
361+ }
362+
363+ function test_respondedWithSwipe_data() {
364+ return [
365+ {tag: "left", toTheRight: false, hiddenX: -view.width},
366+ {tag: "right", toTheRight: true, hiddenX: view.width},
367+ ];
368+ }
369+ function test_respondedWithSwipe(data) {
370+ swipeAwayCover(data.toTheRight);
371+ var coverPage = findChild(view, "coverPage");
372+ compare(coverPage.x, data.hiddenX);
373+ compare(respondedSpy.count, 1);
374+ compare(respondedSpy.signalArguments[0][0], "");
375+ }
376+
377+ function test_emergencyCall() {
378+ view.locked = true;
379+ swipeAwayCover();
380+ var emergencyCallLabel = findChild(view, "emergencyCallLabel");
381+ tap(emergencyCallLabel);
382+ compare(emergencySpy.count, 1);
383+ }
384+
385+ function test_fullyShown() {
386+ tryCompare(view, "fullyShown", true);
387+ swipeAwayCover();
388+ tryCompare(view, "fullyShown", false);
389+ view.locked = true;
390+ tryCompare(view, "fullyShown", true);
391+ view.locked = false;
392+ tryCompare(view, "fullyShown", false);
393+ }
394+
395+ function test_required() {
396+ tryCompare(view, "required", true);
397+ swipeAwayCover();
398+ tryCompare(view, "required", false);
399+ view.locked = true;
400+ tryCompare(view, "required", true);
401+ view.locked = false;
402+ tryCompare(view, "required", false);
403+ }
404+
405+ function test_tryToUnlock() {
406+ var coverPage = findChild(view, "coverPage");
407+ tryCompare(coverPage, "showProgress", 1);
408+ compare(view.tryToUnlock(false), true);
409+ tryCompare(coverPage, "showProgress", 0);
410+ compare(view.tryToUnlock(false), false);
411+ }
412+
413+ /*
414+ Regression test for https://bugs.launchpad.net/ubuntu/+source/unity8/+bug/1388359
415+ "User metrics can no longer be changed by double tap"
416+ */
417+ function test_doubleTapSwitchesToNextInfographic() {
418+ var infographicPrivate = findInvisibleChild(view, "infographicPrivate");
419+ verify(infographicPrivate);
420+
421+ // wait for the UI to settle down before double tapping it
422+ tryCompare(infographicPrivate, "animating", false);
423+
424+ var dataCircle = findChild(view, "dataCircle");
425+ verify(dataCircle);
426+
427+ tap(dataCircle);
428+ wait(1);
429+ tap(dataCircle);
430+
431+ tryCompare(infographicDataChangedSpy, "count", 1);
432+ }
433+
434+ function test_movesBackIntoPlaceWhenNotDraggedFarEnough() {
435+ var coverPage = findChild(view, "coverPage");
436+
437+ var dragEvaluator = findInvisibleChild(coverPage, "edgeDragEvaluator");
438+ verify(dragEvaluator);
439+
440+ // Make it easier to get a rejection/rollback. Otherwise would have to inject
441+ // a fake timer into dragEvaluator.
442+ // Afterall, we are testing if the CoverPage indeed moves back on a
443+ // rollback decision, not the drag evaluation itself.
444+ dragEvaluator.minDragDistance = dragEvaluator.maxDragDistance / 2;
445+
446+ // it starts as fully shown
447+ compare(coverPage.x, 0);
448+
449+ // then we drag it a bit
450+ var startX = coverPage.width - 1;
451+ var touchY = coverPage.height / 2;
452+ var dragXDelta = -(dragEvaluator.minDragDistance * 0.3);
453+ touchFlick(coverPage,
454+ startX , touchY, // start pos
455+ startX + dragXDelta, touchY, // end pos
456+ true /* beginTouch */, false /* endTouch */);
457+
458+ // which should make it move a bit
459+ tryCompareFunction(function() {return coverPage.x < 0;}, true);
460+
461+ // then we release it
462+ touchRelease(coverPage, startX + dragXDelta, touchY);
463+
464+ // which should make it move back into its original position as it didn't move
465+ // far enough to have it hidden
466+ tryCompare(coverPage, "x", 0);
467+ }
468+
469+ function test_dragToHide_data() {
470+ return [
471+ {tag: "left", startX: view.width * 0.95, endX: view.width * 0.1, hiddenX: -view.width},
472+ {tag: "right", startX: view.width * 0.1, endX: view.width * 0.95, hiddenX: view.width},
473+ ];
474+ }
475+ function test_dragToHide(data) {
476+ var coverPage = findChild(view, "coverPage");
477+ compare(coverPage.x, 0);
478+ compare(coverPage.visible, true);
479+ compare(coverPage.shown, true);
480+ compare(coverPage.showProgress, 1);
481+ compare(view.fullyShown, true);
482+
483+ touchFlick(view,
484+ data.startX, view.height / 2, // start pos
485+ data.endX, view.height / 2); // end pos
486+
487+ tryCompare(coverPage, "x", data.hiddenX);
488+ tryCompare(coverPage, "visible", false);
489+ tryCompare(coverPage, "shown", false);
490+ tryCompare(coverPage, "showProgress", 0);
491+ compare(view.fullyShown, false);
492+ }
493+
494+ function test_hiddenViewRemainsHiddenAfterResize_data() {
495+ return [
496+ {tag: "left", startX: view.width * 0.95, endX: view.width * 0.1},
497+ {tag: "right", startX: view.width * 0.1, endX: view.width * 0.95},
498+ ];
499+ }
500+ function test_hiddenViewRemainsHiddenAfterResize(data) {
501+ touchFlick(view,
502+ data.startX, view.height / 2, // start pos
503+ data.endX, view.height / 2); // end pos
504+
505+ var coverPage = findChild(view, "coverPage");
506+ tryCompare(coverPage, "x", data.tag == "left" ? -view.width : view.width);
507+ tryCompare(coverPage, "visible", false);
508+ tryCompare(coverPage, "shown", false);
509+ tryCompare(coverPage, "showProgress", 0);
510+
511+ // flip dimensions to simulate an orientation change
512+ view.width = loader.height;
513+ view.height = loader.width;
514+
515+ // All properties should remain consistent
516+ tryCompare(coverPage, "x", data.tag == "left" ? -view.width : view.width);
517+ tryCompare(coverPage, "visible", false);
518+ tryCompare(coverPage, "shown", false);
519+ tryCompare(coverPage, "showProgress", 0);
520+ }
521+ }
522+}
tests/qmltests/Greeter/tst_SingleGreeter.qmldeleted−244
@@ -1,244 +0,0 @@
1−/*
2− * Copyright 2013 Canonical Ltd.
3− *
4− * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU General Public License as published by
6− * the Free Software Foundation; version 3.
7− *
8− * This program is distributed in the hope that it will be useful,
9− * but WITHOUT ANY WARRANTY; without even the implied warranty of
10− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU General Public License for more details.
12− *
13− * You should have received a copy of the GNU General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− */
16−
17−import QtQuick 2.0
18−import QtTest 1.0
19−import ".."
20−import "../../../qml/Greeter"
21−import AccountsService 0.1
22−import LightDM 0.1 as LightDM
23−import Ubuntu.Components 0.1
24−import Unity.Test 0.1 as UT
25−
26−Rectangle {
27− color: "darkblue"
28− width: units.gu(60)
29− height: units.gu(80)
30−
31− Button {
32− anchors.centerIn: parent
33− text: "Show Greeter"
34− onClicked: {
35− if (greeterLoader.item)
36− greeterLoader.item.show();
37− }
38− }
39−
40− Loader {
41− id: greeterLoader
42− anchors.fill: parent
43−
44− property bool itemDestroyed: false
45−
46− sourceComponent: Component {
47− Greeter {
48− width: greeterLoader.width
49− height: greeterLoader.height
50−
51− Component.onDestruction: {
52− greeterLoader.itemDestroyed = true;
53− }
54− SignalSpy {
55− objectName: "selectedSpy"
56− target: parent
57− signalName: "selected"
58− }
59− }
60− }
61− }
62−
63− SignalSpy {
64− id: unlockSpy
65− target: greeterLoader.item
66− signalName: "unlocked"
67− }
68−
69− SignalSpy {
70− id: tappedSpy
71− target: greeterLoader.item
72− signalName: "tapped"
73− }
74−
75− SignalSpy {
76− id: infographicDataChangedSpy
77− target: LightDM.Infographic
78− signalName: "dataChanged"
79− }
80−
81− UT.UnityTestCase {
82− name: "SingleGreeter"
83− when: windowShown
84−
85− property Greeter greeter: greeterLoader.item
86−
87− function cleanup() {
88− AccountsService.statsWelcomeScreen = true
89−
90− // force a reload so that we get a fresh Greeter for the next test
91− greeterLoader.itemDestroyed = false;
92− greeterLoader.active = false;
93− tryCompare(greeterLoader, "itemDestroyed", true);
94−
95− unlockSpy.clear();
96− tappedSpy.clear();
97−
98− greeterLoader.active = true;
99− tryCompare(greeterLoader, "status", Loader.Ready);
100− removeTimeConstraintsFromDirectionalDragAreas(greeterLoader.item);
101− }
102−
103− function test_properties() {
104− compare(greeter.multiUser, false)
105− compare(greeter.narrowMode, true)
106− }
107−
108− function test_teasingArea_data() {
109− return [
110− {tag: "left", posX: units.gu(2), leftPressed: true, rightPressed: false},
111− {tag: "right", posX: greeter.width - units.gu(2), leftPressed: false, rightPressed: true}
112− ]
113− }
114−
115− function test_teasingArea(data) {
116− tappedSpy.clear()
117− tap(greeter, data.posX, greeter.height - units.gu(1))
118− tappedSpy.wait()
119− tryCompare(tappedSpy, "count", 1)
120− }
121−
122− function test_statsWelcomeScreen() {
123− // Test logic in greeter that turns statsWelcomeScreen setting into infographic changes
124− compare(AccountsService.statsWelcomeScreen, true)
125− tryCompare(LightDM.Infographic, "username", "single")
126− AccountsService.statsWelcomeScreen = false
127− tryCompare(LightDM.Infographic, "username", "")
128− AccountsService.statsWelcomeScreen = true
129− tryCompare(LightDM.Infographic, "username", "single")
130− }
131−
132− function test_initial_selected_signal() {
133− var selectedSpy = findChild(greeter, "selectedSpy");
134− selectedSpy.wait();
135− tryCompare(selectedSpy, "count", 1);
136− }
137−
138− /*
139− Regression test for https://bugs.launchpad.net/ubuntu/+source/unity8/+bug/1388359
140− "User metrics can no longer be changed by double tap"
141− */
142− function test_doubleTapSwitchesToNextInfographic() {
143− infographicDataChangedSpy.clear();
144−
145− var infographicPrivate = findInvisibleChild(greeter, "infographicPrivate");
146− verify(infographicPrivate);
147−
148− // wait for the UI to settle down before double tapping it
149− tryCompare(infographicPrivate, "animating", false);
150−
151− var dataCircle = findChild(greeter, "dataCircle");
152− verify(dataCircle);
153−
154− tap(dataCircle);
155− wait(1);
156− tap(dataCircle);
157−
158− tryCompare(infographicDataChangedSpy, "count", 1);
159− }
160−
161− function test_movesBackIntoPlaceWhenNotDraggedFarEnough() {
162−
163− var dragEvaluator = findInvisibleChild(greeter, "edgeDragEvaluator");
164− verify(dragEvaluator);
165−
166− // Make it easier to get a rejection/rollback. Otherwise would have to inject
167− // a fake timer into dragEvaluator.
168− // Afterall, we are testing if the Greeter indeed moves back on a
169− // rollback decision, not the drag evaluation itself.
170− dragEvaluator.minDragDistance = dragEvaluator.maxDragDistance / 2;
171−
172− // it starts as fully shown
173− compare(greeter.x, 0);
174−
175− // then we drag it a bit
176− var startX = greeter.width - 1;
177− var touchY = greeter.height / 2;
178− var dragXDelta = -(dragEvaluator.minDragDistance * 0.3);
179− touchFlick(greeter,
180− startX , touchY, // start pos
181− startX + dragXDelta, touchY, // end pos
182− true /* beginTouch */, false /* endTouch */);
183−
184− // which should make it move a bit
185− tryCompareFunction(function(){return greeter.x < 0;}, true);
186−
187− // then we release it
188− touchRelease(greeter, startX + dragXDelta, touchY);
189−
190− // which should make it move back into its original position as it didn't move
191− // far enough to have it hidden
192− tryCompare(greeter, "x", 0);
193− }
194−
195− function test_dragToHide_data() {
196− return [
197− {tag: "left", startX: greeter.width * 0.95, endX: greeter.width * 0.1, hiddenX: -greeter.width},
198− {tag: "right", startX: greeter.width * 0.1, endX: greeter.width * 0.95, hiddenX: greeter.width},
199− ];
200− }
201− function test_dragToHide(data) {
202− compare(greeter.x, 0);
203− compare(greeter.visible, true);
204− compare(greeter.shown, true);
205− compare(greeter.showProgress, 1);
206−
207− touchFlick(greeter,
208− data.startX, greeter.height / 2, // start pos
209− data.endX, greeter.height / 2); // end pos
210−
211− tryCompare(greeter, "x", data.hiddenX);
212− tryCompare(greeter, "visible", false);
213− tryCompare(greeter, "shown", false);
214− tryCompare(greeter, "showProgress", 0);
215− }
216−
217− function test_hiddenGreeterRemainsHiddenAfterResize_data() {
218− return [
219− {tag: "left", startX: greeter.width * 0.95, endX: greeter.width * 0.1},
220− {tag: "right", startX: greeter.width * 0.1, endX: greeter.width * 0.95},
221− ];
222− }
223− function test_hiddenGreeterRemainsHiddenAfterResize(data) {
224− touchFlick(greeter,
225− data.startX, greeter.height / 2, // start pos
226− data.endX, greeter.height / 2); // end pos
227−
228− tryCompare(greeter, "x", data.tag == "left" ? -greeter.width : greeter.width);
229− tryCompare(greeter, "visible", false);
230− tryCompare(greeter, "shown", false);
231− tryCompare(greeter, "showProgress", 0);
232−
233− // flip dimensions to simulate an orientation change
234− greeter.width = greeterLoader.height;
235− greeter.height = greeterLoader.width;
236−
237− // All properties should remain consistent
238− tryCompare(greeter, "x", data.tag == "left" ? -greeter.width : greeter.width);
239− tryCompare(greeter, "visible", false);
240− tryCompare(greeter, "shown", false);
241− tryCompare(greeter, "showProgress", 0);
242− }
243− }
244−}
tests/qmltests/Greeter/tst_WideView.qmlnew file+519
@@ -0,0 +1,519 @@
1+/*
2+ * Copyright 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 QtTest 1.0
19+import ".."
20+import "../../../qml/Greeter"
21+import LightDM 0.1 as LightDM
22+import Ubuntu.Components 0.1
23+import Unity.Test 0.1 as UT
24+
25+Item {
26+ id: root
27+ width: units.gu(120)
28+ height: units.gu(80)
29+
30+ Binding {
31+ target: LightDM.Users
32+ property: "mockMode"
33+ value: "full"
34+ }
35+
36+ Row {
37+ anchors.fill: parent
38+ Loader {
39+ id: loader
40+ width: root.width - controls.width
41+ height: parent.height
42+
43+ property bool itemDestroyed: false
44+ sourceComponent: Component {
45+ WideView {
46+ id: view
47+
48+ background: Qt.resolvedUrl("../../../qml/graphics/tablet_background.jpg")
49+ userModel: LightDM.Users
50+ infographicModel: LightDM.Infographic
51+
52+ launcherOffset: parseFloat(launcherOffsetField.text)
53+ currentIndex: parseInt(currentIndexField.text, 10)
54+ delayMinutes: parseInt(delayMinutesField.text, 10)
55+ backgroundTopMargin: parseFloat(backgroundTopMarginField.text)
56+ locked: lockedCheckBox.checked
57+ inputMethod: fakeInputMethod
58+
59+ Component.onDestruction: {
60+ loader.itemDestroyed = true
61+ }
62+
63+ onSelected: {
64+ currentIndexField.text = index;
65+ }
66+
67+ QtObject {
68+ id: fakeInputMethod
69+ property bool visible: fakeKeyboard.visible
70+ property var keyboardRectangle: QtObject {
71+ property real x: fakeKeyboard.x
72+ property real y: fakeKeyboard.y
73+ property real width: fakeKeyboard.width
74+ property real height: fakeKeyboard.height
75+ }
76+ }
77+
78+ Rectangle {
79+ id: fakeKeyboard
80+ color: "green"
81+ opacity: 0.7
82+ anchors.bottom: view.bottom
83+ width: view.width
84+ height: view.height * 0.6
85+ visible: keyboardVisibleCheckBox.checked
86+ Text {
87+ text: "Keyboard Rectangle"
88+ color: "yellow"
89+ font.bold: true
90+ fontSizeMode: Text.Fit
91+ minimumPixelSize: 10; font.pixelSize: 200
92+ verticalAlignment: Text.AlignVCenter
93+ x: (parent.width - width) / 2
94+ y: (parent.height - height) / 2
95+ width: parent.width
96+ height: parent.height
97+ }
98+ }
99+
100+ }
101+ }
102+ }
103+
104+ Rectangle {
105+ id: controls
106+ color: "white"
107+ width: units.gu(40)
108+ height: parent.height
109+
110+ Column {
111+ anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
112+ spacing: units.gu(1)
113+
114+ Row {
115+ Button {
116+ text: "Show Last Chance"
117+ onClicked: loader.item.showLastChance()
118+ }
119+ }
120+ Row {
121+ Button {
122+ text: "Hide"
123+ onClicked: loader.item.hide()
124+ }
125+ }
126+ Row {
127+ Button {
128+ text: "Reset"
129+ onClicked: loader.item.reset()
130+ }
131+ }
132+ Row {
133+ Button {
134+ text: "Show Message"
135+ onClicked: loader.item.showMessage(messageField.text)
136+ }
137+ TextField {
138+ id: messageField
139+ width: units.gu(10)
140+ text: ""
141+ }
142+ }
143+ Row {
144+ Button {
145+ text: "Show Prompt"
146+ onClicked: loader.item.showPrompt(promptField.text, isSecretCheckBox.checked, isDefaultPromptCheckBox.checked)
147+ }
148+ TextField {
149+ id: promptField
150+ width: units.gu(10)
151+ text: ""
152+ }
153+ CheckBox {
154+ id: isSecretCheckBox
155+ }
156+ Label {
157+ text: "secret"
158+ }
159+ CheckBox {
160+ id: isDefaultPromptCheckBox
161+ }
162+ Label {
163+ text: "default"
164+ }
165+ }
166+ Row {
167+ Button {
168+ text: "Authenticated"
169+ onClicked: {
170+ if (successCheckBox.checked) {
171+ loader.item.notifyAuthenticationSucceeded();
172+ } else {
173+ loader.item.notifyAuthenticationFailed();
174+ }
175+ }
176+ }
177+ CheckBox {
178+ id: successCheckBox
179+ }
180+ Label {
181+ text: "success"
182+ }
183+ }
184+ Row {
185+ Button {
186+ text: "Try To Unlock"
187+ onClicked: loader.item.tryToUnlock(toTheRightCheckBox.checked)
188+ }
189+ CheckBox {
190+ id: toTheRightCheckBox
191+ }
192+ Label {
193+ text: "toTheRight"
194+ }
195+ }
196+ Row {
197+ TextField {
198+ id: launcherOffsetField
199+ width: units.gu(10)
200+ text: "0"
201+ }
202+ Label {
203+ text: "launcherOffset"
204+ }
205+ }
206+ Row {
207+ TextField {
208+ id: currentIndexField
209+ width: units.gu(10)
210+ text: "0"
211+ }
212+ Label {
213+ text: "currentIndex"
214+ }
215+ }
216+ Row {
217+ TextField {
218+ id: delayMinutesField
219+ width: units.gu(10)
220+ text: "0"
221+ }
222+ Label {
223+ text: "delayMinutes"
224+ }
225+ }
226+ Row {
227+ TextField {
228+ id: backgroundTopMarginField
229+ width: units.gu(10)
230+ text: "0"
231+ }
232+ Label {
233+ text: "backgroundTopMargin"
234+ }
235+ }
236+ Row {
237+ CheckBox {
238+ id: lockedCheckBox
239+ }
240+ Label {
241+ text: "locked"
242+ }
243+ }
244+ Row {
245+ Label {
246+ text: "selected: " + selectedSpy.count
247+ }
248+ }
249+ Row {
250+ Label {
251+ text: "responded: " + respondedSpy.count
252+ }
253+ }
254+ Row {
255+ Label {
256+ text: "teased: " + teaseSpy.count
257+ }
258+ }
259+ Row {
260+ Label {
261+ text: "emergency: " + emergencySpy.count
262+ }
263+ }
264+ Row {
265+ Button {
266+ text: "Reload View"
267+ onClicked: {
268+ loader.active = false;
269+ loader.active = true;
270+ }
271+ }
272+ }
273+ Row {
274+ CheckBox {
275+ id: keyboardVisibleCheckBox
276+ }
277+ Label {
278+ text: "Keyboard Visible"
279+ }
280+ }
281+ }
282+ }
283+ }
284+
285+ SignalSpy {
286+ id: selectedSpy
287+ target: loader.item
288+ signalName: "selected"
289+ }
290+
291+ SignalSpy {
292+ id: respondedSpy
293+ target: loader.item
294+ signalName: "responded"
295+ }
296+
297+ SignalSpy {
298+ id: teaseSpy
299+ target: loader.item
300+ signalName: "tease"
301+ }
302+
303+ SignalSpy {
304+ id: emergencySpy
305+ target: loader.item
306+ signalName: "emergencyCall"
307+ }
308+
309+ UT.UnityTestCase {
310+ name: "WideView"
311+ when: windowShown
312+
313+ property Item view: loader.status === Loader.Ready ? loader.item : null
314+
315+ function init() {
316+ view.currentIndex = 0; // break binding with text field
317+ selectedSpy.clear();
318+ respondedSpy.clear();
319+ teaseSpy.clear();
320+ emergencySpy.clear();
321+ }
322+
323+ function cleanup() {
324+ keyboardVisibleCheckBox.checked = false;
325+
326+ loader.itemDestroyed = false;
327+ loader.active = false;
328+ tryCompare(loader, "status", Loader.Null);
329+ tryCompare(loader, "item", null);
330+ tryCompare(loader, "itemDestroyed", true);
331+ loader.active = true;
332+ tryCompare(loader, "status", Loader.Ready);
333+ removeTimeConstraintsFromDirectionalDragAreas(loader.item);
334+ }
335+
336+ function getIndexOf(name) {
337+ for (var i = 0; i < LightDM.Users.count; i++) {
338+ if (name === LightDM.Users.data(i, LightDM.UserRoles.NameRole)) {
339+ return i;
340+ }
341+ }
342+ fail("Didn't find name")
343+ return -1;
344+ }
345+
346+ function selectUser(name) {
347+ var i = getIndexOf(name);
348+ view.currentIndex = i;
349+ return i;
350+ }
351+
352+ function swipeAwayCover() {
353+ tryCompare(view, "fullyShown", true);
354+ var touchY = view.height / 2;
355+ touchFlick(view, view.width, touchY, 0, touchY);
356+ var coverPage = findChild(view, "coverPage");
357+ tryCompare(coverPage, "showProgress", 0);
358+ waitForRendering(view);
359+ }
360+
361+ function test_tease_data() {
362+ return [
363+ {tag: "locked", x: 0, offset: 0, count: 0, locked: true},
364+ {tag: "left", x: 0, offset: 0, count: 1, locked: false},
365+ {tag: "leftWithOffsetPass", x: 10, offset: 10, count: 1, locked: false},
366+ {tag: "leftWithOffsetFail", x: 9, offset: 10, count: 0, locked: false},
367+ {tag: "right", x: view.width, offset: 0, count: 1, locked: false},
368+ ]
369+ }
370+ function test_tease(data) {
371+ view.locked = data.locked;
372+ view.dragHandleLeftMargin = data.offset;
373+ tap(view, data.x, 0);
374+ compare(teaseSpy.count, data.count);
375+ }
376+
377+ function test_selected() {
378+ var delegate = findChild(view, "username2");
379+ tap(delegate);
380+ compare(selectedSpy.count, 1);
381+ compare(selectedSpy.signalArguments[0][0], 2);
382+ compare(view.currentIndex, 0); // confirm we didn't change
383+ }
384+
385+ function test_respondedWithPassword() {
386+ view.locked = true;
387+ view.showPrompt("Prompt", true, true);
388+ var passwordInput = findChild(view, "passwordInput");
389+ compare(passwordInput.placeholderText, "Prompt");
390+ compare(passwordInput.echoMode, TextInput.Password);
391+ tap(passwordInput);
392+ typeString("password");
393+ keyClick(Qt.Key_Enter);
394+ compare(respondedSpy.count, 1);
395+ compare(respondedSpy.signalArguments[0][0], "password");
396+ }
397+
398+ function test_respondedWithNonSecret() {
399+ view.locked = true;
400+ view.showPrompt("otp", false, false);
401+ var passwordInput = findChild(view, "passwordInput");
402+ compare(passwordInput.placeholderText, "otp");
403+ compare(passwordInput.echoMode, TextInput.Normal);
404+ tap(passwordInput);
405+ typeString("foo");
406+ keyClick(Qt.Key_Enter);
407+ compare(respondedSpy.count, 1);
408+ compare(respondedSpy.signalArguments[0][0], "foo");
409+ }
410+
411+ function test_respondedWithSwipe() {
412+ swipeAwayCover();
413+ compare(respondedSpy.count, 1);
414+ compare(respondedSpy.signalArguments[0][0], "");
415+ }
416+
417+ function test_fullyShown() {
418+ tryCompare(view, "fullyShown", true);
419+ swipeAwayCover();
420+ tryCompare(view, "fullyShown", false);
421+ }
422+
423+ function test_required() {
424+ tryCompare(view, "required", true);
425+ swipeAwayCover();
426+ tryCompare(view, "required", false);
427+ }
428+
429+ function test_showMessage() {
430+ view.showMessage("Welcome to Unity Greeter");
431+ view.showMessage("<font color=\"#df382c\">This is an error</font>");
432+ view.showMessage("You should have seen three messages and this is a really long message too. wow so long much length");
433+ var infoLabel = findChild(view, "infoLabel");
434+ compare(infoLabel.text, "Welcome to Unity Greeter<br><font color=\"#df382c\">This is an error</font><br>You should have seen three messages and this is a really long message too. wow so long much length");
435+ compare(infoLabel.textFormat, Text.StyledText);
436+ compare(infoLabel.clip, true);
437+ verify(infoLabel.contentWidth > infoLabel.width);
438+ verify(infoLabel.opacity < 1);
439+ tryCompare(infoLabel, "opacity", 1);
440+ }
441+
442+ // Escape is used to reset the authentication, especially if PAM is unresponsive
443+ function test_escape() {
444+ view.currentIndex = 1;
445+ view.locked = true;
446+ view.showPrompt("Prompt", true, true);
447+ var passwordInput = findChild(view, "passwordInput");
448+ tap(passwordInput);
449+ compare(passwordInput.focus, true);
450+ compare(passwordInput.enabled, true);
451+
452+ typeString("password");
453+ keyClick(Qt.Key_Enter);
454+ compare(passwordInput.focus, false);
455+ compare(passwordInput.enabled, false);
456+
457+ compare(selectedSpy.count, 0);
458+ keyClick(Qt.Key_Escape);
459+ compare(selectedSpy.count, 1);
460+ compare(selectedSpy.signalArguments[0][0], 1);
461+
462+ view.reset();
463+ compare(passwordInput.focus, false);
464+ compare(passwordInput.enabled, true);
465+ }
466+
467+ function test_unicode() {
468+ var index = selectUser("unicode");
469+ var label = findChild(view, "username" + index);
470+ tryCompare(label, "text", "가나다라마");
471+ }
472+
473+ function test_longName() {
474+ var index = selectUser("long-name");
475+ var label = findChild(view, "username" + index);
476+ tryCompare(label, "truncated", true);
477+ }
478+
479+ function test_promptless() {
480+ var passwordInput = findChild(view, "passwordInput");
481+
482+ view.locked = true;
483+ compare(passwordInput.placeholderText, "Retry");
484+ tap(passwordInput);
485+ compare(respondedSpy.count, 0);
486+ compare(selectedSpy.count, 1);
487+ compare(selectedSpy.signalArguments[0][0], 0);
488+ selectedSpy.clear();
489+
490+ view.locked = false;
491+ compare(passwordInput.placeholderText, "Tap to unlock");
492+ tap(passwordInput);
493+ compare(selectedSpy.count, 0);
494+ compare(respondedSpy.count, 1);
495+ compare(respondedSpy.signalArguments[0][0], "");
496+ }
497+
498+ function test_loginListNotCoveredByKeyboard() {
499+ var loginList = findChild(view, "loginList");
500+ compare(loginList.height, view.height);
501+
502+ // when the vkb shows up, loginList is moved up to remain fully uncovered
503+
504+ keyboardVisibleCheckBox.checked = true;
505+
506+ tryCompare(loginList, "height", view.height - view.inputMethod.keyboardRectangle.height);
507+ tryCompareFunction( function() {
508+ var loginListRect = loginList.mapToItem(view, 0, 0, loginList.width, loginList.height);
509+ return loginListRect.y + loginListRect.height <= view.inputMethod.keyboardRectangle.y;
510+ }, true);
511+
512+ // once the vkb goes away, loginList goes back to its full height
513+
514+ keyboardVisibleCheckBox.checked = false;
515+
516+ tryCompare(loginList, "height", view.height);
517+ }
518+ }
519+}
tests/qmltests/Tutorial/tst_Tutorial.qml+5−4
@@ -112,9 +112,9 @@ Item {
112112
113113 function init() {
114114 tryCompare(shell, "enabled", true); // enabled by greeter when ready
115− swipeAwayGreeter();
116115 AccountsService.demoEdges = false;
117116 AccountsService.demoEdges = true;
117+ swipeAwayGreeter();
118118 }
119119
120120 function cleanup() {
@@ -151,13 +151,14 @@ Item {
151151 }
152152
153153 function swipeAwayGreeter() {
154− var greeter = findChild(shell, "greeter");
155− tryCompare(greeter, "showProgress", 1);
154+ var coverPage = findChild(shell, "coverPage");
155+ tryCompare(coverPage, "showProgress", 1);
156156
157157 touchFlick(shell, halfWidth, halfHeight, shell.width, halfHeight);
158158
159159 // wait until the animation has finished
160− tryCompare(greeter, "showProgress", 0);
160+ var greeter = findChild(shell, "greeter");
161+ tryCompare(greeter, "required", false);
161162 waitForRendering(greeter);
162163 }
163164
tests/qmltests/tst_Shell.qml+65−43
@@ -19,9 +19,11 @@
1919
2020import QtQuick 2.0
2121import QtTest 1.0
22+import AccountsService 0.1
2223import GSettings 1.0
2324import LightDM 0.1 as LightDM
2425import Ubuntu.Components 1.1
26+import Ubuntu.Components.ListItems 1.0 as ListItem
2527import Ubuntu.Telephony 0.1 as Telephony
2628import Unity.Application 0.1
2729import Unity.Connectivity 0.1
@@ -80,7 +82,7 @@ Item {
8082 }
8183
8284 Rectangle {
83− color: "white"
85+ color: "darkgrey"
8486 width: units.gu(30)
8587 height: shellLoader.height
8688
@@ -98,11 +100,23 @@ Item {
98100
99101 var greeter = testCase.findChild(shellLoader.item, "greeter");
100102 if (!greeter.shown) {
101− greeter.show();
103+ LightDM.Greeter.showGreeter();
102104 }
103105 }
104106 }
105107 }
108+ ListItem.ItemSelector {
109+ anchors { left: parent.left; right: parent.right }
110+ activeFocusOnPress: false
111+ text: "LightDM mock mode"
112+ model: ["single", "single-passphrase", "single-pin"]
113+ onSelectedIndexChanged: {
114+ shellLoader.active = false;
115+ LightDM.Greeter.mockMode = model[selectedIndex];
116+ LightDM.Users.mockMode = model[selectedIndex];
117+ shellLoader.active = true;
118+ }
119+ }
106120 }
107121 }
108122 }
@@ -284,11 +298,13 @@ Item {
284298
285299 function test_leftEdgeDrag_data() {
286300 return [
287− {tag: "without launcher", revealLauncher: false, swipeLength: units.gu(27), appHides: true, focusedApp: "dialer-app", launcherHides: true},
288− {tag: "with launcher", revealLauncher: true, swipeLength: units.gu(27), appHides: true, focusedApp: "dialer-app", launcherHides: true},
289− {tag: "small swipe", revealLauncher: false, swipeLength: units.gu(25), appHides: false, focusedApp: "dialer-app", launcherHides: false},
290− {tag: "long swipe", revealLauncher: false, swipeLength: units.gu(27), appHides: true, focusedApp: "dialer-app", launcherHides: true},
291− {tag: "long swipe", revealLauncher: false, swipeLength: units.gu(27), appHides: true, focusedApp: "unity8-dash", launcherHides: false}
301+ {tag: "without launcher", revealLauncher: false, swipeLength: units.gu(27), appHides: true, focusedApp: "dialer-app", launcherHides: true, greeterShown: false},
302+ {tag: "with launcher", revealLauncher: true, swipeLength: units.gu(27), appHides: true, focusedApp: "dialer-app", launcherHides: true, greeterShown: false},
303+ {tag: "small swipe", revealLauncher: false, swipeLength: units.gu(25), appHides: false, focusedApp: "dialer-app", launcherHides: false, greeterShown: false},
304+ {tag: "long swipe", revealLauncher: false, swipeLength: units.gu(27), appHides: true, focusedApp: "dialer-app", launcherHides: true, greeterShown: false},
305+ {tag: "small swipe with greeter", revealLauncher: false, swipeLength: units.gu(25), appHides: false, focusedApp: "dialer-app", launcherHides: false, greeterShown: true},
306+ {tag: "long swipe with greeter", revealLauncher: false, swipeLength: units.gu(27), appHides: true, focusedApp: "dialer-app", launcherHides: true, greeterShown: true},
307+ {tag: "swipe over dash", revealLauncher: false, swipeLength: units.gu(27), appHides: true, focusedApp: "unity8-dash", launcherHides: false, greeterShown: false},
292308 ];
293309 }
294310
@@ -299,6 +315,12 @@ Item {
299315 ApplicationManager.focusApplication(data.focusedApp)
300316 waitUntilApplicationWindowIsFullyVisible();
301317
318+ var greeter = findChild(shell, "greeter");
319+ if (data.greeterShown) {
320+ LightDM.Greeter.showGreeter();
321+ tryCompare(greeter, "fullyShown", true);
322+ }
323+
302324 if (data.revealLauncher) {
303325 dragLauncherIntoView();
304326 }
@@ -306,8 +328,10 @@ Item {
306328 swipeFromLeftEdge(data.swipeLength);
307329 if (data.appHides) {
308330 waitUntilDashIsFocused();
331+ tryCompare(greeter, "shown", false);
309332 } else {
310333 waitUntilApplicationWindowIsFullyVisible();
334+ compare(greeter.fullyShown, data.greeterShown);
311335 }
312336
313337 var launcher = findChild(shell, "launcherPanel");
@@ -335,20 +359,20 @@ Item {
335359 // Suspend while call is active...
336360 callManager.foregroundCall = phoneCall;
337361 Powerd.status = Powerd.Off;
338− tryCompare(greeter, "showProgress", 0);
362+ tryCompare(greeter, "shown", false);
339363
340364 // Now try again after ending call
341365 callManager.foregroundCall = null;
342366 Powerd.status = Powerd.On;
343367 Powerd.status = Powerd.Off;
344− tryCompare(greeter, "showProgress", 1);
368+ tryCompare(greeter, "fullyShown", true);
345369
346370 tryCompare(ApplicationManager, "suspended", true);
347371 compare(mainApp.state, ApplicationInfoInterface.Suspended);
348372
349373 // And wake up
350374 Powerd.status = Powerd.On;
351− tryCompare(greeter, "showProgress", 1);
375+ tryCompare(greeter, "fullyShown", true);
352376
353377 // Swipe away greeter to focus app
354378 swipeAwayGreeter();
@@ -359,14 +383,14 @@ Item {
359383
360384 function swipeAwayGreeter() {
361385 var greeter = findChild(shell, "greeter");
362− tryCompare(greeter, "showProgress", 1);
386+ tryCompare(greeter, "fullyShown", true);
363387
364388 var touchX = shell.width - (shell.edgeSize / 2);
365389 var touchY = shell.height / 2;
366390 touchFlick(shell, touchX, touchY, shell.width * 0.1, touchY);
367391
368392 // wait until the animation has finished
369− tryCompare(greeter, "showProgress", 0);
393+ tryCompare(greeter, "shown", false);
370394 waitForRendering(greeter);
371395 }
372396
@@ -608,12 +632,12 @@ Item {
608632
609633 waitUntilDashIsFocused();
610634
611− greeter.show();
612− tryCompare(greeter, "showProgress", 1);
635+ LightDM.Greeter.showGreeter();
636+ tryCompare(greeter, "fullyShown", true);
613637
614638 // The main point of this test
615639 ApplicationManager.requestFocusApplication("dialer-app");
616− tryCompare(greeter, "showProgress", 0);
640+ tryCompare(greeter, "shown", false);
617641 waitForRendering(greeter);
618642 }
619643
@@ -631,24 +655,22 @@ Item {
631655
632656 function test_showAndHideGreeterDBusCalls() {
633657 var greeter = findChild(shell, "greeter")
634− tryCompare(greeter, "showProgress", 0)
658+ tryCompare(greeter, "shown", false)
635659 waitForRendering(greeter);
636660 LightDM.Greeter.showGreeter()
637661 waitForRendering(greeter)
638− tryCompare(greeter, "showProgress", 1)
662+ tryCompare(greeter, "fullyShown", true)
639663 LightDM.Greeter.hideGreeter()
640− tryCompare(greeter, "showProgress", 0)
664+ tryCompare(greeter, "shown", false)
641665 }
642666
643667 function test_login() {
644− sessionSpy.clear()
645−
646668 var greeter = findChild(shell, "greeter")
647669 waitForRendering(greeter)
648− greeter.show()
649− tryCompare(greeter, "showProgress", 1)
670+ LightDM.Greeter.showGreeter();
671+ tryCompare(greeter, "fullyShown", true)
650672
651− tryCompare(sessionSpy, "count", 0)
673+ sessionSpy.clear();
652674 swipeAwayGreeter()
653675 tryCompare(sessionSpy, "count", 1)
654676 }
@@ -689,7 +711,8 @@ Item {
689711
690712 function test_unlockedProperties() {
691713 // Confirm that various properties have the correct values when unlocked
692− tryCompare(shell, "locked", false)
714+ var greeter = findChild(shell, "greeter");
715+ tryCompare(greeter, "locked", false);
693716
694717 var launcher = findChild(shell, "launcher")
695718 tryCompare(launcher, "available", true)
@@ -702,25 +725,6 @@ Item {
702725 tryCompare(unlockAllModemsSpy, "count", 1)
703726 }
704727
705− function test_leftEdgeDragOnGreeter_data() {
706− return [
707− {tag: "short swipe", targetX: shell.width / 3, unlocked: false},
708− {tag: "long swipe", targetX: shell.width / 3 * 2, unlocked: true}
709− ]
710− }
711−
712− function test_leftEdgeDragOnGreeter(data) {
713− var greeter = findChild(shell, "greeter");
714− greeter.show();
715− tryCompare(greeter, "showProgress", 1);
716−
717− var touchStartX = 2;
718− var touchStartY = shell.height / 2;
719− touchFlick(shell, touchStartX, touchStartY, data.targetX, touchStartY);
720−
721− tryCompare(greeter, "showProgress", data.unlocked ? 0 : 1);
722− }
723−
724728 function test_tapOnRightEdgeReachesApplicationSurface() {
725729 var topmostSpreadDelegate = findChild(shell, "appDelegate0");
726730 var topmostSurface = findChild(topmostSpreadDelegate, "surfaceContainer").surface;
@@ -830,5 +834,23 @@ Item {
830834 var launcherPanel = findChild(shell, "launcherPanel");
831835 tryCompare(launcherPanel, "x", -launcherPanel.width);
832836 }
837+
838+ function test_background_data() {
839+ return [
840+ {tag: "color", accounts: Qt.resolvedUrl("data:image/svg+xml,<svg><rect width='100%' height='100%' fill='#dd4814'/></svg>"), gsettings: "", output: Qt.resolvedUrl("data:image/svg+xml,<svg><rect width='100%' height='100%' fill='#dd4814'/></svg>")},
841+ {tag: "empty", accounts: "", gsettings: "", output: shell.defaultBackground},
842+ {tag: "as-specified", accounts: Qt.resolvedUrl("../data/unity/backgrounds/blue.png"), gsettings: "", output: Qt.resolvedUrl("../data/unity/backgrounds/blue.png")},
843+ {tag: "gs-specified", accounts: "", gsettings: Qt.resolvedUrl("../data/unity/backgrounds/red.png"), output: Qt.resolvedUrl("../data/unity/backgrounds/red.png")},
844+ {tag: "both-specified", accounts: Qt.resolvedUrl("../data/unity/backgrounds/blue.png"), gsettings: Qt.resolvedUrl("../data/unity/backgrounds/red.png"), output: Qt.resolvedUrl("../data/unity/backgrounds/blue.png")},
845+ {tag: "invalid-as", accounts: Qt.resolvedUrl("../data/unity/backgrounds/nope.png"), gsettings: Qt.resolvedUrl("../data/unity/backgrounds/red.png"), output: Qt.resolvedUrl("../data/unity/backgrounds/red.png")},
846+ {tag: "invalid-both", accounts: Qt.resolvedUrl("../data/unity/backgrounds/nope.png"), gsettings: Qt.resolvedUrl("../data/unity/backgrounds/stillnope.png"), output: shell.defaultBackground},
847+ ]
848+ }
849+ function test_background(data) {
850+ AccountsService.backgroundFile = data.accounts;
851+ var backgroundSettings = findInvisibleChild(shell, "backgroundSettings");
852+ backgroundSettings.pictureUri = data.gsettings;
853+ tryCompare(shell, "background", data.output);
854+ }
833855 }
834856}
tests/qmltests/tst_ShellWithPin.qml+97−48
@@ -74,7 +74,6 @@ Item {
7474 Component.onDestruction: {
7575 shellLoader.itemDestroyed = true
7676 }
77− maxFailedLogins: maxRetriesTextField.text
7877 }
7978 }
8079 }
@@ -90,15 +89,7 @@ Item {
9089 Button {
9190 anchors { left: parent.left; right: parent.right }
9291 text: "Show Greeter"
93− onClicked: {
94− if (shellLoader.status !== Loader.Ready)
95− return
96−
97− var greeter = testCase.findChild(shellLoader.item, "greeter")
98− if (!greeter.shown) {
99− greeter.show()
100− }
101− }
92+ onClicked: LightDM.Greeter.showGreeter()
10293 }
10394
10495 Label {
@@ -108,6 +99,10 @@ Item {
10899 TextField {
109100 id: maxRetriesTextField
110101 text: "-1"
102+ onTextChanged: {
103+ var greeter = testCase.findChild(shellLoader.item, "greeter");
104+ greeter.maxFailedLogins = text;
105+ }
111106 }
112107 }
113108 }
@@ -144,11 +139,12 @@ Item {
144139
145140 function init() {
146141 tryCompare(shell, "enabled", true); // will be enabled when greeter is all ready
147− sessionSpy.target = findChild(shell, "greeter")
148− swipeAwayGreeter()
142+ var greeter = findChild(shell, "greeter");
143+ sessionSpy.target = greeter;
144+ swipeAwayGreeter(true);
149145 waitForLockscreen()
150− shell.failedLoginsDelayAttempts = -1
151− maxRetriesTextField.text = "-1"
146+ greeter.failedLoginsDelayAttempts = -1;
147+ greeter.maxFailedLogins = -1;
152148 }
153149
154150 function cleanup() {
@@ -192,17 +188,21 @@ Item {
192188 compare(ApplicationManager.count, 1)
193189 }
194190
195− function swipeAwayGreeter() {
191+ function swipeAwayGreeter(waitForCoverPage) {
196192 var greeter = findChild(shell, "greeter");
197193 waitForRendering(greeter)
198− tryCompare(greeter, "showProgress", 1);
194+ var coverPage = findChild(shell, "coverPage");
195+ tryCompare(coverPage, "showProgress", 1);
199196
200197 var touchX = shell.width - (shell.edgeSize / 2);
201198 var touchY = shell.height / 2;
202199 touchFlick(shell, touchX, touchY, shell.width * 0.1, touchY);
203200
204− // wait until the animation has finished
205− tryCompare(greeter, "showProgress", 0);
201+ if (waitForCoverPage) {
202+ // wait until the animation has finished
203+ var coverPage = findChild(shell, "coverPage");
204+ tryCompare(coverPage, "showProgress", 0);
205+ }
206206 }
207207
208208 function waitForLockscreen() {
@@ -213,19 +213,17 @@ Item {
213213 }
214214
215215 function enterPin(pin) {
216− var inputField = findChild(shell, "pinentryField")
217216 for (var i = 0; i < pin.length; ++i) {
218217 var character = pin.charAt(i)
219218 var button = findChild(shell, "pinPadButton" + character)
219+ tryCompare(button, "enabled", true);
220220 tap(button)
221221 }
222222 }
223223
224224 function confirmLockedApp(app) {
225225 var greeter = findChild(shell, "greeter")
226− var lockscreen = findChild(shell, "lockscreen")
227226 tryCompare(greeter, "shown", false)
228− tryCompare(lockscreen, "shown", false)
229227 tryCompare(greeter, "hasLockedApp", true)
230228 tryCompare(greeter, "lockedApp", app)
231229 tryCompare(LightDM.Greeter, "active", true)
@@ -264,13 +262,16 @@ Item {
264262 AccountsService.demoEdges = true
265263 tryCompare(lockscreen, "shown", false)
266264
267− swipeAwayGreeter()
265+ var greeter = findChild(shell, "greeter");
266+ swipeAwayGreeter(false);
267+ tryCompare(greeter, "shown", false);
268268 tryCompare(sessionSpy, "count", 1)
269269
270270 // Lockscreen is only hidden by the edge demo, so if we turn that
271271 // off and show greeter again, lockscreen should appear
272272 AccountsService.demoEdges = false
273273 LightDM.Greeter.showGreeter()
274+ lockscreen = findChild(shell, "lockscreen");
274275 tryCompare(lockscreen, "shown", true)
275276 }
276277
@@ -288,18 +289,16 @@ Item {
288289
289290 function test_emergencyCall() {
290291 var greeter = findChild(shell, "greeter")
291− var lockscreen = findChild(shell, "lockscreen")
292− var emergencyButton = findChild(lockscreen, "emergencyCallLabel")
293292 var panel = findChild(shell, "panel")
294293 var indicators = findChild(shell, "indicators")
295294 var launcher = findChild(shell, "launcher")
296295 var stage = findChild(shell, "stage")
297296
298− tap(emergencyButton)
297+ tap(findChild(greeter, "emergencyCallLabel"));
299298
300299 tryCompare(greeter, "lockedApp", "dialer-app")
301300 tryCompare(greeter, "hasLockedApp", true)
302− tryCompare(lockscreen, "shown", false)
301+ tryCompare(greeter, "shown", false);
303302 tryCompare(panel, "fullscreenMode", true)
304303 tryCompare(indicators, "available", false)
305304 tryCompare(launcher, "available", false)
@@ -312,7 +311,7 @@ Item {
312311 tryCompare(greeter, "shown", true)
313312 tryCompare(greeter, "lockedApp", "")
314313 tryCompare(greeter, "hasLockedApp", false)
315− tryCompare(lockscreen, "shown", true)
314+ tryCompare(greeter, "fullyShown", true);
316315 tryCompare(panel, "fullscreenMode", false)
317316 tryCompare(indicators, "available", true)
318317 tryCompare(launcher, "available", true)
@@ -320,23 +319,25 @@ Item {
320319 }
321320
322321 function test_emergencyCallCrash() {
323− var lockscreen = findChild(shell, "lockscreen")
324− var emergencyButton = findChild(lockscreen, "emergencyCallLabel")
322+ var greeter = findChild(shell, "greeter");
323+ var emergencyButton = findChild(greeter, "emergencyCallLabel");
325324 tap(emergencyButton)
326325
327− tryCompare(lockscreen, "shown", false)
326+ tryCompare(greeter, "shown", false);
328327 killApps() // kill dialer-app, as if it crashed
329− tryCompare(lockscreen, "shown", true)
328+ tryCompare(greeter, "shown", true);
329+ tryCompare(findChild(greeter, "lockscreen"), "shown", true);
330+ tryCompare(findChild(greeter, "coverPage"), "shown", false);
330331 }
331332
332333 function test_emergencyCallAppLaunch() {
333− var lockscreen = findChild(shell, "lockscreen")
334− var emergencyButton = findChild(lockscreen, "emergencyCallLabel")
334+ var greeter = findChild(shell, "greeter");
335+ var emergencyButton = findChild(greeter, "emergencyCallLabel");
335336 tap(emergencyButton)
336337
337− tryCompare(lockscreen, "shown", false)
338+ tryCompare(greeter, "shown", false);
338339 ApplicationManager.startApplication("gallery-app", ApplicationManager.NoFlag)
339− tryCompare(lockscreen, "shown", true)
340+ tryCompare(greeter, "shown", true);
340341 }
341342
342343 function test_failedLoginsCount() {
@@ -350,7 +351,8 @@ Item {
350351 }
351352
352353 function test_wrongEntries() {
353− shell.failedLoginsDelayAttempts = 3
354+ var greeter = findChild(shell, "greeter");
355+ greeter.failedLoginsDelayAttempts = 3;
354356
355357 var placeHolder = findChild(shell, "wrongNoticeLabel")
356358 tryCompare(placeHolder, "text", "")
@@ -364,7 +366,7 @@ Item {
364366 var lockscreen = findChild(shell, "lockscreen")
365367 tryCompare(lockscreen, "delayMinutes", 0)
366368 enterPin("1111")
367− tryCompare(lockscreen, "delayMinutes", shell.failedLoginsDelayMinutes)
369+ tryCompare(lockscreen, "delayMinutes", greeter.failedLoginsDelayMinutes);
368370 }
369371
370372 function test_factoryReset() {
@@ -410,7 +412,7 @@ Item {
410412 // And when we kill the app, we go back to locked tablet mode
411413 killApps()
412414 var greeter = findChild(shell, "greeter")
413− tryCompare(greeter, "showProgress", 1)
415+ tryCompare(greeter, "fullyShown", true)
414416 tryCompare(shell, "sideStageEnabled", true)
415417 tryCompare(applicationsDisplayLoader, "tabletMode", true)
416418 }
@@ -453,11 +455,11 @@ Item {
453455 // - Should be back in normal dialer
454456 // (we've had a bug where we locked screen in this case)
455457
456− var lockscreen = findChild(shell, "lockscreen");
458+ var greeter = findChild(shell, "greeter");
457459 var panel = findChild(shell, "panel");
458460
459461 enterPin("1234");
460− tryCompare(lockscreen, "shown", false);
462+ tryCompare(greeter, "shown", false);
461463 tryCompare(LightDM.Greeter, "active", false);
462464
463465 ApplicationManager.startApplication("dialer-app", ApplicationManager.NoFlag);
@@ -472,10 +474,32 @@ Item {
472474 ApplicationManager.requestFocusApplication("dialer-app");
473475
474476 tryCompare(ApplicationManager, "focusedApplicationId", "dialer-app");
475− tryCompare(lockscreen, "shown", false);
477+ tryCompare(greeter, "shown", false);
476478 tryCompare(LightDM.Greeter, "active", false);
477479 }
478480
481+ function test_focusRequestedHidesCoverPage() {
482+ LightDM.Greeter.showGreeter();
483+
484+ var app = ApplicationManager.startApplication("gallery-app");
485+ // wait until the app is fully loaded (ie, real surface replaces splash screen)
486+ tryCompareFunction(function() { return app.session !== null && app.session.surface !== null }, true);
487+
488+ // New app hides coverPage?
489+ var greeter = findChild(shell, "greeter");
490+ var coverPage = testCase.findChild(shell, "coverPage");
491+ tryCompare(coverPage, "showProgress", 0);
492+ tryCompare(greeter, "fullyShown", true);
493+
494+ LightDM.Greeter.showGreeter();
495+ tryCompare(coverPage, "showProgress", 1);
496+
497+ // Make sure focusing same app triggers same behavior
498+ ApplicationManager.requestFocusApplication("gallery-app");
499+ tryCompare(coverPage, "showProgress", 0);
500+ tryCompare(greeter, "fullyShown", true);
501+ }
502+
479503 function test_suspend() {
480504 var greeter = findChild(shell, "greeter");
481505
@@ -487,10 +511,10 @@ Item {
487511
488512 // And wake up
489513 Powerd.status = Powerd.On;
490− tryCompare(greeter, "showProgress", 1);
514+ tryCompare(greeter, "fullyShown", true);
491515
492516 // Swipe away greeter to focus app
493− swipeAwayGreeter();
517+ swipeAwayGreeter(true);
494518
495519 // We have a lockscreen, make sure we're still suspended
496520 tryCompare(ApplicationManager, "suspended", true);
@@ -539,9 +563,9 @@ Item {
539563 only after its idle timeout is triggered.
540564 */
541565 function test_shortLeftEdgeSwipeMakesLauncherStayVisible() {
542− var greeter = testCase.findChild(shell, "greeter")
543− greeter.show();
544− tryCompare(greeter, "showProgress", 1);
566+ LightDM.Greeter.showGreeter();
567+ var coverPage = testCase.findChild(shell, "coverPage");
568+ tryCompare(coverPage, "showProgress", 1);
545569
546570 var launcher = testCase.findChild(shell, "launcher")
547571 {
@@ -562,12 +586,12 @@ Item {
562586 tryCompare(launcherPanel, "x", 0);
563587
564588 // Greeter should be displaced
565− tryCompareFunction(function() { return greeter.mapToItem(shell, 0, 0).x > shell.width*0.2; }, true);
589+ tryCompareFunction(function() { return coverPage.mapToItem(shell, 0, 0).x > shell.width*0.2; }, true);
566590
567591 touchRelease(shell, toX, shell.height * 0.5);
568592
569593 // Upon release the greeter should have slid back into full view
570− tryCompareFunction(function() { return greeter.mapToItem(shell, 0, 0).x === 0; }, true);
594+ tryCompareFunction(function() { return coverPage.mapToItem(shell, 0, 0).x === 0; }, true);
571595
572596 // And the launcher should stay fully shown
573597 for (var i = 0; i < 10; ++i) {
@@ -575,5 +599,30 @@ Item {
575599 compare(launcherPanel.x, 0);
576600 }
577601 }
602+
603+ function test_longLeftEdgeDrags() {
604+ var coverPage = findChild(shell, "coverPage");
605+ var lockscreen = findChild(shell, "lockscreen");
606+ var launcher = findChild(shell, "launcherPanel");
607+ ApplicationManager.startApplication("gallery-app");
608+ tryCompare(ApplicationManager, "focusedApplicationId", "gallery-app");
609+
610+ // Show greeter
611+ LightDM.Greeter.showGreeter();
612+ tryCompare(coverPage, "showProgress", 1);
613+
614+ // Swipe cover page away
615+ touchFlick(shell, 2, shell.height / 2, units.gu(27), shell.height / 2);
616+ tryCompare(launcher, "x", -launcher.width);
617+ tryCompare(coverPage, "showProgress", 0);
618+ compare(lockscreen.shown, true);
619+ compare(ApplicationManager.focusedApplicationId, "gallery-app");
620+
621+ // Now attempt a swipe on lockscreen
622+ touchFlick(shell, 2, shell.height / 2, units.gu(27), shell.height / 2);
623+ tryCompare(launcher, "x", 0);
624+ compare(lockscreen.shown, true);
625+ compare(ApplicationManager.focusedApplicationId, "gallery-app");
626+ }
578627 }
579628}
tests/qmltests/tst_TabletShell.qml+50−60
@@ -28,9 +28,10 @@ import Powerd 0.1
2828
2929import "../../qml"
3030
31−Row {
31+Item {
3232 id: root
33− spacing: 0
33+ width: units.gu(120)
34+ height: units.gu(80)
3435
3536 Component.onCompleted: {
3637 // must set the mock mode before loading the Shell
@@ -55,49 +56,52 @@ Row {
5556 }
5657 }
5758
58− Loader {
59− id: shellLoader
59+ Row {
60+ anchors.fill: parent
61+ Loader {
62+ id: shellLoader
6063
61− active: false
62− width: units.gu(100)
63− height: units.gu(80)
64+ active: false
65+ width: units.gu(100)
66+ height: parent.height
6467
65− property bool itemDestroyed: false
66− sourceComponent: Component {
67− Shell {
68− property string indicatorProfile: "phone"
68+ property bool itemDestroyed: false
69+ sourceComponent: Component {
70+ Shell {
71+ property string indicatorProfile: "phone"
6972
70− Component.onDestruction: {
71− shellLoader.itemDestroyed = true
73+ Component.onDestruction: {
74+ shellLoader.itemDestroyed = true
75+ }
7276 }
7377 }
7478 }
75− }
7679
77− Rectangle {
78− color: "white"
79− width: units.gu(20)
80− height: shellLoader.height
81−
82− Column {
83− anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
84− spacing: units.gu(1)
85− Button {
86− text: "Show Greeter"
87− onClicked: {
88− if (shellLoader.status !== Loader.Ready)
89− return
90−
91− var greeter = testCase.findChild(shellLoader.item, "greeter")
92− if (!greeter.shown) {
93− greeter.show()
80+ Rectangle {
81+ color: "white"
82+ width: units.gu(20)
83+ height: parent.height
84+
85+ Column {
86+ anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
87+ spacing: units.gu(1)
88+ Button {
89+ text: "Show Greeter"
90+ onClicked: {
91+ if (shellLoader.status !== Loader.Ready)
92+ return
93+
94+ var greeter = testCase.findChild(shellLoader.item, "greeter")
95+ if (!greeter.shown) {
96+ greeter.show()
97+ }
9498 }
9599 }
96− }
97− Button {
98− text: "Demo edges"
99− onClicked: {
100− AccountsService.demoEdges = true
100+ Button {
101+ text: "Demo edges"
102+ onClicked: {
103+ AccountsService.demoEdges = true;
104+ }
101105 }
102106 }
103107 }
@@ -133,6 +137,8 @@ Row {
133137
134138 function init() {
135139 tryCompare(shell, "enabled", true); // will be enabled when greeter is all ready
140+ var userList = findChild(shell, "userList");
141+ tryCompare(userList, "movingInternally", false);
136142 sessionSpy.clear()
137143 sessionSpy.target = findChild(shell, "greeter")
138144 dashCommunicatorSpy.target = findInvisibleChild(shell, "dashCommunicator")
@@ -188,8 +194,7 @@ Row {
188194 if (userlist.currentIndex > i) {
189195 next = userlist.currentIndex - 1
190196 }
191− var account = findChild(greeter, "username"+next)
192− mouseClick(account, 1, 1)
197+ tap(findChild(greeter, "username"+next));
193198 tryCompare(userlist, "currentIndex", next)
194199 tryCompare(userlist, "movingInternally", false)
195200 }
@@ -197,13 +202,12 @@ Row {
197202
198203 function selectUser(name) {
199204 // Find index of user with the right name
200− var greeter = findChild(shell, "greeter")
201− for (var i = 0; i < greeter.model.count; i++) {
202− if (greeter.model.data(i, LightDM.UserRoles.NameRole) == name) {
205+ for (var i = 0; i < LightDM.Users.count; i++) {
206+ if (LightDM.Users.data(i, LightDM.UserRoles.NameRole) == name) {
203207 break
204208 }
205209 }
206− if (i == greeter.model.count) {
210+ if (i == LightDM.Users.count) {
207211 fail("Didn't find name")
208212 return -1
209213 }
@@ -213,7 +217,7 @@ Row {
213217
214218 function clickPasswordInput(isButton) {
215219 var greeter = findChild(shell, "greeter")
216− tryCompare(greeter, "showProgress", 1)
220+ tryCompare(greeter, "fullyShown", true);
217221
218222 var passwordMouseArea = findChild(shell, "passwordMouseArea")
219223 tryCompare(passwordMouseArea, "enabled", isButton)
@@ -223,9 +227,9 @@ Row {
223227 }
224228
225229 function confirmLoggedIn(loggedIn) {
226− var greeterWrapper = findChild(shell, "greeterWrapper")
227− tryCompare(greeterWrapper, "showProgress", loggedIn ? 0 : 1)
228− tryCompare(sessionSpy, "count", loggedIn ? 1 : 0)
230+ var greeter = findChild(shell, "greeter");
231+ tryCompare(greeter, "shown", loggedIn ? false : true);
232+ verify(loggedIn ? sessionSpy.count > 0 : sessionSpy.count === 0);
229233 }
230234
231235 function swipeFromLeftEdge(swipeLength) {
@@ -234,20 +238,6 @@ Row {
234238 touchFlick(shell, touchStartX, touchStartY, swipeLength, touchStartY)
235239 }
236240
237− function test_noLockscreen() {
238− selectUser("has-password")
239− var lockscreen = findChild(shell, "lockscreen")
240− tryCompare(lockscreen, "shown", false)
241− }
242−
243− function test_showAndHideGreeterDBusCalls() {
244− var greeter = findChild(shell, "greeter")
245− LightDM.Greeter.hideGreeter()
246− tryCompare(greeter, "showProgress", 0)
247− LightDM.Greeter.showGreeter()
248− tryCompare(greeter, "showProgress", 1)
249− }
250−
251241 function test_login_data() {
252242 return [
253243 {tag: "auth error", user: "auth-error", loggedIn: false, password: ""},