on xenial help sign in
Use a model for PAM prompts, supporting more possible interactions.

- This lets us show more than one message and more than one prompt from PAM. - We now also show error text when a user enters a wrong password. This matches unity7-greeter behavior. - Similarly, when a fingerprint login is attempted, but we are demanding a real login (e.g. the user hasn't logged in before), we now show a message to explain that a bit. - Simplify the Greeter/View interactions a bit. We no longer need to signal quite as much events, since the view reflects the prompts model directly. - Fix keyboard focus highlight handling among prompts. - Update and expand the tests. Approved by: Albert Astals Cid

commit6d9907ef7fdbf470f3331b0feade1656f34089a2 authorMichael Terry <mike@mterry.name> dateTue 24 Jan 2017, 07:40 (10 years ago) parentf94aa2806efe1d
33 files changed+944−624
plugins/LightDM/CMakeLists.txt+1 plugins/LightDM/DBusGreeterList.cpp+3−3 plugins/LightDM/DBusGreeterList.h+1−1 plugins/LightDM/Greeter.cpp+69−22 plugins/LightDM/Greeter.h+6−6 plugins/LightDM/GreeterPrivate.h+5−2 plugins/LightDM/PromptsModel.cpp+97 plugins/LightDM/PromptsModel.h+70 plugins/LightDM/plugin.cpp+9−2 qml/Greeter/Circle.qml+1−1 qml/Greeter/FullLightDMImpl.qml+1 qml/Greeter/Greeter.qml+27−57 qml/Greeter/GreeterPrompt.qml+31−31 qml/Greeter/IntegratedLightDMImpl.qml+1 qml/Greeter/LightDMService.qml+1 qml/Greeter/LoginList.qml+32−106 qml/Greeter/NarrowView.qml+11−21 qml/Greeter/PromptList.qml+148 qml/Greeter/WideView.qml+13−19 tests/mocks/AccountsService/CMakeLists.txt+1 tests/mocks/liblightdm/MockController.cpp−24 tests/mocks/liblightdm/MockController.h−15 tests/mocks/liblightdm/MockGreeter.cpp+53−7 tests/mocks/liblightdm/MockUsersModel.cpp+8−4 tests/plugins/LightDM/IntegratedLightDM/CMakeLists.txt+29 tests/plugins/LightDM/IntegratedLightDM/dbus.cpp+6−11 tests/plugins/LightDM/IntegratedLightDM/promptsmodel.cpp+175 tests/qmltests/Greeter/TestView.qml+6−16 tests/qmltests/Greeter/tst_Greeter.qml+21−101 tests/qmltests/Greeter/tst_NarrowView.qml+11−41 tests/qmltests/Greeter/tst_WideView.qml+96−116 tests/qmltests/tst_Shell.qml+9−8 tests/qmltests/tst_ShellWithPin.qml+2−10
plugins/LightDM/CMakeLists.txt+1
@@ -17,6 +17,7 @@ set(QMLPLUGIN_SRC
1717 DBusGreeterList.cpp
1818 Greeter.cpp
1919 plugin.cpp
20+ PromptsModel.cpp
2021 SessionsModel.cpp
2122 UsersModel.cpp
2223 )
plugins/LightDM/DBusGreeterList.cpp+3−3
@@ -25,7 +25,7 @@ DBusGreeterList::DBusGreeterList(Greeter *greeter, const QString &path)
2525 m_greeter(greeter)
2626{
2727 connect(m_greeter, &Greeter::authenticationUserChanged, this, &DBusGreeterList::authenticationUserChangedHandler);
28− connect(m_greeter, &Greeter::promptlessChanged, this, &DBusGreeterList::promptlessChangedHandler);
28+ connect(m_greeter, &Greeter::isAuthenticatedChanged, this, &DBusGreeterList::authenticatedChangedHandler);
2929}
3030
3131QString DBusGreeterList::GetActiveEntry() const
@@ -40,7 +40,7 @@ void DBusGreeterList::SetActiveEntry(const QString &entry)
4040
4141bool DBusGreeterList::entryIsLocked() const
4242{
43− return !m_greeter->promptless();
43+ return !m_greeter->isAuthenticated();
4444}
4545
4646void DBusGreeterList::authenticationUserChangedHandler()
@@ -49,7 +49,7 @@ void DBusGreeterList::authenticationUserChangedHandler()
4949 Q_EMIT EntrySelected(m_greeter->authenticationUser());
5050}
5151
52−void DBusGreeterList::promptlessChangedHandler()
52+void DBusGreeterList::authenticatedChangedHandler()
5353{
5454 notifyPropertyChanged(QStringLiteral("EntryIsLocked"), entryIsLocked());
5555 Q_EMIT entryIsLockedChanged();
plugins/LightDM/DBusGreeterList.h+1−1
@@ -48,7 +48,7 @@ Q_SIGNALS:
4848
4949private Q_SLOTS:
5050 void authenticationUserChangedHandler();
51− void promptlessChangedHandler();
51+ void authenticatedChangedHandler();
5252
5353private:
5454 Greeter *m_greeter;
plugins/LightDM/Greeter.cpp+69−22
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013-2016 Canonical, Ltd.
2+ * Copyright (C) 2013-2017 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
@@ -25,8 +25,8 @@ static Greeter *singleton = nullptr;
2525GreeterPrivate::GreeterPrivate(Greeter* parent)
2626 : m_greeter(new QLightDM::Greeter(parent)),
2727 m_active(false),
28− wasPrompted(false),
29− promptless(false),
28+ responded(false),
29+ everResponded(false),
3030 q_ptr(parent)
3131{
3232}
@@ -64,6 +64,12 @@ Greeter *Greeter::instance()
6464 return singleton;
6565}
6666
67+PromptsModel *Greeter::promptsModel()
68+{
69+ Q_D(Greeter);
70+ return &d->prompts;
71+}
72+
6773bool Greeter::isActive() const
6874{
6975 Q_D(const Greeter);
@@ -106,12 +112,6 @@ QString Greeter::defaultSessionHint() const
106112 return d->m_greeter->defaultSessionHint();
107113}
108114
109−bool Greeter::promptless() const
110−{
111− Q_D(const Greeter);
112− return d->promptless;
113−}
114−
115115QString Greeter::selectUser() const
116116{
117117 Q_D(const Greeter);
@@ -143,11 +143,14 @@ bool Greeter::hideUsersHint() const
143143void Greeter::authenticate(const QString &username)
144144{
145145 Q_D(Greeter);
146− d->wasPrompted = false;
147− if (d->promptless) {
148− d->promptless = false;
149− Q_EMIT promptlessChanged();
146+ d->prompts.clear();
147+ d->responded = false;
148+ d->everResponded = false;
149+
150+ if (authenticationUser() == username) {
151+ d->prompts = d->leftovers;
150152 }
153+ d->leftovers.clear();
151154
152155 if (username == QStringLiteral("*guest")) {
153156 d->m_greeter->authenticateAsGuest();
@@ -157,6 +160,7 @@ void Greeter::authenticate(const QString &username)
157160 d->m_greeter->authenticate(username);
158161 }
159162
163+ Q_EMIT authenticationStarted();
160164 Q_EMIT isAuthenticatedChanged();
161165 checkAuthenticationUser();
162166}
@@ -164,6 +168,8 @@ void Greeter::authenticate(const QString &username)
164168void Greeter::respond(const QString &response)
165169{
166170 Q_D(Greeter);
171+ d->responded = true;
172+ d->everResponded = true;
167173 d->m_greeter->respond(response);
168174}
169175
@@ -176,14 +182,17 @@ bool Greeter::startSessionSync(const QString &session)
176182void Greeter::showPromptFilter(const QString &text, QLightDM::Greeter::PromptType type)
177183{
178184 Q_D(Greeter);
179− d->wasPrompted = true;
180185
181186 checkAuthenticationUser(); // may have changed in liblightdm
182187
183188 bool isDefaultPrompt = (text == dgettext("Linux-PAM", "Password: "));
189+ bool isSecret = type == QLightDM::Greeter::PromptTypeSecret;
190+
191+ QString trimmedText;
192+ if (!isDefaultPrompt)
193+ trimmedText = text.trimmed();
184194
185195 // Strip prompt of any colons at the end
186− QString trimmedText = text.trimmed();
187196 if (trimmedText.endsWith(':') || trimmedText.endsWith(QStringLiteral(":"))) {
188197 trimmedText.chop(1);
189198 }
@@ -194,23 +203,61 @@ void Greeter::showPromptFilter(const QString &text, QLightDM::Greeter::PromptTyp
194203 trimmedText = gettext("Username");
195204 }
196205
197− Q_EMIT showPrompt(trimmedText, type == QLightDM::Greeter::PromptTypeSecret, isDefaultPrompt);
206+ if (d->responded) {
207+ d->prompts.clear();
208+ d->responded = false;
209+ }
210+
211+ d->prompts.append(trimmedText, isSecret ? PromptsModel::Secret : PromptsModel::Question);
198212}
199213
200214void Greeter::showMessageFilter(const QString &text, QLightDM::Greeter::MessageType type)
201215{
216+ Q_D(Greeter);
217+
202218 checkAuthenticationUser(); // may have changed in liblightdm
203− Q_EMIT showMessage(text, type == QLightDM::Greeter::MessageTypeError);
219+
220+ bool isError = type == QLightDM::Greeter::MessageTypeError;
221+
222+ if (d->responded) {
223+ d->prompts.clear();
224+ d->responded = false;
225+ }
226+ d->prompts.append(text, isError? PromptsModel::Error : PromptsModel::Message);
204227}
205228
206229void Greeter::authenticationCompleteFilter()
207230{
208231 Q_D(Greeter);
209− if (!d->wasPrompted) {
210− d->promptless = true;
211− Q_EMIT promptlessChanged();
212− }
213232
214233 Q_EMIT isAuthenticatedChanged();
215− Q_EMIT authenticationComplete();
234+
235+ bool automatic = !d->everResponded;
236+ bool pamHasLeftoverMessages = !d->prompts.hasPrompt() && d->prompts.rowCount() > 0;
237+
238+ if (!isAuthenticated()) {
239+ if (pamHasLeftoverMessages) {
240+ d->leftovers = d->prompts; // Prefer PAM's messages
241+ } else if (automatic) {
242+ d->leftovers.append(gettext("Failed to authenticate"), PromptsModel::Error);
243+ } else {
244+ d->leftovers.append(gettext("Invalid password, please try again"), PromptsModel::Error);
245+ }
246+ } else if (pamHasLeftoverMessages) {
247+ automatic = true; // treat this successful login as automatic, so user sees message
248+ d->leftovers = d->prompts;
249+ }
250+
251+ if (automatic) {
252+ d->prompts = d->leftovers; // OK, we'll just use these now
253+ d->leftovers.clear();
254+ d->prompts.append(isAuthenticated() ? gettext("Log In") : gettext("Retry"),
255+ PromptsModel::Button);
256+ }
257+
258+ if (isAuthenticated()) {
259+ Q_EMIT loginSuccess(automatic);
260+ } else {
261+ Q_EMIT loginError(automatic);
262+ }
216263}
plugins/LightDM/Greeter.h+6−6
@@ -27,6 +27,7 @@
2727#include <QtCore/QObject>
2828
2929class GreeterPrivate;
30+class PromptsModel;
3031
3132class Greeter : public QObject
3233{
@@ -36,7 +37,6 @@ class Greeter : public QObject
3637 Q_PROPERTY(bool authenticated READ isAuthenticated NOTIFY isAuthenticatedChanged)
3738 Q_PROPERTY(QString authenticationUser READ authenticationUser NOTIFY authenticationUserChanged)
3839 Q_PROPERTY(QString defaultSession READ defaultSessionHint CONSTANT)
39− Q_PROPERTY(bool promptless READ promptless NOTIFY promptlessChanged)
4040 Q_PROPERTY(QString selectUser READ selectUser CONSTANT)
4141
4242public:
@@ -47,12 +47,13 @@ public:
4747 bool isAuthenticated() const;
4848 QString authenticationUser() const;
4949 QString defaultSessionHint() const;
50− bool promptless() const;
5150 QString selectUser() const;
5251 bool hasGuestAccount() const;
5352 bool showManualLoginHint() const;
5453 bool hideUsersHint() const;
5554
55+ PromptsModel *promptsModel();
56+
5657public Q_SLOTS:
5758 void authenticate(const QString &username=QString());
5859 void respond(const QString &response);
@@ -60,15 +61,14 @@ public Q_SLOTS:
6061 void setIsActive(bool isActive);
6162
6263Q_SIGNALS:
63− void showMessage(const QString &text, bool isError);
64− void showPrompt(const QString &text, bool isSecret, bool isDefaultPrompt);
65− void authenticationComplete();
6664 void authenticationUserChanged();
6765 void isActiveChanged();
6866 void isAuthenticatedChanged();
69− void promptlessChanged();
7067 void showGreeter();
7168 void hideGreeter();
69+ void loginError(bool automatic);
70+ void loginSuccess(bool automatic);
71+ void authenticationStarted(); // useful for testing
7272
7373 // This signal is emitted by external agents like indicators, and the UI
7474 // should switch to this user if possible.
plugins/LightDM/GreeterPrivate.h+5−2
@@ -16,6 +16,7 @@
1616
1717#pragma once
1818
19+#include "PromptsModel.h"
1920#include <QObject>
2021
2122namespace QLightDM {
@@ -29,8 +30,10 @@ public:
2930
3031 QLightDM::Greeter *m_greeter;
3132 bool m_active;
32− bool wasPrompted;
33− bool promptless;
33+ PromptsModel prompts;
34+ PromptsModel leftovers; // prompts to show during next auth for same user
35+ bool responded;
36+ bool everResponded;
3437 QString cachedAuthUser;
3538
3639protected:
plugins/LightDM/PromptsModel.cppnew file+97
@@ -0,0 +1,97 @@
1+/*
2+ * Copyright (C) 2017 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+
18+#include "PromptsModel.h"
19+
20+PromptsModel::PromptsModel(QObject* parent)
21+ : QAbstractListModel(parent)
22+{
23+ m_roleNames[TypeRole] = "type";
24+ m_roleNames[TextRole] = "text";
25+}
26+
27+PromptsModel& PromptsModel::operator=(const PromptsModel &other)
28+{
29+ beginResetModel();
30+ m_prompts = other.m_prompts;
31+ endResetModel();
32+ Q_EMIT countChanged();
33+ return *this;
34+}
35+
36+int PromptsModel::rowCount(const QModelIndex &parent) const
37+{
38+ if (parent.isValid())
39+ return 0;
40+
41+ return m_prompts.size();
42+}
43+
44+QVariant PromptsModel::data(const QModelIndex &index, int role) const
45+{
46+ if (!index.isValid() || index.column() > 0 || index.row() >= m_prompts.size())
47+ return QVariant();
48+
49+ switch (role) {
50+ case Qt::DisplayRole: // fallthrough
51+ case TextRole: return m_prompts[index.row()].prompt;
52+ case TypeRole: return m_prompts[index.row()].type;
53+ default: return QVariant();
54+ }
55+}
56+
57+QHash<int, QByteArray> PromptsModel::roleNames() const
58+{
59+ return m_roleNames;
60+}
61+
62+void PromptsModel::prepend(const QString &text, PromptType type)
63+{
64+ beginInsertRows(QModelIndex(), 0, 0);
65+ m_prompts.prepend(PromptInfo{text, type});
66+ endInsertRows();
67+
68+ Q_EMIT countChanged();
69+}
70+
71+void PromptsModel::append(const QString &text, PromptType type)
72+{
73+ beginInsertRows(QModelIndex(), m_prompts.size(), m_prompts.size());
74+ m_prompts.append(PromptInfo{text, type});
75+ endInsertRows();
76+
77+ Q_EMIT countChanged();
78+}
79+
80+void PromptsModel::clear()
81+{
82+ beginResetModel();
83+ m_prompts.clear();
84+ endResetModel();
85+
86+ Q_EMIT countChanged();
87+}
88+
89+bool PromptsModel::hasPrompt() const
90+{
91+ Q_FOREACH(const PromptInfo &info, m_prompts) {
92+ if (info.type == PromptType::Secret || info.type == PromptType::Question) {
93+ return true;
94+ }
95+ }
96+ return false;
97+}
plugins/LightDM/PromptsModel.hnew file+70
@@ -0,0 +1,70 @@
1+/*
2+ * Copyright (C) 2017 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+
18+#pragma once
19+
20+#include <QAbstractListModel>
21+
22+class PromptsModel : public QAbstractListModel
23+{
24+ Q_OBJECT
25+
26+ Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
27+
28+public:
29+ enum PromptsModelRoles {
30+ TypeRole = Qt::UserRole,
31+ TextRole,
32+ };
33+ Q_ENUM(PromptsModelRoles)
34+
35+ enum PromptType {
36+ Message,
37+ Error,
38+ Secret,
39+ Question,
40+ Button,
41+ };
42+ Q_ENUM(PromptType)
43+
44+ explicit PromptsModel(QObject* parent=0);
45+
46+ PromptsModel& operator=(const PromptsModel &other);
47+
48+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
49+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
50+ QHash<int, QByteArray> roleNames() const override;
51+
52+ Q_INVOKABLE void prepend(const QString &text, PromptType type);
53+ Q_INVOKABLE void append(const QString &text, PromptType type);
54+
55+ void clear();
56+
57+ bool hasPrompt() const;
58+
59+Q_SIGNALS:
60+ void countChanged();
61+
62+private:
63+ struct PromptInfo {
64+ QString prompt;
65+ PromptType type;
66+ };
67+
68+ QList<PromptInfo> m_prompts;
69+ QHash<int, QByteArray> m_roleNames;
70+};
plugins/LightDM/plugin.cpp+9−2
@@ -19,6 +19,7 @@
1919#include "DBusGreeter.h"
2020#include "DBusGreeterList.h"
2121#include "Greeter.h"
22+#include "PromptsModel.h"
2223#include "SessionsModel.h"
2324#include "UsersModel.h"
2425#include <libusermetricsoutput/ColorTheme.h>
@@ -43,6 +44,13 @@ static QObject *greeter_provider(QQmlEngine *engine, QJSEngine *scriptEngine)
4344 return greeter;
4445}
4546
47+static QObject *prompts_provider(QQmlEngine *engine, QJSEngine *)
48+{
49+ auto model = Greeter::instance()->promptsModel();
50+ engine->setObjectOwnership(model, QQmlEngine::CppOwnership);
51+ return model;
52+}
53+
4654static QObject *sessions_provider(QQmlEngine *engine, QJSEngine *scriptEngine)
4755{
4856 Q_UNUSED(engine)
@@ -79,8 +87,7 @@ void PLUGIN_CLASSNAME::registerTypes(const char *uri)
7987 #error No library defined in LightDM plugin
8088#endif
8189
82− qRegisterMetaType<QLightDM::Greeter::MessageType>("QLightDM::Greeter::MessageType");
83− qRegisterMetaType<QLightDM::Greeter::PromptType>("QLightDM::Greeter::PromptType");
90+ qmlRegisterSingletonType<PromptsModel>(uri, 0, 1, "Prompts", prompts_provider);
8491
8592 qmlRegisterSingletonType<SessionsModel>(uri, 0, 1, "Sessions", sessions_provider);
8693 qmlRegisterUncreatableType<QLightDM::SessionsModel>(uri, 0, 1, "SessionRoles", QStringLiteral("Type is not instantiable"));
qml/Greeter/Circle.qml+1−1
@@ -27,7 +27,7 @@ Canvas {
2727 onCenterCircleChanged: requestPaint()
2828
2929 onPaint: {
30− if (circleScale <= 0) {
30+ if (circleScale <= 0 || width <= 0 || height <= 0) {
3131 return;
3232 }
3333
qml/Greeter/FullLightDMImpl.qml+1
@@ -22,6 +22,7 @@ Item{
2222
2323 property var greeter: LightDM.Greeter
2424 property var infographic: LightDM.Infographic
25+ property var prompts: LightDM.Prompts
2526 property var sessions: LightDM.Sessions
2627 property var sessionRoles: LightDM.SessionRoles
2728 property var users: LightDM.Users
qml/Greeter/Greeter.qml+27−57
@@ -74,11 +74,11 @@ Showable {
7474 forcedUnlock = false;
7575 if (required) {
7676 if (loader.item) {
77− loader.item.reset(true /* forceShow */);
77+ loader.item.forceShow();
7878 }
7979 // Normally loader.onLoaded will select a user, but if we're
8080 // already shown, do it manually.
81− d.selectUser(d.currentIndex, false);
81+ d.selectUser(d.currentIndex);
8282 }
8383
8484 // Even though we may already be shown, we want to call show() for its
@@ -154,7 +154,7 @@ Showable {
154154 readonly property bool multiUser: LightDMService.users.count > 1
155155 readonly property int selectUserIndex: d.getUserIndex(LightDMService.greeter.selectUser)
156156 property int currentIndex: Math.max(selectUserIndex, 0)
157− property bool waiting
157+ readonly property bool waiting: LightDMService.prompts.count == 0 && !root.forcedUnlock
158158 property bool isLockscreen // true when we are locking an active session, rather than first user login
159159 readonly property bool secureFingerprint: isLockscreen &&
160160 AccountsService.failedFingerprintLogins <
@@ -189,13 +189,9 @@ Showable {
189189 return -1;
190190 }
191191
192− function selectUser(index, reset) {
192+ function selectUser(index) {
193193 if (index < 0 || index >= LightDMService.users.count)
194194 return;
195− d.waiting = true;
196− if (reset) {
197− loader.item.reset(false /* forceShow */);
198− }
199195 currentIndex = index;
200196 var user = LightDMService.users.data(index, LightDMService.userRoles.NameRole);
201197 AccountsService.user = user;
@@ -206,20 +202,17 @@ Showable {
206202 function hideView() {
207203 if (loader.item) {
208204 loader.item.enabled = false; // drop OSK and prevent interaction
209− loader.item.notifyAuthenticationSucceeded(false /* showFakePassword */);
210205 loader.item.hide();
211206 }
212207 }
213208
214209 function login() {
215− d.waiting = true;
216210 if (LightDMService.greeter.startSessionSync(root.sessionToStart())) {
217211 sessionStarted();
218212 hideView();
219213 } else if (loader.item) {
220214 loader.item.notifyAuthenticationFailed();
221215 }
222− d.waiting = false;
223216 }
224217
225218 function startUnlock(toTheRight) {
@@ -239,27 +232,13 @@ Showable {
239232 }
240233 }
241234
242− function showPromptMessage(text, isError) {
243− // inefficient, but we only rarely deal with messages
244− var html = text.replace(/&/g, "&amp;")
245− .replace(/</g, "&lt;")
246− .replace(/>/g, "&gt;")
247− .replace(/\n/g, "<br>");
248− if (isError) {
249− html = "<font color=\"#df382c\">" + html + "</font>";
250− }
251−
252− if (loader.item) {
253− loader.item.showMessage(html);
254− }
255− }
256−
257235 function showFingerprintMessage(msg) {
236+ d.selectUser(d.currentIndex);
237+ LightDMService.prompts.prepend(msg, LightDMService.prompts.Error);
258238 if (loader.item) {
259− loader.item.reset(false /* forceShow */);
260239 loader.item.showErrorMessage(msg);
240+ loader.item.notifyAuthenticationFailed();
261241 }
262− showPromptMessage(msg, true);
263242 }
264243 }
265244
@@ -285,7 +264,6 @@ Showable {
285264
286265 onRequiredChanged: {
287266 if (required) {
288− d.waiting = true;
289267 lockedApp = "";
290268 }
291269 }
@@ -377,14 +355,14 @@ Showable {
377355 onLoaded: {
378356 root.lockedApp = "";
379357 item.forceActiveFocus();
380− d.selectUser(d.currentIndex, true);
358+ d.selectUser(d.currentIndex);
381359 LightDMService.infographic.readyForDataChange();
382360 }
383361
384362 Connections {
385363 target: loader.item
386364 onSelected: {
387− d.selectUser(index, true);
365+ d.selectUser(index);
388366 }
389367 onResponded: {
390368 if (root.locked) {
@@ -481,27 +459,15 @@ Showable {
481459 onShowGreeter: root.forceShow()
482460 onHideGreeter: root.forcedUnlock = true
483461
484− onShowMessage: d.showPromptMessage(text, isError)
485−
486− onShowPrompt: {
487− if (loader.item) {
488− loader.item.showPrompt(text, isSecret, isDefaultPrompt);
462+ onLoginError: {
463+ if (!loader.item) {
464+ return;
489465 }
490466
491− d.waiting = false;
492− }
493−
494− onAuthenticationComplete: {
495− d.waiting = false;
467+ loader.item.notifyAuthenticationFailed();
496468
497− if (LightDMService.greeter.authenticated) {
498− if (!LightDMService.greeter.promptless) {
499− d.login();
500− }
501− } else {
502− if (!LightDMService.greeter.promptless) {
503− AccountsService.failedLogins++;
504− }
469+ if (!automatic) {
470+ AccountsService.failedLogins++;
505471
506472 // Check if we should initiate a factory reset
507473 if (maxFailedLogins >= 2) { // require at least a warning
@@ -519,14 +485,17 @@ Showable {
519485 forcedDelayTimer.forceDelay();
520486 }
521487
522− loader.item.notifyAuthenticationFailed();
523− if (!LightDMService.greeter.promptless) {
524− d.selectUser(d.currentIndex, false);
525− }
488+ d.selectUser(d.currentIndex);
489+ }
490+ }
491+
492+ onLoginSuccess: {
493+ if (!automatic) {
494+ d.login();
526495 }
527496 }
528497
529− onRequestAuthenticationUser: d.selectUser(d.getUserIndex(user), true)
498+ onRequestAuthenticationUser: d.selectUser(d.getUserIndex(user))
530499 }
531500
532501 Connections {
@@ -589,7 +558,9 @@ Showable {
589558 if (!d.secureFingerprint) {
590559 d.startUnlock(false /* toTheRight */); // use normal login instead
591560 }
592− var msg = d.secureFingerprint ? i18n.tr("Try again") : "";
561+ var msg = d.secureFingerprint ? i18n.tr("Try again") :
562+ d.alphanumeric ? i18n.tr("Enter passphrase to unlock") :
563+ i18n.tr("Enter passcode to unlock");
593564 d.showFingerprintMessage(msg);
594565 }
595566
@@ -609,8 +580,7 @@ Showable {
609580 }
610581 console.log("Identified user by fingerprint:", result);
611582 if (loader.item) {
612− loader.item.enabled = false;
613− loader.item.notifyAuthenticationSucceeded(true /* showFakePassword */);
583+ loader.item.showFakePassword();
614584 }
615585 if (root.active)
616586 root.forcedUnlock = true;
qml/Greeter/GreeterPrompt.qml+31−31
@@ -27,31 +27,25 @@ FocusScope {
2727 property bool isAlphanumeric
2828 property string text
2929 property bool isSecret
30+ property bool interactive: true
31+ readonly property alias enteredText: passwordInput.text
3032
3133 signal clicked()
3234 signal canceled()
33− signal responded(string text)
34−
35− function reset() {
36− passwordInput.text = "";
37− fakeLabel.text = "";
38− d.enabled = true;
39− }
35+ signal accepted()
4036
4137 function showFakePassword() {
4238 // Just a silly hack for looking like 4 pin numbers got entered, if
4339 // a fingerprint was used and we happen to be using a pin. This was
4440 // a request from Design.
4541 if (isSecret && isPrompt && !isAlphanumeric) {
46− d.enabled = false;
47− text = "...."; // actual text doesn't matter
42+ passwordInput.text = "...."; // actual text doesn't matter
4843 }
4944 }
5045
5146 StyledItem {
5247 id: d
5348
54− property bool enabled: true
5549 readonly property color textColor: passwordInput.enabled ? theme.palette.normal.raisedText
5650 : theme.palette.disabled.raisedText
5751 readonly property color selectedColor: passwordInput.enabled ? theme.palette.normal.raised
@@ -60,12 +54,6 @@ FocusScope {
6054 : theme.palette.disabled.raisedSecondaryText
6155 readonly property color errorColor: passwordInput.enabled ? theme.palette.normal.negative
6256 : theme.palette.disabled.negative
63−
64− onEnabledChanged: {
65− if (!enabled) {
66− fakeLabel.text = passwordInput.displayText;
67− }
68− }
6957 }
7058
7159 Rectangle {
@@ -86,15 +74,17 @@ FocusScope {
8674 }
8775 }
8876
89− Rectangle {
77+ StyledItem {
9078 id: promptButton
9179 objectName: "promptButton"
9280 anchors.fill: parent
9381 visible: !root.isPrompt
82+ activeFocusOnTab: true
83+
84+ styleName: "FocusShape"
9485
9586 function triggered() {
96− if (d.enabled) {
97− d.enabled = false;
87+ if (root.interactive) {
9888 root.clicked();
9989 }
10090 }
@@ -109,6 +99,7 @@ FocusScope {
10999 }
110100 }
111101
102+ Keys.onSpacePressed: triggered();
112103 Keys.onReturnPressed: triggered();
113104 Keys.onEnterPressed: triggered();
114105 MouseArea {
@@ -129,6 +120,7 @@ FocusScope {
129120 anchors.fill: parent
130121 visible: root.isPrompt
131122 opacity: fakeLabel.visible ? 0 : 1
123+ activeFocusOnTab: true
132124
133125 validator: RegExpValidator {
134126 regExp: root.isAlphanumeric ? /^.*$/ : /^\d{4}$/
@@ -142,15 +134,23 @@ FocusScope {
142134
143135 readonly property real frameSpacing: units.gu(0.5)
144136
145− style: Item {
146− property color color: d.textColor
147− property color selectedTextColor: d.selectedColor
148− property color selectionColor: d.textColor
149− property color borderColor: "transparent"
150− property color backgroundColor: "transparent"
151− property color errorColor: d.errorColor
152− property real frameSpacing: passwordInput.frameSpacing
137+ style: StyledItem {
153138 anchors.fill: parent
139+ styleName: "FocusShape"
140+
141+ // Properties needed by TextField
142+ readonly property color color: d.textColor
143+ readonly property color selectedTextColor: d.selectedColor
144+ readonly property color selectionColor: d.textColor
145+ readonly property color borderColor: "transparent"
146+ readonly property color backgroundColor: "transparent"
147+ readonly property color errorColor: d.errorColor
148+ readonly property real frameSpacing: styledItem.frameSpacing
149+
150+ // Properties needed by FocusShape
151+ readonly property bool enabled: styledItem.enabled
152+ readonly property bool keyNavigationFocus: styledItem.keyNavigationFocus
153+ property bool activeFocusOnTab
154154 }
155155
156156 secondaryItem: [
@@ -178,9 +178,8 @@ FocusScope {
178178 onAccepted: respond()
179179
180180 function respond() {
181− if (d.enabled) {
182− d.enabled = false;
183− root.responded(text);
181+ if (root.interactive) {
182+ root.accepted();
184183 }
185184 }
186185
@@ -224,6 +223,7 @@ FocusScope {
224223 anchors.leftMargin: passwordInput.frameSpacing * 2
225224 anchors.rightMargin: passwordInput.frameSpacing * 2 + capsIcon.visibleWidth
226225 color: d.drawColor
227− visible: root.isPrompt && !d.enabled
226+ text: passwordInput.displayText
227+ visible: root.isPrompt && !root.interactive
228228 }
229229}
qml/Greeter/IntegratedLightDMImpl.qml+1
@@ -22,6 +22,7 @@ Item{
2222
2323 property var greeter: LightDM.Greeter
2424 property var infographic: LightDM.Infographic
25+ property var prompts: LightDM.Prompts
2526 property var sessions: LightDM.Sessions
2627 property var sessionRoles: LightDM.SessionRoles
2728 property var users: LightDM.Users
qml/Greeter/LightDMService.qml+1
@@ -27,6 +27,7 @@ Loader {
2727
2828 property var greeter: d.valid ? loader.item.greeter : null
2929 property var infographic: d.valid ? loader.item.infographic : null
30+ property var prompts: d.valid ? loader.item.prompts : null
3031 property var sessions: d.valid ? loader.item.sessions : null
3132 property var sessionRoles: d.valid ? loader.item.sessionRoles : null
3233 property var users: d.valid ? loader.item.users : null
qml/Greeter/LoginList.qml+32−106
@@ -25,83 +25,36 @@ StyledItem {
2525 focus: true
2626
2727 property alias model: userList.model
28− property bool alphanumeric: true
28+ property alias alphanumeric: promptList.alphanumeric
2929 property int currentIndex
3030 property bool locked
3131 property bool waiting
3232 property alias boxVerticalOffset: highlightItem.y
3333
34− readonly property alias passwordInput: passwordInput
3534 readonly property int numAboveBelow: 4
3635 readonly property int cellHeight: units.gu(5)
37− readonly property int highlightedHeight: units.gu(15)
36+ readonly property int highlightedHeight: highlightItem.height
3837 readonly property int moveDuration: UbuntuAnimation.FastDuration
3938 property string currentSession
4039 readonly property string currentUser: userList.currentItem.username
41− property bool wasPrompted: false
4240
4341 signal responded(string response)
4442 signal selected(int index)
4543 signal sessionChooserButtonClicked()
4644
4745 function tryToUnlock() {
48− if (wasPrompted) {
49− passwordInput.forceActiveFocus();
50− } else {
51− if (root.locked) {
52− root.selected(currentIndex);
53− } else {
54− root.responded("");
55− }
56− }
57− }
58−
59− function showMessage(html) {
60− if (infoLabel.text === "") {
61− infoLabel.text = html;
62− } else {
63− infoLabel.text += "<br>" + html;
64− }
65− }
66−
67− function showPrompt(text, isSecret, isDefaultPrompt) {
68− passwordInput.text = isDefaultPrompt ? alphanumeric ? i18n.tr("Passphrase")
69− : i18n.tr("Passcode")
70− : text;
71− passwordInput.isPrompt = true;
72− passwordInput.isSecret = isSecret;
73− passwordInput.reset();
74− wasPrompted = true;
46+ promptList.forceActiveFocus();
7547 }
7648
7749 function showError() {
7850 wrongPasswordAnimation.start();
79− root.resetAuthentication();
80− }
81−
82− function reset() {
83− root.resetAuthentication();
8451 }
8552
8653 function showFakePassword() {
87− passwordInput.showFakePassword();
54+ promptList.interactive = false;
55+ promptList.showFakePassword();
8856 }
8957
90− QtObject {
91− id: d
92−
93− function checkIfPromptless() {
94− if (!waiting && !wasPrompted) {
95− passwordInput.isPrompt = false;
96− passwordInput.text = root.locked ? i18n.tr("Retry")
97− : i18n.tr("Log In")
98− }
99− }
100− }
101−
102− onWaitingChanged: d.checkIfPromptless()
103− onLockedChanged: d.checkIfPromptless()
104−
10558 theme: ThemeSettings {
10659 name: "Ubuntu.Components.Themes.Ambiance"
10760 }
@@ -133,7 +86,8 @@ StyledItem {
13386 rightMargin: units.gu(2)
13487 }
13588
136− height: root.highlightedHeight
89+ height: Math.max(units.gu(15), promptList.height + units.gu(8))
90+ Behavior on height { NumberAnimation { duration: root.moveDuration; easing.type: Easing.InOutQuad; } }
13791 }
13892
13993 ListView {
@@ -151,14 +105,8 @@ StyledItem {
151105 interactive: count > 1
152106
153107 readonly property bool movingInternally: moveTimer.running || userList.moving
154− onMovingInternallyChanged: {
155− if (!movingInternally) {
156− root.selected(currentIndex);
157− }
158− }
159108
160109 onCurrentIndexChanged: {
161− root.resetAuthentication();
162110 moveTimer.start();
163111 }
164112
@@ -310,64 +258,42 @@ StyledItem {
310258 }
311259 }
312260
313− FadingLabel {
314− id: infoLabel
315− objectName: "infoLabel"
316− anchors {
317− bottom: passwordInput.top
318− left: highlightItem.left
319− topMargin: units.gu(1)
320− bottomMargin: units.gu(1)
321− leftMargin: units.gu(2)
322− rightMargin: units.gu(1)
323− }
324−
325− color: theme.palette.normal.raisedText
326− width: root.width - anchors.leftMargin - anchors.rightMargin
327− fontSize: "small"
328− textFormat: Text.StyledText
329−
330− opacity: (userList.movingInternally || text == "") ? 0 : 1
331− Behavior on opacity {
332− NumberAnimation { duration: 100 }
333− }
334− }
335−
336− GreeterPrompt {
337− id: passwordInput
338− objectName: "passwordInput"
261+ PromptList {
262+ id: promptList
263+ objectName: "promptList"
339264 anchors {
340265 bottom: highlightItem.bottom
341266 horizontalCenter: highlightItem.horizontalCenter
342267 margins: units.gu(2)
343268 }
344269 width: highlightItem.width - anchors.margins * 2
345− opacity: userList.movingInternally ? 0 : 1
346−
347− activeFocusOnTab: true
348− isAlphanumeric: root.alphanumeric
349−
350− onClicked: root.tryToUnlock()
351− onResponded: root.responded(text)
352− onCanceled: root.selected(currentIndex)
353270
354− Behavior on opacity {
355− NumberAnimation { duration: 100 }
271+ onClicked: {
272+ interactive = false;
273+ if (root.locked) {
274+ root.selected(currentIndex);
275+ } else {
276+ root.responded("");
277+ }
278+ }
279+ onResponded: {
280+ interactive = false;
281+ root.responded(text);
282+ }
283+ onCanceled: {
284+ interactive = false;
285+ root.selected(currentIndex);
356286 }
357287
358− WrongPasswordAnimation {
359− id: wrongPasswordAnimation
360− objectName: "wrongPasswordAnimation"
361− target: passwordInput
288+ Connections {
289+ target: LightDMService.prompts
290+ onModelReset: promptList.interactive = true
362291 }
363292 }
364293
365− function resetAuthentication() {
366− if (!userList.currentItem) {
367− return;
368− }
369− infoLabel.text = "";
370− passwordInput.reset();
371− root.wasPrompted = false;
294+ WrongPasswordAnimation {
295+ id: wrongPasswordAnimation
296+ objectName: "wrongPasswordAnimation"
297+ target: promptList
372298 }
373299}
qml/Greeter/NarrowView.qml+11−21
@@ -48,14 +48,6 @@ FocusScope {
4848 signal tease()
4949 signal emergencyCall()
5050
51− function showMessage(html) {
52− loginList.showMessage(html);
53− }
54−
55− function showPrompt(text, isSecret, isDefaultPrompt) {
56− loginList.showPrompt(text, isSecret, isDefaultPrompt);
57− }
58−
5951 function showLastChance() {
6052 /* TODO: when we finish support for resetting device after too many
6153 failed logins, we should re-add this popup.
@@ -75,10 +67,8 @@ FocusScope {
7567 coverPage.hide();
7668 }
7769
78− function notifyAuthenticationSucceeded(showFakePassword) {
79− if (showFakePassword) {
80− loginList.showFakePassword();
81− }
70+ function showFakePassword() {
71+ loginList.showFakePassword();
8272 }
8373
8474 function notifyAuthenticationFailed() {
@@ -89,11 +79,8 @@ FocusScope {
8979 coverPage.showErrorMessage(msg);
9080 }
9181
92− function reset(forceShow) {
93− loginList.reset();
94− if (forceShow) {
95− coverPage.show();
96− }
82+ function forceShow() {
83+ coverPage.show();
9784 }
9885
9986 function tryToUnlock(toTheRight) {
@@ -120,6 +107,7 @@ FocusScope {
120107 objectName: "lockscreen"
121108 anchors.fill: parent
122109 shown: false
110+ opacity: 0
123111
124112 showAnimation: StandardAnimation { property: "opacity"; to: 1 }
125113 hideAnimation: StandardAnimation { property: "opacity"; to: 0 }
@@ -195,10 +183,12 @@ FocusScope {
195183 onClicked: hide()
196184
197185 onShowProgressChanged: {
198− if (showProgress === 1) {
199− loginList.reset();
200− } else if (showProgress === 0) {
201− loginList.tryToUnlock();
186+ if (showProgress === 0) {
187+ if (lockscreen.shown) {
188+ loginList.tryToUnlock();
189+ } else {
190+ root.responded("");
191+ }
202192 }
203193 }
204194
qml/Greeter/PromptList.qmlnew file+148
@@ -0,0 +1,148 @@
1+/*
2+ * Copyright (C) 2017 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import Ubuntu.Components 1.3
19+import "../Components"
20+import "." 0.1
21+
22+FocusScope {
23+ id: root
24+ height: childrenRect.height
25+
26+ property bool alphanumeric: true
27+ property bool interactive: true
28+
29+ signal responded(string text)
30+ signal clicked()
31+ signal canceled()
32+
33+ function showFakePassword() {
34+ for (var i = 0; i < repeater.count; i++) {
35+ var item = repeater.itemAt(i).item;
36+ if (item.isPrompt) {
37+ item.showFakePassword();
38+ }
39+ }
40+ }
41+
42+ QtObject {
43+ id: d
44+
45+ function sendResponse() {
46+ for (var i = 0; i < repeater.count; i++) {
47+ var item = repeater.itemAt(i).item;
48+ if (item.isPrompt) {
49+ root.responded(item.enteredText);
50+ }
51+ }
52+ }
53+ }
54+
55+ Column {
56+ width: parent.width
57+ spacing: units.gu(0.5)
58+
59+ Repeater {
60+ id: repeater
61+ model: LightDMService.prompts
62+
63+ delegate: Loader {
64+ id: loader
65+
66+ readonly property bool isLabel: model.type == LightDMService.prompts.Message ||
67+ model.type == LightDMService.prompts.Error
68+ readonly property var modelData: model
69+
70+ sourceComponent: isLabel ? infoLabel : greeterPrompt
71+
72+ onLoaded: {
73+ for (var i = 0; i < repeater.count; i++) {
74+ var item = repeater.itemAt(i);
75+ if (item && !item.isLabel) {
76+ item.focus = true;
77+ break;
78+ }
79+ }
80+ loader.item.opacity = 1;
81+ }
82+
83+ Binding {
84+ target: loader.item
85+ property: "model"
86+ value: loader.modelData
87+ }
88+ }
89+ }
90+ }
91+
92+ Component {
93+ id: infoLabel
94+
95+ FadingLabel {
96+ objectName: "infoLabel" + model.index
97+ width: root.width
98+
99+ property var model
100+ readonly property bool isPrompt: false
101+
102+ color: model.type === LightDMService.prompts.Message ? theme.palette.normal.raisedText
103+ : theme.palette.normal.negative
104+ fontSize: "small"
105+ textFormat: Text.PlainText
106+ text: model.text
107+
108+ Behavior on opacity { UbuntuNumberAnimation {} }
109+ opacity: 0
110+ }
111+ }
112+
113+ Component {
114+ id: greeterPrompt
115+
116+ GreeterPrompt {
117+ objectName: "greeterPrompt" + model.index
118+ width: root.width
119+
120+ property var model
121+
122+ interactive: root.interactive
123+ isAlphanumeric: model.text !== "" || root.alphanumeric
124+ isPrompt: model.type !== LightDMService.prompts.Button
125+ isSecret: model.type === LightDMService.prompts.Secret
126+ text: model.text ? model.text : (isAlphanumeric ? i18n.tr("Passphrase") : i18n.tr("Passcode"))
127+
128+ onClicked: root.clicked()
129+ onAccepted: {
130+ // If there is another GreeterPrompt, focus it.
131+ for (var i = model.index + 1; i < repeater.count; i++) {
132+ var item = repeater.itemAt(i).item;
133+ if (item.isPrompt) {
134+ item.forceActiveFocus();
135+ return;
136+ }
137+ }
138+
139+ // Nope we're the last one; just send our response.
140+ d.sendResponse();
141+ }
142+ onCanceled: root.canceled()
143+
144+ Behavior on opacity { UbuntuNumberAnimation {} }
145+ opacity: 0
146+ }
147+ }
148+}
qml/Greeter/WideView.qml+13−19
@@ -52,16 +52,8 @@ FocusScope {
5252 loginList.showError();
5353 }
5454
55− function reset(forceShow) {
56− loginList.reset();
57− }
58−
59− function showMessage(html) {
60− loginList.showMessage(html);
61− }
62−
63− function showPrompt(text, isSecret, isDefaultPrompt) {
64− loginList.showPrompt(text, isSecret, isDefaultPrompt);
55+ function forceShow() {
56+ // Nothing to do, we are always fully shown
6557 }
6658
6759 function tryToUnlock(toTheRight) {
@@ -84,10 +76,8 @@ FocusScope {
8476 coverPage.hide();
8577 }
8678
87− function notifyAuthenticationSucceeded(showFakePassword) {
88− if (showFakePassword) {
89− loginList.showFakePassword();
90− }
79+ function showFakePassword() {
80+ loginList.showFakePassword();
9181 }
9282
9383 function showLastChance() {
@@ -140,13 +130,17 @@ FocusScope {
140130
141131 model: root.userModel
142132 onResponded: root.responded(response)
143− onSelected: {
144− root.selected(index)
145− currentSession = LightDMService.users.data(index, LightDMService.userRoles.SessionRole);
146− }
133+ onSelected: root.selected(index)
147134 onSessionChooserButtonClicked: parent.state = "SessionsList"
135+ onCurrentIndexChanged: setCurrentSession()
148136
149137 Keys.forwardTo: [sessionChooserLoader.item]
138+
139+ Component.onCompleted: setCurrentSession()
140+
141+ function setCurrentSession() {
142+ currentSession = LightDMService.users.data(currentIndex, LightDMService.userRoles.SessionRole);
143+ }
150144 }
151145
152146 Loader {
@@ -174,7 +168,7 @@ FocusScope {
174168 onSessionSelected: loginList.currentSession = sessionKey
175169 onShowLoginList: {
176170 coverPage.state = "LoginList"
177− loginList.passwordInput.forceActiveFocus();
171+ loginList.tryToUnlock();
178172 }
179173 ignoreUnknownSignals: true
180174 }
tests/mocks/AccountsService/CMakeLists.txt+1
@@ -7,6 +7,7 @@ include_directories(
77
88add_library(MockAccountsService-qml MODULE
99 ${CMAKE_SOURCE_DIR}/plugins/LightDM/Greeter.cpp
10+ ${CMAKE_SOURCE_DIR}/plugins/LightDM/PromptsModel.cpp
1011 ${CMAKE_SOURCE_DIR}/plugins/LightDM/UsersModel.cpp
1112 ${CMAKE_SOURCE_DIR}/plugins/Utils/unitysortfilterproxymodelqml.cpp
1213 AccountsService.cpp
tests/mocks/liblightdm/MockController.cpp−24
@@ -152,30 +152,6 @@ void MockController::setUserMode(const QString &userMode)
152152 }
153153}
154154
155−QString MockController::sessionName() const
156−{
157− return QString("INVALID - Please read directly from the UserModel");
158−}
159−
160−void MockController::setSessionName(const QString &sessionName)
161−{
162− // Let the user model deal with this
163− Q_EMIT sessionNameChanged(sessionName, m_currentUsername);
164−}
165−
166−QString MockController::currentUsername() const
167−{
168− return m_currentUsername;
169−}
170−
171−void MockController::setCurrentUsername(const QString &username)
172−{
173− if (m_currentUsername != username) {
174− m_currentUsername = username;
175− Q_EMIT currentUsernameChanged();
176− }
177−}
178−
179155QString MockController::sessionMode() const
180156{
181157 return m_sessionMode;
tests/mocks/liblightdm/MockController.h−15
@@ -38,11 +38,6 @@ class Q_DECL_EXPORT MockController : public QObject
3838 // single, none, full
3939 Q_PROPERTY(QString sessionMode READ sessionMode WRITE setSessionMode NOTIFY sessionModeChanged)
4040
41− // This would be best as a Q_INVOKABLE, but using a property allows for
42− // keeping the mock cleaner
43− Q_PROPERTY(QString sessionName READ sessionName WRITE setSessionName NOTIFY sessionNameChanged)
44− Q_PROPERTY(QString currentUsername READ currentUsername WRITE setCurrentUsername NOTIFY currentUsernameChanged)
45−
4641 Q_PROPERTY(int numAvailableSessions READ numFullSessions CONSTANT)
4742 Q_PROPERTY(int numSessions READ numSessions WRITE setNumSessions NOTIFY numSessionsChanged)
4843
@@ -73,12 +68,6 @@ public:
7368 QString sessionMode() const;
7469 void setSessionMode(const QString &sessionMode);
7570
76− QString sessionName() const;
77− void setSessionName(const QString &userIndex);
78−
79− QString currentUsername() const;
80− void setCurrentUsername(const QString &username);
81−
8271 class SessionItem
8372 {
8473 public:
@@ -92,7 +81,6 @@ public:
9281 void setNumSessions(int numSessions);
9382
9483Q_SIGNALS:
95− void currentUsernameChanged();
9684 void selectUserHintChanged();
9785 void selectGuestHintChanged();
9886 void hasGuestAccountHintChanged();
@@ -101,13 +89,10 @@ Q_SIGNALS:
10189 void userModeChanged();
10290 void sessionModeChanged();
10391 void numSessionsChanged();
104− void sessionNameChanged(const QString &sessionName, const QString &username);
10592
10693private:
10794 explicit MockController(QObject* parent=0);
10895
109− QString m_currentUsername;
110− QString m_sessionName;
11196 QString m_selectUserHint;
11297 bool m_selectGuestHint;
11398 bool m_hasGuestAccountHint;
tests/mocks/liblightdm/MockGreeter.cpp+53−7
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2014-2016 Canonical, Ltd.
2+ * Copyright (C) 2014-2017 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
@@ -29,12 +29,18 @@ public:
2929 bool authenticated = false;
3030 QString authenticationUser;
3131 bool twoFactorDone = false;
32+ QTimer authCallbackTimer;
3233};
3334
3435Greeter::Greeter(QObject *parent)
3536 : QObject(parent)
3637 , d_ptr(new GreeterPrivate)
3738{
39+ Q_D(Greeter);
40+
41+ d->authCallbackTimer.setSingleShot(true);
42+ connect(&d->authCallbackTimer, &QTimer::timeout,
43+ this, &QLightDM::Greeter::handleAuthenticate);
3844}
3945
4046Greeter::~Greeter()
@@ -134,13 +140,19 @@ void Greeter::authenticate(const QString &username)
134140 d->authenticated = false;
135141 d->authenticationUser = username;
136142 d->twoFactorDone = false;
137− QTimer::singleShot(0, this, &Greeter::handleAuthenticate);
143+ d->authCallbackTimer.start();
138144}
139145
140146void Greeter::handleAuthenticate()
141147{
142148 Q_D(Greeter);
143149
150+ if (d->authenticated) {
151+ // This can happen from the guest authentication flow
152+ Q_EMIT authenticationComplete();
153+ return;
154+ }
155+
144156 if (d->authenticationUser.isEmpty()) {
145157 Q_EMIT showPrompt("Username: ", Greeter::PromptTypeQuestion);
146158 return;
@@ -165,13 +177,24 @@ void Greeter::handleAuthenticate()
165177 if (d->authenticationUser == "no-password") {
166178 d->authenticated = true;
167179 Q_EMIT authenticationComplete();
168− } else if (d->authenticationUser == "has-pin"){
180+ } else if (d->authenticationUser == "has-pin") {
169181 Q_EMIT showPrompt("Password: ", Greeter::PromptTypeSecret);
170182 } else if (d->authenticationUser == "auth-error") {
171183 d->authenticated = false;
172184 Q_EMIT authenticationComplete();
173185 } else if (d->authenticationUser == "different-prompt") {
174186 Q_EMIT showPrompt("Secret word: ", Greeter::PromptTypeSecret);
187+ } else if (d->authenticationUser == "question-prompt") {
188+ Q_EMIT showPrompt("Favorite Color (blue): ", Greeter::PromptTypeQuestion);
189+ } else if (d->authenticationUser == "two-prompts") {
190+ Q_EMIT showPrompt("Favorite Color (blue):", Greeter::PromptTypeQuestion);
191+ Q_EMIT showPrompt("Password: ", Greeter::PromptTypeSecret);
192+ } else if (d->authenticationUser == "wacky-prompts") {
193+ Q_EMIT showMessage("First message", Greeter::MessageTypeInfo);
194+ Q_EMIT showPrompt("Favorite Color (blue)", Greeter::PromptTypeQuestion);
195+ Q_EMIT showMessage("Second message", Greeter::MessageTypeError);
196+ Q_EMIT showPrompt("Password: ", Greeter::PromptTypeSecret);
197+ Q_EMIT showMessage("Last message", Greeter::MessageTypeInfo);
175198 } else {
176199 Q_EMIT showPrompt("Password: ", Greeter::PromptTypeSecret);
177200 }
@@ -184,7 +207,7 @@ void Greeter::authenticateAsGuest()
184207 d->authenticated = true;
185208 d->authenticationUser = QString(); // this is what the real liblightdm does
186209 d->twoFactorDone = false;
187− QTimer::singleShot(0, this, &Greeter::authenticationComplete);
210+ d->authCallbackTimer.start();
188211}
189212
190213void Greeter::authenticateAutologin()
@@ -197,7 +220,10 @@ void Greeter::authenticateRemote(const QString &session, const QString &username
197220}
198221
199222void Greeter::cancelAuthentication()
200−{}
223+{
224+ Q_D(Greeter);
225+ d->authCallbackTimer.stop();
226+}
201227
202228void Greeter::setLanguage (const QString &language)
203229{
@@ -231,9 +257,13 @@ void Greeter::respond(const QString &response)
231257 return;
232258 }
233259
234− if (d->authenticationUser == "no-response")
260+ if (d->authenticationUser == "no-response") {
261+ return;
262+ } else if (d->authenticationUser == "locked") {
263+ Q_EMIT showMessage("Account is locked", Greeter::MessageTypeError);
264+ sendAuthenticationComplete();
235265 return;
236− else if (d->authenticationUser == "two-factor") {
266+ } else if (d->authenticationUser == "two-factor") {
237267 if (!d->twoFactorDone) {
238268 if (response == "password") {
239269 d->twoFactorDone = true;
@@ -247,13 +277,29 @@ void Greeter::respond(const QString &response)
247277 sendAuthenticationComplete();
248278 }
249279 return;
280+ } else if (d->authenticationUser == "two-prompts" || d->authenticationUser == "wacky-prompts") {
281+ if (!d->twoFactorDone) {
282+ d->authenticated = (response == "blue");
283+ d->twoFactorDone = true;
284+ } else {
285+ d->authenticated = d->authenticated && (response == "password");
286+ sendAuthenticationComplete();
287+ }
288+ return;
250289 }
251290
252291 if (d->authenticationUser == "has-pin") {
253292 d->authenticated = (response == "1234");
293+ } else if (d->authenticationUser == "question-prompt") {
294+ d->authenticated = (response == "blue");
254295 } else {
255296 d->authenticated = (response == "password");
256297 }
298+
299+ if (d->authenticationUser == "info-after-login" && d->authenticated) {
300+ Q_EMIT showMessage("Congratulations on logging in!", Greeter::MessageTypeInfo);
301+ }
302+
257303 sendAuthenticationComplete();
258304}
259305
tests/mocks/liblightdm/MockUsersModel.cpp+8−4
@@ -73,8 +73,6 @@ UsersModel::UsersModel(QObject *parent)
7373 this, &UsersModel::resetEntries);
7474 connect(MockController::instance(), &MockController::userModeChanged,
7575 this, &UsersModel::resetEntries);
76− connect(MockController::instance(), &MockController::sessionNameChanged,
77− this, &UsersModel::setCurrentSessionName);
7876 resetEntries();
7977}
8078
@@ -177,18 +175,24 @@ void UsersModel::resetEntries()
177175 { "has-password", "Has Password", 0, 0, false, false, "ubuntu", 0 },
178176 { "has-pin", "Has PIN", 0, 0, false, false, "ubuntu", 0 },
179177 { "html-info-prompt", "HTML Info Prompt", 0, 0, false, false, "ubuntu", 0 },
178+ { "info-after-login", "Info After Login", 0, 0, false, false, "ubuntu", 0 },
180179 { "info-prompt", "Info Prompt", 0, 0, false, false, "ubuntu", 0 },
180+ { "wacky-prompts", "Interspersed Prompts", 0, 0, false, false, "ubuntu", 0 },
181+ { "invalid-session", "Invalid Session", 0, 0, false, false, "invalid", 0 },
182+ { "locked", "Locked", 0, 0, false, false, "ubuntu", 0 },
181183 { "long-info-prompt", "Long Info Prompt", 0, 0, false, false, "ubuntu", 0 },
182184 { "long-name", "Long name (far far too long to fit, seriously this would never fit on the screen, you will never see this part of the name)", 0, 0, false, false, "ubuntu", 0 },
183185 { "multi-info-prompt", "Multi Info Prompt", 0, 0, false, false, "ubuntu", 0 },
184186 { "no-background", "No Background", "", 0, false, false, "ubuntu", 0 },
185187 { "no-password", "No Password", 0, 0, false, false, "ubuntu", 0 },
186188 { "no-response", "No Response", 0, 0, false, false, "ubuntu", 0 },
189+ { "question-prompt", "Question Prompt", 0, 0, false, false, "ubuntu", 0 },
187190 { "two-factor", "Two Factor", 0, 0, false, false, "ubuntu", 0 },
188− { "unicode", "가나다라마", 0, 0, false, false, "ubuntu", 0 },
191+ { "two-prompts", "Two Prompts", 0, 0, false, false, "ubuntu", 0 },
189192 // white and black are a bit redundant, but useful for manually testing if UI is still readable
190193 { "white-background", "White Background", "#ffffff", 0, false, false, "ubuntu", 0 },
191− { "wide-info-prompt", "Wide Info Prompt", 0, 0, false, false, "ubuntu", 0 }
194+ { "wide-info-prompt", "Wide Info Prompt", 0, 0, false, false, "ubuntu", 0 },
195+ { "unicode", "가나다라마", 0, 0, false, false, "ubuntu", 0 }
192196 };
193197 }
194198
tests/plugins/LightDM/IntegratedLightDM/CMakeLists.txt+29
@@ -7,6 +7,7 @@ include_directories(
77add_executable(GreeterDBusTestExec
88 dbus.cpp
99 ${CMAKE_SOURCE_DIR}/plugins/LightDM/Greeter.cpp
10+ ${CMAKE_SOURCE_DIR}/plugins/LightDM/PromptsModel.cpp
1011 )
1112# This add_dependencies is needed since we're linking with with -L and -l below
1213# make seems to work but ninja breaks without it
@@ -76,6 +77,7 @@ add_unity8_uitest(GreeterSessions GreeterSessionsModelTestExec
7677add_executable(GreeterUsersModelTestExec
7778 usersmodel.cpp
7879 ${CMAKE_SOURCE_DIR}/plugins/LightDM/Greeter.cpp
80+ ${CMAKE_SOURCE_DIR}/plugins/LightDM/PromptsModel.cpp
7981 ${CMAKE_SOURCE_DIR}/plugins/LightDM/UsersModel.cpp
8082 ${CMAKE_SOURCE_DIR}/plugins/Utils/unitysortfilterproxymodelqml.cpp
8183 )
@@ -99,6 +101,33 @@ add_unity8_uitest(GreeterUsers GreeterUsersModelTestExec
99101 DEPENDS MockLightDM
100102 )
101103
104+# PromptsModelTest
105+add_executable(GreeterPromptsModelTestExec
106+ promptsmodel.cpp
107+ ${CMAKE_SOURCE_DIR}/plugins/LightDM/Greeter.cpp
108+ ${CMAKE_SOURCE_DIR}/plugins/LightDM/PromptsModel.cpp
109+ ${CMAKE_SOURCE_DIR}/plugins/Utils/unitysortfilterproxymodelqml.cpp
110+ )
111+# This add_dependencies is needed since we're linking with with -L and -l below
112+# make seems to work but ninja breaks without it
113+add_dependencies(GreeterPromptsModelTestExec MockLightDM)
114+qt5_use_modules(GreeterPromptsModelTestExec Core Test)
115+target_link_libraries(GreeterPromptsModelTestExec
116+ MockLightDM
117+ )
118+target_include_directories(GreeterPromptsModelTestExec PUBLIC
119+ ${CMAKE_SOURCE_DIR}/plugins/LightDM
120+ ${CMAKE_SOURCE_DIR}/plugins/Utils
121+ ${CMAKE_SOURCE_DIR}/tests/mocks/liblightdm
122+ )
123+install(TARGETS GreeterPromptsModelTestExec
124+ DESTINATION "${SHELL_PRIVATE_LIBDIR}/tests/plugins/LightDM/IntegratedLightDM"
125+ )
126+add_unity8_uitest(GreeterPrompts GreeterPromptsModelTestExec
127+ DEPENDS MockLightDM
128+ )
129+
130+# IntegratedTest
102131add_executable(GreeterIntegratedTestExec
103132 integrated.cpp
104133 )
tests/plugins/LightDM/IntegratedLightDM/dbus.cpp+6−11
@@ -138,10 +138,10 @@ private Q_SLOTS:
138138 {
139139 QSignalSpy spy(this, &GreeterDBusTest::PropertiesChangedRelay);
140140 greeter->authenticate("has-password");
141− spy.wait();
142141
143− QVERIFY(spy.count() > 0);
144− QList<QVariant> arguments = spy.takeFirst();
142+ QTRY_COMPARE(spy.count(), 2); // EntryIsLocked then ActiveEntry
143+
144+ QList<QVariant> arguments = spy[1];
145145 QVERIFY(arguments.at(0).toString() == "com.canonical.UnityGreeter.List");
146146 QVERIFY(arguments.at(1).toMap().contains("ActiveEntry"));
147147 QVERIFY(arguments.at(1).toMap()["ActiveEntry"] == "has-password");
@@ -163,15 +163,10 @@ private Q_SLOTS:
163163 QSignalSpy spy(this, &GreeterDBusTest::PropertiesChangedRelay);
164164 greeter->authenticate("no-password");
165165
166− // Two property changed signals will be emitted, one for the IsLocked
167− // property, one for the ActiveEntry; the first will be IsLocked.
168− spy.wait();
169− if (spy.count() < 2) {
170− spy.wait();
171− }
172− QCOMPARE(spy.count(), 2);
166+ // EntryIsLocked=true, ActiveEntry, then EntryIsLocked=false
167+ QTRY_COMPARE(spy.count(), 3);
173168
174− QList<QVariant> arguments = spy.takeLast();
169+ QList<QVariant> arguments = spy[2];
175170 QVERIFY(arguments.at(0).toString() == "com.canonical.UnityGreeter.List");
176171 QVERIFY(arguments.at(1).toMap().contains("EntryIsLocked"));
177172 QVERIFY(arguments.at(1).toMap()["EntryIsLocked"] == false);
tests/plugins/LightDM/IntegratedLightDM/promptsmodel.cppnew file+175
@@ -0,0 +1,175 @@
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 "Greeter.h"
18+#include "PromptsModel.h"
19+
20+#include <QtTest>
21+
22+class GreeterPromptsModelTest : public QObject
23+{
24+ Q_OBJECT
25+
26+private:
27+ class PromptInfo {
28+ public:
29+ PromptInfo(PromptsModel::PromptType t, const QString &p)
30+ : type(t), text(p) {}
31+ PromptsModel::PromptType type;
32+ QString text;
33+ };
34+
35+ void selectUser(const QString &user)
36+ {
37+ Greeter::instance()->authenticate(user);
38+ QCOMPARE(Greeter::instance()->authenticationUser(), user);
39+ }
40+
41+ void respond(const QString &response, bool authenticated)
42+ {
43+ Greeter::instance()->respond(response);
44+ QCOMPARE(Greeter::instance()->isAuthenticated(), authenticated);
45+ }
46+
47+ void comparePrompts(const QList<PromptInfo> &expected)
48+ {
49+ QTRY_COMPARE(prompts->rowCount(), expected.size());
50+ for (int i = 0; i < prompts->rowCount(); i++) {
51+ QCOMPARE(prompts->data(prompts->index(i, 0), PromptsModel::TypeRole).toInt(), (int)expected[i].type);
52+ QCOMPARE(prompts->data(prompts->index(i, 0), PromptsModel::TextRole).toString(), expected[i].text);
53+ }
54+ }
55+
56+private Q_SLOTS:
57+
58+ void initTestCase()
59+ {
60+ prompts = Greeter::instance()->promptsModel();
61+ QVERIFY(prompts);
62+ }
63+
64+ void init()
65+ {
66+ QCOMPARE(prompts->rowCount(), 0);
67+ }
68+
69+ void cleanup()
70+ {
71+ prompts->clear();
72+ }
73+
74+ void testSimpleFailure()
75+ {
76+ selectUser("has-password");
77+ comparePrompts({{PromptsModel::Secret, ""}});
78+ respond("nope", false);
79+ comparePrompts({{PromptsModel::Secret, ""}}); // prompts don't change immediately
80+ selectUser("has-password");
81+ comparePrompts({{PromptsModel::Error, "Invalid password, please try again"},
82+ {PromptsModel::Secret, ""}});
83+ }
84+
85+ void testSimpleSuccess()
86+ {
87+ selectUser("has-password");
88+ comparePrompts({{PromptsModel::Secret, ""}});
89+ respond("password", true);
90+ comparePrompts({{PromptsModel::Secret, ""}}); // prompts don't change
91+ }
92+
93+ void testHasPassword()
94+ {
95+ selectUser("has-password");
96+ comparePrompts({{PromptsModel::Secret, ""}});
97+ }
98+
99+ void testDifferentPrompt()
100+ {
101+ selectUser("different-prompt");
102+ comparePrompts({{PromptsModel::Secret, "Secret word"}});
103+ }
104+
105+ void testNoPassword()
106+ {
107+ selectUser("no-password");
108+ comparePrompts({{PromptsModel::Button, "Log In"}});
109+ }
110+
111+ void testAuthError()
112+ {
113+ selectUser("auth-error");
114+ comparePrompts({{PromptsModel::Error, "Failed to authenticate"},
115+ {PromptsModel::Button, "Retry"}});
116+ }
117+
118+ void testTwoFactor()
119+ {
120+ selectUser("two-factor");
121+ comparePrompts({{PromptsModel::Secret, ""}});
122+ respond("password", false);
123+ comparePrompts({{PromptsModel::Question, "otp"}});
124+ }
125+
126+ void testTwoPrompts()
127+ {
128+ selectUser("two-prompts");
129+ comparePrompts({{PromptsModel::Question, "Favorite Color (blue)"},
130+ {PromptsModel::Secret, ""}});
131+ }
132+
133+ void testWackyPrompts()
134+ {
135+ selectUser("wacky-prompts");
136+ comparePrompts({{PromptsModel::Message, "First message"},
137+ {PromptsModel::Question, "Favorite Color (blue)"},
138+ {PromptsModel::Error, "Second message"},
139+ {PromptsModel::Secret, ""},
140+ {PromptsModel::Message, "Last message"}});
141+ }
142+
143+ void testHtmlInfoPrompts()
144+ {
145+ selectUser("html-info-prompt");
146+ comparePrompts({{PromptsModel::Message, "<b>&</b>"},
147+ {PromptsModel::Secret, ""}});
148+ }
149+
150+ void testInfoAfterLogin()
151+ {
152+ selectUser("info-after-login");
153+ comparePrompts({{PromptsModel::Secret, ""}});
154+ respond("password", true);
155+ comparePrompts({{PromptsModel::Message, "Congratulations on logging in!"},
156+ {PromptsModel::Button, "Log In"}});
157+ }
158+
159+ void testLocked()
160+ {
161+ selectUser("locked");
162+ comparePrompts({{PromptsModel::Secret, ""}});
163+ respond("nope", false);
164+ selectUser("locked");
165+ comparePrompts({{PromptsModel::Error, "Account is locked"},
166+ {PromptsModel::Secret, ""}});
167+ }
168+
169+private:
170+ PromptsModel *prompts;
171+};
172+
173+QTEST_GUILESS_MAIN(GreeterPromptsModelTest)
174+
175+#include "promptsmodel.moc"
tests/qmltests/Greeter/TestView.qml+6−16
@@ -46,24 +46,14 @@ Item {
4646 signal tease()
4747 signal emergencyCall()
4848
49− signal _showMessageCalled(string html)
50− signal _showPromptCalled(string text, bool isSecret, bool isDefaultPrompt)
5149 signal _showLastChanceCalled()
5250 signal _hideCalled()
53− signal _notifyAuthenticationSucceededCalled(bool showFakePassword)
51+ signal _showFakePasswordCalled()
5452 signal _notifyAuthenticationFailedCalled()
5553 signal _showErrorMessageCalled(string msg)
56− signal _resetCalled(bool forceShow)
54+ signal _forceShowCalled()
5755 signal _tryToUnlockCalled(bool toTheRight)
5856
59− function showMessage(html) {
60− _showMessageCalled(html);
61− }
62−
63− function showPrompt(text, isSecret, isDefaultPrompt) {
64− _showPromptCalled(text, isSecret, isDefaultPrompt);
65− }
66−
6757 function showLastChance() {
6858 _showLastChanceCalled();
6959 }
@@ -74,8 +64,8 @@ Item {
7464 _fullyShown = false;
7565 }
7666
77− function notifyAuthenticationSucceeded(showFakePassword) {
78− _notifyAuthenticationSucceededCalled(showFakePassword);
67+ function showFakePassword() {
68+ _showFakePasswordCalled();
7969 }
8070
8171 function notifyAuthenticationFailed() {
@@ -86,8 +76,8 @@ Item {
8676 _showErrorMessageCalled(msg);
8777 }
8878
89− function reset(forceShow) {
90− _resetCalled(forceShow);
79+ function forceShow() {
80+ _forceShowCalled();
9181 }
9282
9383 function tryToUnlock(toTheRight) {
tests/qmltests/Greeter/tst_Greeter.qml+21−101
@@ -75,6 +75,12 @@ Item {
7575 signalName: "activeChanged"
7676 }
7777
78+ SignalSpy {
79+ id: authStartedSpy
80+ target: LightDM.Greeter
81+ signalName: "authenticationStarted"
82+ }
83+
7884 GSettings {
7985 id: greeterSettings
8086 schema.id: "com.canonical.Unity8.Greeter"
@@ -88,18 +94,6 @@ Item {
8894 property Item greeter: loader.status === Loader.Ready ? loader.item : null
8995 property Item view
9096
91− SignalSpy {
92− id: viewShowMessageSpy
93− target: testCase.view
94− signalName: "_showMessageCalled"
95− }
96−
97− SignalSpy {
98− id: viewShowPromptSpy
99− target: testCase.view
100− signalName: "_showPromptCalled"
101− }
102−
10397 SignalSpy {
10498 id: viewShowLastChanceSpy
10599 target: testCase.view
@@ -113,9 +107,9 @@ Item {
113107 }
114108
115109 SignalSpy {
116− id: viewAuthenticationSucceededSpy
110+ id: viewShowFakePasswordSpy
117111 target: testCase.view
118− signalName: "_notifyAuthenticationSucceededCalled"
112+ signalName: "_showFakePasswordCalled"
119113 }
120114
121115 SignalSpy {
@@ -131,9 +125,9 @@ Item {
131125 }
132126
133127 SignalSpy {
134− id: viewResetSpy
128+ id: viewForceShowSpy
135129 target: testCase.view
136− signalName: "_resetCalled"
130+ signalName: "_forceShowCalled"
137131 }
138132
139133 SignalSpy {
@@ -153,16 +147,15 @@ Item {
153147 Biometryd.available = true;
154148 AccountsService.enableFingerprintIdentification = true;
155149 AccountsService.failedFingerprintLogins = 0;
156− viewShowMessageSpy.clear();
157− viewShowPromptSpy.clear();
158150 viewShowLastChanceSpy.clear();
159151 viewHideSpy.clear();
160− viewAuthenticationSucceededSpy.clear();
152+ viewShowFakePasswordSpy.clear();
161153 viewAuthenticationFailedSpy.clear();
162154 viewShowErrorMessageSpy.clear();
163− viewResetSpy.clear();
155+ viewForceShowSpy.clear();
164156 viewTryToUnlockSpy.clear();
165157 resetLoader();
158+ authStartedSpy.clear();
166159 }
167160
168161 function resetLoader() {
@@ -210,7 +203,6 @@ Item {
210203
211204 function verifyLoggedIn() {
212205 tryCompare(sessionStartedSpy, "count", 1);
213− verify(viewAuthenticationSucceededSpy.count > 0);
214206 compare(LightDM.Greeter.authenticated, true);
215207 compare(greeter.shown, false);
216208 }
@@ -225,107 +217,52 @@ Item {
225217
226218 function test_unlockPass() {
227219 selectUser("has-password");
228− tryCompare(viewShowPromptSpy, "count", 1);
229− compare(viewShowPromptSpy.signalArguments[0][0], "Password");
230− compare(viewShowPromptSpy.signalArguments[0][1], true);
231− compare(viewShowPromptSpy.signalArguments[0][2], true);
232−
233220 view.responded("password");
234221 verifyLoggedIn();
235222 }
236223
237224 function test_unlockFail() {
238225 selectUser("has-password");
239− tryCompare(viewShowPromptSpy, "count", 1);
240− compare(viewShowPromptSpy.signalArguments[0][0], "Password");
241− compare(viewShowPromptSpy.signalArguments[0][1], true);
242− compare(viewShowPromptSpy.signalArguments[0][2], true);
226+ tryCompare(authStartedSpy, "count", 1);
243227
244228 view.responded("wr0ng p4ssw0rd");
245229 tryCompare(viewAuthenticationFailedSpy, "count", 1);
246230
247− tryCompare(viewShowPromptSpy, "count", 2);
248− compare(viewShowPromptSpy.signalArguments[0][0], "Password");
249− compare(viewShowPromptSpy.signalArguments[0][1], true);
250− compare(viewShowPromptSpy.signalArguments[0][2], true);
231+ tryCompare(authStartedSpy, "count", 2);
251232 }
252233
253234 function test_promptless() {
254235 selectUser("no-password");
255236 tryCompare(view, "locked", false);
256− compare(viewShowPromptSpy.count, 0);
257237 compare(viewHideSpy.count, 0);
258238 }
259239
260240 function test_twoFactorPass() {
261241 selectUser("two-factor");
262− tryCompare(viewShowPromptSpy, "count", 1);
263− compare(viewShowPromptSpy.signalArguments[0][0], "Password");
264− compare(viewShowPromptSpy.signalArguments[0][1], true);
265− compare(viewShowPromptSpy.signalArguments[0][2], true);
266−
267242 view.responded("password");
268− tryCompare(viewShowPromptSpy, "count", 2);
269− compare(viewShowPromptSpy.signalArguments[1][0], "otp");
270− compare(viewShowPromptSpy.signalArguments[1][1], false);
271− compare(viewShowPromptSpy.signalArguments[1][2], false);
272−
273243 view.responded("otp");
274244 verifyLoggedIn();
275245 }
276246
277247 function test_twoFactorFailOnFirst() {
278248 selectUser("two-factor");
279− tryCompare(viewShowPromptSpy, "count", 1);
280− compare(viewShowPromptSpy.signalArguments[0][0], "Password");
281− compare(viewShowPromptSpy.signalArguments[0][1], true);
282− compare(viewShowPromptSpy.signalArguments[0][2], true);
249+ tryCompare(authStartedSpy, "count", 1);
283250
284251 view.responded("wr0ng p4ssw0rd");
285252 tryCompare(viewAuthenticationFailedSpy, "count", 1);
286253
287− tryCompare(viewShowPromptSpy, "count", 2);
288− compare(viewShowPromptSpy.signalArguments[0][0], "Password");
289− compare(viewShowPromptSpy.signalArguments[0][1], true);
290− compare(viewShowPromptSpy.signalArguments[0][2], true);
254+ tryCompare(authStartedSpy, "count", 2);
291255 }
292256
293257 function test_twoFactorFailOnSecond() {
294258 selectUser("two-factor");
295− tryCompare(viewShowPromptSpy, "count", 1);
296− compare(viewShowPromptSpy.signalArguments[0][0], "Password");
297− compare(viewShowPromptSpy.signalArguments[0][1], true);
298− compare(viewShowPromptSpy.signalArguments[0][2], true);
259+ tryCompare(authStartedSpy, "count", 1);
299260
300261 view.responded("password");
301− tryCompare(viewShowPromptSpy, "count", 2);
302− compare(viewShowPromptSpy.signalArguments[1][0], "otp");
303− compare(viewShowPromptSpy.signalArguments[1][1], false);
304− compare(viewShowPromptSpy.signalArguments[1][2], false);
305−
306262 view.responded("wr0ng p4ssw0rd");
307263 tryCompare(viewAuthenticationFailedSpy, "count", 1);
308264
309− tryCompare(viewShowPromptSpy, "count", 3);
310− compare(viewShowPromptSpy.signalArguments[0][0], "Password");
311− compare(viewShowPromptSpy.signalArguments[0][1], true);
312− compare(viewShowPromptSpy.signalArguments[0][2], true);
313− }
314−
315− function test_htmlInfoPrompt() {
316− selectUser("html-info-prompt");
317− tryCompare(viewShowPromptSpy, "count", 1);
318− compare(viewShowMessageSpy.count, 1);
319− compare(viewShowMessageSpy.signalArguments[0][0], "&lt;b&gt;&amp;&lt;/b&gt;");
320− }
321−
322− function test_multiInfoPrompt() {
323− selectUser("multi-info-prompt");
324− tryCompare(viewShowPromptSpy, "count", 1);
325− compare(viewShowMessageSpy.count, 3);
326− compare(viewShowMessageSpy.signalArguments[0][0], "Welcome to Unity Greeter");
327− compare(viewShowMessageSpy.signalArguments[1][0], "<font color=\"#df382c\">This is an error</font>");
328− compare(viewShowMessageSpy.signalArguments[2][0], "You should have seen three messages");
265+ tryCompare(authStartedSpy, "count", 2);
329266 }
330267
331268 function test_locked() {
@@ -435,18 +372,9 @@ Item {
435372 compare(view.backgroundTopMargin, -5);
436373 }
437374
438− function test_differentPrompt() {
439− selectUser("different-prompt");
440− tryCompare(viewShowPromptSpy, "count", 1);
441− compare(viewShowPromptSpy.signalArguments[0][0], "Secret word");
442− compare(viewShowPromptSpy.signalArguments[0][1], true);
443− compare(viewShowPromptSpy.signalArguments[0][2], false);
444− }
445−
446375 function test_authError() {
447376 selectUser("auth-error");
448377 tryCompare(viewAuthenticationFailedSpy, "count", 1);
449− compare(viewShowPromptSpy.count, 0);
450378 compare(view.locked, true);
451379 }
452380
@@ -464,7 +392,6 @@ Item {
464392 greeter.failedLoginsDelayAttempts = 1;
465393 greeter.failedLoginsDelayMinutes = 1;
466394 selectUser("has-password");
467− tryCompare(viewShowPromptSpy, "count", 1);
468395 compare(greeterSettings.lockedOutTime, 0);
469396 view.responded("wr0ng p4ssw0rd");
470397
@@ -498,7 +425,6 @@ Item {
498425 greeter.failedLoginsDelayMinutes = 0.001; // make delay very short
499426
500427 selectUser("has-password");
501− tryCompare(viewShowPromptSpy, "count", 1);
502428
503429 compare(view.delayMinutes, 0);
504430 view.responded("wr0ng p4ssw0rd");
@@ -558,16 +484,10 @@ Item {
558484
559485 function test_dbusShowGreeterFromShownState() {
560486 selectUser("has-password");
561− compare(viewResetSpy.count, 1);
562− tryCompare(viewShowPromptSpy, "count", 1);
563−
564− viewResetSpy.clear();
565− viewShowPromptSpy.clear();
566487
488+ compare(viewForceShowSpy.count, 0);
567489 LightDM.Greeter.showGreeter();
568− compare(viewResetSpy.count, 1);
569− compare(viewResetSpy.signalArguments[0][0], true);
570− tryCompare(viewShowPromptSpy, "count", 1);
490+ compare(viewForceShowSpy.count, 1);
571491 }
572492
573493 function test_selectUserHint() {
tests/qmltests/Greeter/tst_NarrowView.qml+11−41
@@ -79,28 +79,16 @@ Item {
7979 anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
8080 spacing: units.gu(1)
8181
82− Row {
83− Button {
84− text: "Show Last Chance"
85− onClicked: loader.item.showLastChance()
86− }
87− }
8882 Row {
8983 Button {
9084 text: "Hide"
9185 onClicked: loader.item.hide()
9286 }
9387 }
94− Row {
95− Button {
96− text: "Reset"
97− onClicked: loader.item.reset()
98− }
99− }
10088 Row {
10189 Button {
10290 text: "Show Message"
103− onClicked: loader.item.showMessage(messageField.text)
91+ onClicked: LightDMService.prompts.append(messageField.text, LightDMService.prompts.Message)
10492 }
10593 TextField {
10694 id: messageField
@@ -111,7 +99,7 @@ Item {
11199 Row {
112100 Button {
113101 text: "Show Prompt"
114− onClicked: loader.item.showPrompt(promptField.text, isSecretCheckBox.checked, isDefaultPromptCheckBox.checked)
102+ onClicked: LightDMService.prompts.append(promptField.text, isSecretCheckBox.checked ? LightDMService.prompts.Secret : LightDMService.prompts.Question)
115103 }
116104 TextField {
117105 id: promptField
@@ -124,36 +112,14 @@ Item {
124112 Label {
125113 text: "secret"
126114 }
127− CheckBox {
128− id: isDefaultPromptCheckBox
129− }
130− Label {
131− text: "default"
132− }
133115 }
134116 Row {
135117 Button {
136− text: "Authenticated"
118+ text: "Notify Auth Failure"
137119 onClicked: {
138− if (successCheckBox.checked) {
139− loader.item.notifyAuthenticationSucceeded(fakePasswordCheckBox.checked);
140− } else {
141− loader.item.notifyAuthenticationFailed();
142− }
120+ loader.item.notifyAuthenticationFailed();
143121 }
144122 }
145− CheckBox {
146− id: successCheckBox
147− }
148− Label {
149− text: "success"
150− }
151− CheckBox {
152− id: fakePasswordCheckBox
153− }
154− Label {
155− text: "fake password"
156− }
157123 }
158124 Row {
159125 Button {
@@ -218,6 +184,7 @@ Item {
218184 Row {
219185 CheckBox {
220186 id: alphanumericCheckBox
187+ checked: true
221188 }
222189 Label {
223190 text: "alphanumeric"
@@ -301,6 +268,9 @@ Item {
301268 function init() {
302269 view.currentIndex = 0; // break binding with text field
303270
271+ LightDM.Greeter.authenticate("no-password");
272+ tryCompare(LightDMService.prompts, "count", 1);
273+
304274 telepathyHelper.ready = true;
305275 telepathyHelper.emergencyCallsAvailable = true;
306276 selectedSpy.clear();
@@ -365,8 +335,9 @@ Item {
365335 }
366336
367337 function test_respondedWithPin() {
338+ LightDM.Greeter.authenticate("has-pin");
368339 view.locked = true;
369− view.showPrompt("", true, true);
340+ view.alphanumeric = false;
370341 swipeAwayCover();
371342 typeString("1234");
372343 compare(respondedSpy.count, 1);
@@ -374,9 +345,8 @@ Item {
374345 }
375346
376347 function test_respondedWithPassphrase() {
348+ LightDM.Greeter.authenticate("has-password");
377349 view.locked = true;
378− view.alphanumeric = true;
379− view.showPrompt("", true, true);
380350 swipeAwayCover();
381351 typeString("test");
382352 keyClick(Qt.Key_Enter);
tests/qmltests/Greeter/tst_WideView.qml+96−116
@@ -110,28 +110,16 @@ StyledItem {
110110 Column {
111111 anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
112112 spacing: units.gu(1)
113− Row {
114− Button {
115− text: "Show Last Chance"
116− onClicked: loader.item.showLastChance()
117− }
118− }
119113 Row {
120114 Button {
121115 text: "Hide"
122116 onClicked: loader.item.hide()
123117 }
124118 }
125− Row {
126− Button {
127− text: "Reset"
128− onClicked: loader.item.reset()
129− }
130− }
131119 Row {
132120 Button {
133121 text: "Show Message"
134− onClicked: loader.item.showMessage(messageField.text)
122+ onClicked: LightDMService.prompts.append(messageField.text, LightDMService.prompts.Message)
135123 }
136124 TextField {
137125 id: messageField
@@ -142,7 +130,7 @@ StyledItem {
142130 Row {
143131 Button {
144132 text: "Show Prompt"
145− onClicked: loader.item.showPrompt(promptField.text, isSecretCheckBox.checked, isDefaultPromptCheckBox.checked)
133+ onClicked: LightDMService.prompts.append(promptField.text, isSecretCheckBox.checked ? LightDMService.prompts.Secret : LightDMService.prompts.Question)
146134 }
147135 TextField {
148136 id: promptField
@@ -155,30 +143,14 @@ StyledItem {
155143 Label {
156144 text: "secret"
157145 }
158− CheckBox {
159− id: isDefaultPromptCheckBox
160− }
161− Label {
162− text: "default"
163− }
164146 }
165147 Row {
166148 Button {
167− text: "Authenticated"
149+ text: "Notify Auth Failure"
168150 onClicked: {
169− if (successCheckBox.checked) {
170− loader.item.notifyAuthenticationSucceeded(false);
171− } else {
172− loader.item.notifyAuthenticationFailed();
173− }
151+ loader.item.notifyAuthenticationFailed();
174152 }
175153 }
176− CheckBox {
177− id: successCheckBox
178− }
179− Label {
180− text: "success"
181− }
182154 }
183155 Row {
184156 Button {
@@ -359,12 +331,17 @@ StyledItem {
359331
360332 function init() {
361333 selectIndex(0); // break binding with text field
334+ tryCompare(LightDMService.prompts, "count", 1);
362335 selectedSpy.clear();
363336 respondedSpy.clear();
364337 teaseSpy.clear();
365338 emergencySpy.clear();
366− LightDMController.sessionMode = "full";
339+ LightDMController.reset();
367340 LightDM.Sessions.iconSearchDirectories = [testIconDirectory];
341+
342+ waitForRendering(view);
343+ var userList = findChild(view, "userList");
344+ tryCompare(userList, "movingInternally", false);
368345 }
369346
370347 function cleanup() {
@@ -391,8 +368,14 @@ StyledItem {
391368 }
392369
393370 function selectIndex(i) {
394− view.currentIndex = i;
371+ var user = LightDM.Users.data(i, LightDM.UserRoles.NameRole);
372+ LightDM.Greeter.authenticate(user);
373+
395374 var userList = findChild(view, "userList");
375+ var promptList = findChild(view, "promptList");
376+
377+ view.currentIndex = i;
378+ tryCompare(promptList, "opacity", 1);
396379 tryCompare(userList, "movingInternally", false);
397380 }
398381
@@ -412,34 +395,16 @@ StyledItem {
412395 }
413396
414397 function test_changingSessionSticksToUser() {
415− LightDMController.sessionMode = "full";
416−
417398 var loginList = findChild(view, "loginList");
418− var fakeSession1 = "ASessionWillNeverBeCalledThis";
419− var fakeSession2 = "ASessionWillAlsoNeverBeCalledThis";
420−
421− // For this test to work, a user has to be selected and the
422− // mock must be made aware of that user
423− selectUser("has-password")
424− LightDMController.currentUsername = "has-password"
425− compare(LightDM.Sessions.count > 1, true);
426− compare(loginList.currentSession != fakeSession1, true);
427− LightDMController.sessionName = fakeSession1;
428−
429− selectUser("has-pin")
430− LightDMController.currentUsername = "has-pin"
431− compare(loginList.currentSession != fakeSession2, true);
432− LightDMController.sessionName = fakeSession2;
433399
434− selectUser("has-password")
435− tryCompare(loginList, "currentSession", fakeSession1);
400+ selectUser("invalid-session");
401+ tryCompare(loginList, "currentSession", "invalid");
436402
437− selectUser("has-pin")
438− tryCompare(loginList, "currentSession", fakeSession2);
403+ selectUser("has-password");
404+ tryCompare(loginList, "currentSession", "ubuntu");
439405
440− // Force a model reset
441− LightDMController.userMode = "single";
442− LightDMController.userMode = "full";
406+ selectUser("invalid-session")
407+ tryCompare(loginList, "currentSession", "invalid");
443408 }
444409
445410 function test_tease_data() {
@@ -512,9 +477,6 @@ StyledItem {
512477 }
513478
514479 function test_sessionIconNotShownWithActiveUser() {
515− LightDMController.sessionMode = "full";
516− compare(LightDM.Sessions.count > 1, true);
517−
518480 selectUser("active");
519481
520482 var sessionChooserButton = findChild(view, "sessionChooserButton");
@@ -522,9 +484,6 @@ StyledItem {
522484 }
523485
524486 function test_sessionIconShownWithMultipleSessions() {
525− LightDMController.sessionMode = "full";
526− compare(LightDM.Sessions.count > 1, true);
527−
528487 selectUser("has-password");
529488
530489 var sessionChooserButton = findChild(view, "sessionChooserButton");
@@ -558,12 +517,9 @@ StyledItem {
558517 }
559518
560519 function test_respondedWithPassword() {
561− view.locked = true;
562− view.showPrompt("Prompt", true, false);
563− var passwordInput = findChild(view, "passwordInput");
564− compare(passwordInput.text, "Prompt");
565− verify(passwordInput.isSecret);
566− tap(passwordInput);
520+ selectUser("has-password");
521+ var greeterPrompt = findChild(view, "greeterPrompt0");
522+ verify(greeterPrompt.isSecret);
567523 typeString("password");
568524 keyClick(Qt.Key_Enter);
569525 compare(respondedSpy.count, 1);
@@ -571,12 +527,9 @@ StyledItem {
571527 }
572528
573529 function test_respondedWithNonSecret() {
574− view.locked = true;
575− view.showPrompt("otp", false, false);
576− var passwordInput = findChild(view, "passwordInput");
577− compare(passwordInput.text, "otp");
578− verify(!passwordInput.isSecret);
579− tap(passwordInput);
530+ selectUser("question-prompt");
531+ var greeterPrompt = findChild(view, "greeterPrompt0");
532+ verify(!greeterPrompt.isSecret);
580533 typeString("foo");
581534 keyClick(Qt.Key_Enter);
582535 compare(respondedSpy.count, 1);
@@ -601,24 +554,47 @@ StyledItem {
601554 tryCompare(view, "required", false);
602555 }
603556
604− function test_showMessage() {
605− view.showMessage("Welcome to Unity Greeter");
606− view.showMessage("<font color=\"#df382c\">This is an error</font>");
607− view.showMessage("You should have seen three messages and this is a really long message too. wow so long much length");
608− var infoLabel = findChild(view, "infoLabel");
609− 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");
610− compare(infoLabel.textFormat, Text.StyledText);
557+ function test_infoPrompt() {
558+ selectUser("info-prompt");
559+
560+ var infoLabel = findChild(view, "infoLabel0");
561+ compare(infoLabel.text, "Welcome to Unity Greeter");
562+ compare(infoLabel.textFormat, Text.PlainText);
563+
564+ verify(findChild(view, "greeterPrompt1") != null);
565+ }
566+
567+ function test_longInfoPrompt() {
568+ selectUser("long-info-prompt");
569+
570+ var infoLabel = findChild(view, "infoLabel0");
571+ compare(infoLabel.text, "Welcome to Unity Greeter\n\nWe like to annoy you with super ridiculously long messages.\nLike this one\n\nThis is the last line of a multiple line message.");
611572 verify(infoLabel.contentWidth > infoLabel.width);
612− verify(infoLabel.opacity < 1);
613− tryCompare(infoLabel, "opacity", 1);
573+
574+ verify(findChild(view, "greeterPrompt1") != null);
575+ }
576+
577+ function test_multiInfoPrompt() {
578+ selectUser("multi-info-prompt");
579+
580+ var infoLabel0 = findChild(view, "infoLabel0");
581+ compare(infoLabel0.text, "Welcome to Unity Greeter");
582+
583+ var infoLabel1 = findChild(view, "infoLabel1");
584+ compare(infoLabel1.text, "This is an error");
585+ compare(infoLabel1.color, theme.palette.normal.negative);
586+
587+ var infoLabel2 = findChild(view, "infoLabel2");
588+ compare(infoLabel2.text, "You should have seen three messages");
589+
590+ verify(findChild(view, "greeterPrompt3") != null);
614591 }
615592
616593 // Escape is used to reset the authentication, especially if PAM is unresponsive
617594 function test_escape() {
618− selectIndex(1);
595+ var index = selectUser("has-password");
619596 selectedSpy.clear();
620597 view.locked = true;
621− view.showPrompt("Prompt", true, true);
622598 var promptField = findChild(view, "promptField");
623599 tap(promptField);
624600 verify(promptField.activeFocus);
@@ -632,9 +608,10 @@ StyledItem {
632608 compare(selectedSpy.count, 0);
633609 keyClick(Qt.Key_Escape);
634610 compare(selectedSpy.count, 1);
635− compare(selectedSpy.signalArguments[0][0], 1);
611+ compare(selectedSpy.signalArguments[0][0], index);
636612
637− view.reset();
613+ selectIndex(index);
614+ var promptField = findChild(view, "promptField");
638615 verify(promptField.activeFocus);
639616 compare(promptField.opacity, 1);
640617 }
@@ -645,21 +622,22 @@ StyledItem {
645622 tryCompare(label, "text", "가나다라마");
646623 }
647624
648− function test_promptless() {
649− var passwordInput = findChild(view, "passwordInput");
650−
625+ function test_authError() {
626+ var index = selectUser("auth-error");
627+ var greeterPrompt = findChild(view, "greeterPrompt1"); // after error message
651628 view.locked = true;
652− compare(passwordInput.text, "Retry");
653− tap(passwordInput);
629+ compare(greeterPrompt.text, "Retry");
630+ tap(greeterPrompt);
654631 compare(respondedSpy.count, 0);
655632 compare(selectedSpy.count, 1);
656− compare(selectedSpy.signalArguments[0][0], 0);
657− selectedSpy.clear();
633+ compare(selectedSpy.signalArguments[0][0], index);
634+ }
658635
659− view.reset();
660− view.locked = false;
661− compare(passwordInput.text, "Log In");
662− tap(passwordInput);
636+ function test_noPassword() {
637+ selectUser("no-password");
638+ var greeterPrompt = findChild(view, "greeterPrompt0");
639+ compare(greeterPrompt.text, "Log In");
640+ tap(greeterPrompt);
663641 compare(selectedSpy.count, 0);
664642 compare(respondedSpy.count, 1);
665643 compare(respondedSpy.signalArguments[0][0], "");
@@ -691,8 +669,8 @@ StyledItem {
691669 }
692670
693671 function test_passphrase() {
672+ var index = selectUser("has-password");
694673 var promptField = findChild(view, "promptField");
695− view.showPrompt("", true, true);
696674
697675 verify(view.alphanumeric);
698676 compare(promptField.inputMethodHints & Qt.ImhDigitsOnly, 0);
@@ -702,8 +680,8 @@ StyledItem {
702680 }
703681
704682 function test_passcode() {
683+ var index = selectUser("has-pin");
705684 var promptField = findChild(view, "promptField");
706− view.showPrompt("", true, true);
707685
708686 view.alphanumeric = false;
709687 compare(promptField.inputMethodHints & Qt.ImhDigitsOnly, Qt.ImhDigitsOnly);
@@ -737,42 +715,44 @@ StyledItem {
737715 }
738716
739717 function test_focusStaysActive() {
740− var promptField = findChild(view, "promptField");
741− var promptButton = findChild(view, "promptButton");
742−
743− verify(promptButton.activeFocus);
718+ selectUser("no-password");
719+ var greeterPrompt = findChild(view, "greeterPrompt0");
720+ verify(greeterPrompt.activeFocus);
744721 keyClick(Qt.Key_Enter);
745722 compare(selectedSpy.count, 0);
746723 compare(respondedSpy.count, 1);
747724 compare(respondedSpy.signalArguments[0][0], "");
748− verify(promptButton.activeFocus);
725+ verify(greeterPrompt.activeFocus);
749726 keyClick(Qt.Key_Enter);
750727 compare(respondedSpy.count, 1);
751728
752− view.showPrompt("", true, true);
753− verify(promptField.activeFocus);
729+ selectUser("has-password");
730+ var greeterPrompt = findChild(view, "greeterPrompt0");
731+ verify(greeterPrompt.activeFocus);
754732 keyClick(Qt.Key_D);
755733 keyClick(Qt.Key_Enter);
756734 compare(selectedSpy.count, 0);
757735 compare(respondedSpy.count, 2);
758736 compare(respondedSpy.signalArguments[1][0], "d");
759− verify(promptField.activeFocus);
737+ verify(greeterPrompt.activeFocus);
760738 keyClick(Qt.Key_Enter);
761739 compare(respondedSpy.count, 2);
762740
763− view.reset();
741+ var index = selectUser("no-password");
742+ var greeterPrompt = findChild(view, "greeterPrompt0");
764743 view.locked = true;
765− verify(promptButton.activeFocus);
744+ verify(greeterPrompt.activeFocus);
766745 keyClick(Qt.Key_Enter);
767746 compare(respondedSpy.count, 2);
768747 compare(selectedSpy.count, 1);
769− compare(selectedSpy.signalArguments[0][0], 0);
770− verify(promptButton.activeFocus);
748+ compare(selectedSpy.signalArguments[0][0], index);
749+ verify(greeterPrompt.activeFocus);
771750 keyClick(Qt.Key_Enter);
772751 compare(selectedSpy.count, 1);
773752
774− view.showPrompt("", true, true);
775− verify(promptField.activeFocus);
753+ selectUser("no-password");
754+ var greeterPrompt = findChild(view, "greeterPrompt0");
755+ verify(greeterPrompt.activeFocus);
776756 }
777757 }
778758}
tests/qmltests/tst_Shell.qml+9−8
@@ -833,13 +833,15 @@ Rectangle {
833833 keyClick(Qt.Key_Escape) // Reset state if we're not moving
834834 while (userlist.currentIndex != i) {
835835 var next = userlist.currentIndex + 1
836+ var key = Qt.Key_Down;
836837 if (userlist.currentIndex > i) {
837838 next = userlist.currentIndex - 1
839+ key = Qt.Key_Up;
838840 }
839− tap(findChild(greeter, "username"+next));
841+ keyPress(key);
840842 tryCompare(userlist, "currentIndex", next)
841− tryCompare(userlist, "movingInternally", false)
842843 }
844+ tryCompare(userlist, "movingInternally", false);
843845 tryCompare(shell, "waitingOnGreeter", false); // wait for PAM to settle
844846 }
845847
@@ -862,15 +864,13 @@ Rectangle {
862864 var greeter = findChild(shell, "greeter")
863865 tryCompare(greeter, "fullyShown", true);
864866
865− var passwordInput = findChild(shell, "passwordInput");
866−
867− var promptButton = findChild(passwordInput, "promptButton");
867+ var promptButton = findChild(greeter, "promptButton");
868868 tryCompare(promptButton, "visible", isButton);
869869
870− var promptField = findChild(passwordInput, "promptField");
870+ var promptField = findChild(greeter, "promptField");
871871 tryCompare(promptField, "visible", !isButton);
872872
873− mouseClick(passwordInput)
873+ mouseClick(promptButton);
874874 }
875875
876876 function confirmLoggedIn(loggedIn) {
@@ -1473,7 +1473,7 @@ Rectangle {
14731473 confirmLoggedIn(data.loggedIn)
14741474
14751475 if (data.passwordFocus) {
1476− var passwordInput = findChild(greeter, "passwordInput")
1476+ var passwordInput = findChild(greeter, "promptField");
14771477 tryCompare(passwordInput, "focus", true)
14781478 }
14791479 }
@@ -1496,6 +1496,7 @@ Rectangle {
14961496 typeString("has-password");
14971497 keyClick(Qt.Key_Enter);
14981498
1499+ promptHint = findChild(greeter, "promptHint");
14991500 tryCompare(username, "text", "has-password");
15001501 tryCompare(promptHint, "text", "Passphrase");
15011502
tests/qmltests/tst_ShellWithPin.qml+2−10
@@ -117,12 +117,6 @@ Item {
117117 signalName: "resettingDevice"
118118 }
119119
120− SignalSpy {
121− id: promptSpy
122− target: LightDM.Greeter
123− signalName: "showPrompt"
124− }
125−
126120 Telephony.CallEntry {
127121 id: phoneCall
128122 phoneNumber: "+447812221111"
@@ -539,22 +533,20 @@ Item {
539533 shellLoader.itemDestroyed = false;
540534 shellLoader.active = false;
541535 tryCompare(shellLoader, "itemDestroyed", true);
542− LightDM.Greeter.authenticate(""); // reset greeter
543536
544537 // Create new shell
545− promptSpy.clear();
546538 shellLoader.active = true;
547539 tryCompareFunction(function() {return shell !== null}, true);
548540
549541 // Confirm that we start disabled
550− compare(promptSpy.count, 0);
542+ compare(LightDM.Prompts.count, 0);
551543 verify(shell.waitingOnGreeter);
552544 var coverPageDragHandle = findChild(shell, "coverPageDragHandle");
553545 verify(!coverPageDragHandle.enabled);
554546
555547 // And that we only become enabled once the lockscreen is up
556548 tryCompare(shell, "waitingOnGreeter", false);
557− verify(promptSpy.count > 0);
549+ verify(LightDM.Prompts.count > 0);
558550 var lockscreen = findChild(shell, "lockscreen");
559551 verify(lockscreen.shown);
560552 }