Merge lp:~uriboni/webbrowser-app/keyboard-navigation into lp:webbrowser-app

Proposed by Ugo Riboni
Status: Merged
Approved by: Olivier Tilloy
Approved revision: 1097
Merged at revision: 1063
Proposed branch: lp:~uriboni/webbrowser-app/keyboard-navigation
Merge into: lp:webbrowser-app
Prerequisite: lp:~rpadovani/webbrowser-app/newTabRefactoring
Diff against target: 1236 lines (+756/-78)
16 files modified
src/app/webbrowser/AddressBar.qml (+16/-8)
src/app/webbrowser/Browser.qml (+213/-21)
src/app/webbrowser/Chrome.qml (+3/-0)
src/app/webbrowser/KeyboardShortcut.qml (+25/-0)
src/app/webbrowser/KeyboardShortcuts.qml (+41/-0)
src/app/webbrowser/Suggestion.qml (+3/-2)
src/app/webbrowser/Suggestions.qml (+40/-38)
src/app/webbrowser/limit-proxy-model.cpp (+16/-0)
src/app/webbrowser/limit-proxy-model.h (+2/-0)
tests/autopilot/webbrowser_app/emulators/browser.py (+17/-2)
tests/autopilot/webbrowser_app/tests/__init__.py (+10/-2)
tests/autopilot/webbrowser_app/tests/http_server.py (+4/-0)
tests/autopilot/webbrowser_app/tests/test_addressbar_bookmark.py (+0/-2)
tests/autopilot/webbrowser_app/tests/test_keyboard.py (+276/-0)
tests/autopilot/webbrowser_app/tests/test_suggestions.py (+68/-3)
tests/unittests/limit-proxy-model/tst_LimitProxyModelTests.cpp (+22/-0)
To merge this branch: bzr merge lp:~uriboni/webbrowser-app/keyboard-navigation
Reviewer Review Type Date Requested Status
Olivier Tilloy Approve
PS Jenkins bot continuous-integration Needs Fixing
Review via email: mp+260183@code.launchpad.net

Commit message

Make the browser chrome usable on desktop by implementing common keyboard shortcuts and behaviors that users normally expect in such an app

Description of the change

The goal of this merge request is to make the browser chrome usable on desktop by implementing common keyboard shortcuts and behaviors that users normally expect in such an app.
Some come directly from the design document and some have been added based on what is obvious and intuitive (and already by other browsers).

This is based on, and supersedes, the following contributor branch: https://code.launchpad.net/~gang65/webbrowser-app/webbrowser-app-keyboard-shortcuts due to the original contributor not replying to requests on the MR in time.

It is also based on this refactoring branch: https://code.launchpad.net/~rpadovani/webbrowser-app/newTabRefactoring/+merge/247498

Finally it also supersedes this branch adding keyboard support to the suggestions list, as it is essentially a superset of that work so they might as well go together: https://code.launchpad.net/~uriboni/webbrowser-app/suggestions-keyboard-navigation

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Olivier Tilloy (osomon) wrote :

The flake8 unit test fails.

A few functional issues (I haven’t done a complete review yet):

 - If the address bar is empty and focused (e.g. when opening a new blank tab), pressing the down arrow key removes focus from it, and pressing the up arrow key doesn’t restore it.

 - If I type a word in the address bar and wait for search suggestions to appear, then press the down arrow key to navigate the suggestions list, the search suggestions disappear.

 - If I focus the address bar, then open the drawer menu with the mouse and open the history view from there, it cannot be dismissed with ESC (it works as expected if the address bar wasn’t focused or if the view was opened with Ctrl+H).

review: Needs Fixing
Revision history for this message
Ugo Riboni (uriboni) wrote :

> - If the address bar is empty and focused (e.g. when opening a new blank
> tab), pressing the down arrow key removes focus from it, and pressing the up
> arrow key doesn’t restore it.

This was fixed by making sure we can move to the suggestions list with Down only if there are actually items in it.

> - If I type a word in the address bar and wait for search suggestions to
> appear, then press the down arrow key to navigate the suggestions list, the
> search suggestions disappear.

This was more tricky than how you described it. It happened only if the only suggestions in the list were from the search engine. If there were history or bookmarks in the list it did not happen.

The cause was the fact that the search engine suggestions model would be disabled (therefore reducing the count of suggestion to zero and closing the list) when the chrome did not have the focus, but did not take into account the case when the suggestion list itself had focus.

> - If I focus the address bar, then open the drawer menu with the mouse and
> open the history view from there, it cannot be dismissed with ESC (it works as
> expected if the address bar wasn’t focused or if the view was opened with
> Ctrl+H).

Wasn't giving focus to the history page when activating from menu.

Note: I added a test case to prevent the first issue from re-occurring, but I have no idea how to test for the other two, as the tests would need to verify that *eventually* something does not happen, which is hard to do without using arbitrary waits that make the tests fragile.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Olivier Tilloy (osomon) wrote :

The following autopilot test is broken on desktop:

    webbrowser_app.tests.test_errorsheet.TestErrorSheet.test_navigating_forward_discards_error_message

review: Needs Fixing
Revision history for this message
Olivier Tilloy (osomon) wrote :

A few more functional issues I found while testing:

 - If I load a page, then focus the address bar, then type e.g. "ab", then press the down arrow to navigate the suggestions, then press the up arrow to re-focus the address bar, then press ESC, the current URL is not restored in the address bar (if I do it while navigating the suggestions it is restored)

 - If I’m browsing a page that has a favicon and that’s loaded over a secure connection (i.e. the lock icon is displayed), and if I focus the address bar to start typing search terms (e.g. "ab"), the favicon and the lock icon are hidden, and a magnifier icon is displayed. But if I then press the down key to navigate the suggestions, the favicon and lock icon are displayed again. I think the magnifier icon should remain when navigating through the suggestions.

 - On a page where I can navigate back/forward, if I focus the address bar with Ctrl+L, then press Alt+[Left/Right], the previous/next page is loaded, but the address bar is cleared. I would say that navigating back/forward should remove focus from the address bar (and that should solve the issue).

 - If I open the history view with Ctrl+H, then press Ctrl+L, nothing visible happens (as expected because the address bar is not visible), however I can’t close the history view with ESC any longer. I think Ctrl+L should not focus the address bar if it’s not visible.

 - Similarly, Ctrl+D to (un)bookmark also works when the history view is visible, but it shouldn’t.

review: Needs Fixing
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Olivier Tilloy (osomon) wrote :

webbrowser_app.tests.test_suggestions.TestSuggestions.test_show_list_of_suggestions reliably fails when run on my desktop.

review: Needs Fixing
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Olivier Tilloy (osomon) wrote :

ESC in the tabs view (recentView) and in the settings page should exit the view (I think it’s ok if more advanced keyboard navigation is not implemented in those views yet, but at the very least it should be possible to leave them using the keyboard).

It might be interesting to have Ctrl+T, Ctrl+W and Ctrl+Tab work while recentView is visible.

The current implementation of Ctrl+Tab doesn’t switch to the next tab, it switches to the last one, is that intended?

I’m not fond of the name of the 'textLocked' property: it kind of suggests that the text cannot be modified, which isn’t true, because keyboard input will modify the text. Can we think of a more appropriate name?

In internal.switchToTab(), you don’t want to focus the address bar when the tab is empty on devices (you only want that behaviour on desktop, because on mobile the OSK would get in the way of the new tab view).

In the definition of LimitProxyModel::get(), there is an extra blank line that should be removed.

In Browser.press_key(), is it right to create a Keyboard instance everytime we press a key? Shouldn’t the instance be created in Browser.__init__() instead? And why is there a try… except block? When can we get a RuntimeError for pressing a key? Silently swallowing exceptions is usually not a good practice, except in very specific cases.

Can PrepopulatedDatabaseTestCaseBase be factored out in a single base class that all tests can re-use?

According to https://docs.python.org/3.3/library/unittest.html#skipping-tests-and-expected-failures, "Classes can be skipped just like methods", so no need to add a skipIf decorator on every single test method in the TestKeyboard class, the entire class should be skipped.

review: Needs Fixing
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Ugo Riboni (uriboni) wrote :

What does not have a reply was simply fixed.

> ESC in the tabs view (recentView) and in the settings page should exit the
> view (I think it’s ok if more advanced keyboard navigation is not implemented
> in those views yet, but at the very least it should be possible to leave them
> using the keyboard).
> It might be interesting to have Ctrl+T, Ctrl+W and Ctrl+Tab work while
> recentView is visible.

I did all of the above, and also unified the way we exit the tabs view (by switching to whatever tab is current, or by switching to the tab that the user clicked on). This helps fix the problem of sometimes not focusing the address bar when the new tab page is selected (and allows having less places where to put the conditional to focus it only when on desktop that you mention below).

> The current implementation of Ctrl+Tab doesn’t switch to the next tab, it
> switches to the last one, is that intended?

Yes, due to the way tabs are implemented the current tab is always brought to the top of the stack, so this is by design.

> I’m not fond of the name of the 'textLocked' property: it kind of suggests
> that the text cannot be modified, which isn’t true, because keyboard input
> will modify the text. Can we think of a more appropriate name?

I suck at naming variables. preventSimplifyText is the best I could come up with. Hope it works.

> In Browser.press_key(), is it right to create a Keyboard instance everytime we
> press a key? Shouldn’t the instance be created in Browser.__init__() instead?
> And why is there a try… except block? When can we get a RuntimeError for
> pressing a key? Silently swallowing exceptions is usually not a good practice,
> except in very specific cases.

This was from the autopilot guide. I think the create() method can give a RuntimeError if no keyboard is available, which is also why I was getting the keyboard only when in need to send a keystroke.
However I looked at the UI toolkit emulators and they get the keyboard at set up and without any error checking, so it must be safe. I did the same thing that they do now.

> Can PrepopulatedDatabaseTestCaseBase be factored out in a single base class
> that all tests can re-use?

I see little point. Most of the class is populating the database with data, and the data is different for every test.
You could possibly generalize some of the database access in a base class and allow subclasses to provide only the list of data, but it would be much work for little advantage in my opinion.
I'd rather avoid this.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Olivier Tilloy (osomon) wrote :

When in the tabs view, pressing Ctrl+T to open a new tab should probably dismiss the view (just like clicking the "New Tab" button does).

> I suck at naming variables. preventSimplifyText is the best I could
> come up with. Hope it works.

While it works, I must say I don’t like the name very much either. I would prefer 'canSimplifyText' (changing the semantics of the property to its opposite).

> You could possibly generalize some of the database access in a base
> class and allow subclasses to provide only the list of data, but it
> would be much work for little advantage in my opinion.

Yeah, that’s what I was kind of suggesting. We can do that separately later on though.

In AddressBar.qml:
  - updateUrlFromFocus() doesn’t seem to be used externally, can it be made a private (internal) function?
  - I’m not sure I understand why the test on 'preventSimplifyText' is not inside updateUrlFromFocus(). When the address bar has active focus, we always want the text to be expanded, right? So the check would be better placed in the 'else if' branch of the function, no?

As pointed out by Bill in his e-mail review (adding it here for future reference), the following need implementing (per design spec):
 - F11 to toggle fullscreen mode
 - F6 should focus the address bar, just like Ctrl+L and Alt+D

 - and F5 for page reload, as you suggested, would be a useful addition, too

review: Needs Fixing
Revision history for this message
Ugo Riboni (uriboni) wrote :

All done except where commented below:

> - I’m not sure I understand why the test on 'preventSimplifyText' is not
> inside updateUrlFromFocus(). When the address bar has active focus, we always
> want the text to be expanded, right? So the check would be better placed in
> the 'else if' branch of the function, no?

Address bar and suggestion list should be logically considered one single unit in terms of what should happen when they have active focus. However they are two different elements in two different locations in the object tree.
The canSimplifyText exists for this reason: when it is true it means that neither the address bar nor the suggestions list have active focus.
I moved the check inside the updateUrlFromFocus anyway, as it makes the code simpler, but it should not be in the else branch as you suggest.

> As pointed out by Bill in his e-mail review (adding it here for future
> reference), the following need implementing (per design spec):
> - F11 to toggle fullscreen mode

This will be addressed later as we first need to create a proper design that takes into account the difference between application fullscreen and page fullscreen. Tracked by: https://pad.lv/1464333

Revision history for this message
Olivier Tilloy (osomon) wrote :

> Address bar and suggestion list should be logically considered one
> single unit in terms of what should happen when they have active focus.
> However they are two different elements in two different locations in
> the object tree.

Right, and we should probably consider refactoring that code to include the suggestions list in the address bar. In a separate branch, of course.

Revision history for this message
Olivier Tilloy (osomon) wrote :

We’re getting there nicely. A few more minor remarks:

288 + if (tabsModel.count >= 0) {

It should be (tabsModel.count > 0), there is no point in trying to close the current tab if there isn’t any.

680 + def setUp(self, url="/test1"):

That argument would better be named 'path', rather than 'url'.

986 + self.assertThat(suggestions.count, Eventually(NotEquals(0)))

Although functionally equivalent, GreaterThan would be more appropriate than NotEquals.

996 + def test_keyboard_movement(self):

Can test_keyboard_movement be renamed test_keyboard_navigation ?

1085 + QCOMPARE(item.count(), 0);

Can I suggest QVERIFY(item.isEmpty()) ?

review: Needs Fixing
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Riccardo Padovani (rpadovani) wrote :

Thanks for working on this, now the desktop experience is definitely better!

I leave some impression, don't know if you want to fix in this branch or in a latter one

Some comments:
- I expect to navigate the history with arrows after I open it with CTRL+H
- If I navigate to a page I can scroll with arrows (good!), then I open history with CTRL+H, then I press ESC, arrows don't do anything anymore. I expect they still scroll the page

Some more keybinding I would like:
- CTRL+SHIFT+T to reopen the last closed tab
- ALT+F (as on Chromium) to open the menu and then arrows to navigate it
- F11 to go fullscreen (but this I think requires some other works)

I took a fast look to the code and, apart of a little typo I left a comment inline, looks good to me, except Python I didn't review due my lack of knowledge of Python.

Cool work!

Revision history for this message
Olivier Tilloy (osomon) wrote :

I don’t think the fix in revision 1085 is correct, the tabs view is not being open any longer, so it breaks the test assumption, doesn’t it?

review: Needs Fixing
Revision history for this message
Olivier Tilloy (osomon) wrote :

> I expect to navigate the history with arrows after I open it with CTRL+H

We agreed that this would be implemented later on

> If I navigate to a page I can scroll with arrows (good!), then I open history with CTRL+H, then I press ESC, arrows don't do anything anymore. I expect they still scroll the page

Good catch, active focus should be restored on the webview when exiting the history view, if the address bar wasn’t focused previously.

> CTRL+SHIFT+T to reopen the last closed tab

Would be nice to have, but will require more work to keep track of recently closed tab, so let’s do that separately.

> ALT+F (as on Chromium) to open the menu and then arrows to navigate it

Would be nice to have too, but not a must for this iteration. Again, let’s do that separately.

> F11 to go fullscreen (but this I think requires some other works)

Yes, this is bug #1464333.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Olivier Tilloy (osomon) wrote :

webbrowser_app.tests.test_addressbar_bookmark.TestAddressBarBookmark.test_cannot_bookmark_empty_page fails here on my desktop.

review: Needs Fixing
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Olivier Tilloy (osomon) :
review: Approve
Revision history for this message
Olivier Tilloy (osomon) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/app/webbrowser/AddressBar.qml'
2--- src/app/webbrowser/AddressBar.qml 2015-05-26 21:42:54 +0000
3+++ src/app/webbrowser/AddressBar.qml 2015-06-16 16:15:45 +0000
4@@ -37,6 +37,7 @@
5 signal requestReload()
6 signal requestStop()
7 property string searchUrl
8+ property bool canSimplifyText: true
9
10 property var securityStatus: null
11
12@@ -48,6 +49,8 @@
13
14 height: textField.height
15
16+ function selectAll() { textField.selectAll() }
17+
18 TextField {
19 id: textField
20 objectName: "addressBarTextField"
21@@ -84,7 +87,7 @@
22 height: parent.height
23 width: height
24
25- visible: addressbar.activeFocus || addressbar.loading || !addressbar.text
26+ visible: addressbar.activeFocus || addressbar.loading || !addressbar.text || !canSimplifyText
27
28 enabled: addressbar.text
29 opacity: enabled ? 1.0 : 0.3
30@@ -229,7 +232,7 @@
31 QtObject {
32 id: internal
33
34- readonly property bool idle: !addressbar.loading && !addressbar.activeFocus
35+ readonly property bool idle: !addressbar.loading && !addressbar.activeFocus && addressbar.canSimplifyText
36
37 readonly property int securityLevel: addressbar.securityStatus ? addressbar.securityStatus.securityLevel : Oxide.SecurityStatus.SecurityLevelNone
38 readonly property bool secureConnection: addressbar.securityStatus ? (securityLevel == Oxide.SecurityStatus.SecurityLevelSecure || securityLevel == Oxide.SecurityStatus.SecurityLevelSecureEV || securityLevel == Oxide.SecurityStatus.SecurityLevelWarning) : false
39@@ -289,16 +292,21 @@
40 return url
41 }
42 }
43- }
44
45- onActiveFocusChanged: {
46- if (activeFocus) {
47- text = actualUrl
48- } else if (!loading && actualUrl.toString()) {
49- text = internal.simplifyUrl(actualUrl)
50+ function updateUrlFromFocus() {
51+ if (canSimplifyText) {
52+ if (addressbar.activeFocus) {
53+ text = actualUrl
54+ } else if (!loading && actualUrl.toString()) {
55+ text = internal.simplifyUrl(actualUrl)
56+ }
57+ }
58 }
59 }
60
61+ onActiveFocusChanged: internal.updateUrlFromFocus()
62+ onCanSimplifyTextChanged: internal.updateUrlFromFocus()
63+
64 onActualUrlChanged: {
65 if (!activeFocus || !actualUrl.toString()) {
66 text = internal.simplifyUrl(actualUrl)
67
68=== modified file 'src/app/webbrowser/Browser.qml'
69--- src/app/webbrowser/Browser.qml 2015-06-02 14:26:38 +0000
70+++ src/app/webbrowser/Browser.qml 2015-06-16 16:15:45 +0000
71@@ -303,7 +303,10 @@
72 text: i18n.tr("History")
73 iconName: "history"
74 enabled: browser.historyModel
75- onTriggered: historyViewComponent.createObject(historyViewContainer)
76+ onTriggered: {
77+ historyViewComponent.createObject(historyViewContainer)
78+ historyViewContainer.focus = true
79+ }
80 },
81 Action {
82 objectName: "tabs"
83@@ -313,6 +316,7 @@
84 onTriggered: {
85 recentView.state = "shown"
86 recentToolbar.state = "shown"
87+ recentView.focus = true
88 }
89 },
90 Action {
91@@ -326,7 +330,10 @@
92 objectName: "settings"
93 text: i18n.tr("Settings")
94 iconName: "settings"
95- onTriggered: settingsComponent.createObject(settingsContainer)
96+ onTriggered: {
97+ settingsComponent.createObject(settingsContainer)
98+ settingsContainer.focus = true
99+ }
100 },
101 Action {
102 objectName: "privatemode"
103@@ -346,6 +353,10 @@
104 }
105 }
106 ]
107+
108+ addressBarCanSimplifyText: !(activeFocus || suggestionsList.activeFocus)
109+ Keys.onDownPressed: if (suggestionsList.count) suggestionsList.focus = true
110+ Keys.onEscapePressed: internal.resetFocus()
111 }
112
113 ChromeController {
114@@ -358,7 +369,7 @@
115
116 Suggestions {
117 id: suggestionsList
118- opacity: ((chrome.state == "shown") && chrome.activeFocus && (count > 0) && !chrome.drawerOpen) ? 1.0 : 0.0
119+ opacity: ((chrome.state == "shown") && (activeFocus || chrome.activeFocus) && count > 0 && !chrome.drawerOpen) ? 1.0 : 0.0
120 Behavior on opacity {
121 UbuntuNumberAnimation {}
122 }
123@@ -372,6 +383,9 @@
124
125 searchTerms: chrome.text.split(/\s+/g).filter(function(term) { return term.length > 0 })
126
127+ Keys.onUpPressed: chrome.focus = true
128+ Keys.onEscapePressed: internal.resetFocus()
129+
130 models: [historySuggestions,
131 bookmarksSuggestions,
132 searchSuggestions.limit(4)]
133@@ -404,7 +418,7 @@
134 id: searchSuggestions
135 terms: suggestionsList.searchTerms
136 searchEngine: currentSearchEngine
137- active: chrome.activeFocus &&
138+ active: (chrome.activeFocus || suggestionsList.activeFocus) &&
139 !browser.incognito &&
140 !UrlManagement.looksLikeAUrl(chrome.text.replace(/ /g, "+"))
141
142@@ -416,7 +430,7 @@
143 }
144 }
145
146- onSelected: {
147+ onActivated: {
148 browser.currentWebview.url = url
149 browser.currentWebview.forceActiveFocus()
150 chrome.requestedUrl = url
151@@ -426,6 +440,7 @@
152
153 FocusScope {
154 id: recentView
155+ objectName: "recentView"
156
157 anchors.fill: parent
158 visible: bottomEdgeHandle.dragging || tabslist.animating || (state == "shown")
159@@ -434,6 +449,13 @@
160 name: "shown"
161 }
162
163+ function closeAndSwitchToTab(index) {
164+ recentView.reset()
165+ internal.switchToTab(index)
166+ }
167+
168+ Keys.onEscapePressed: closeAndSwitchToTab(0)
169+
170 TabsList {
171 id: tabslist
172 anchors.fill: parent
173@@ -453,15 +475,7 @@
174 }
175 }
176 chromeOffset: chrome.height - invisibleTabChrome.height
177- onTabSelected: {
178- var tab = tabsModel.get(index)
179- if (tab) {
180- tab.load()
181- tab.forceActiveFocus()
182- tabslist.model.setCurrent(index)
183- }
184- recentView.reset()
185- }
186+ onTabSelected: recentView.closeAndSwitchToTab(index)
187 onTabClosed: {
188 var tab = tabsModel.remove(index)
189 if (tab) {
190@@ -497,10 +511,7 @@
191
192 text: i18n.tr("Done")
193
194- onClicked: {
195- recentView.reset()
196- tabsModel.currentTab.load()
197- }
198+ onClicked: recentView.closeAndSwitchToTab(0)
199 }
200
201 ToolbarAction {
202@@ -600,8 +611,9 @@
203 }
204 }
205
206- Item {
207+ FocusScope {
208 id: historyViewContainer
209+ objectName: "historyView"
210
211 visible: children.length > 0
212 anchors.fill: parent
213@@ -612,6 +624,12 @@
214 HistoryView {
215 anchors.fill: parent
216 visible: historyViewContainer.children.length == 1
217+ focus: true
218+
219+ Keys.onEscapePressed: {
220+ destroy()
221+ internal.resetFocus()
222+ }
223
224 Timer {
225 // Set the model asynchronously to ensure
226@@ -650,7 +668,7 @@
227 }
228 }
229
230- Item {
231+ FocusScope {
232 id: settingsContainer
233
234 visible: children.length > 0
235@@ -661,9 +679,14 @@
236
237 SettingsPage {
238 anchors.fill: parent
239+ focus: true
240 historyModel: browser.historyModel
241 settingsObject: settings
242 onDone: destroy()
243+ Keys.onEscapePressed: {
244+ destroy()
245+ internal.resetFocus()
246+ }
247 }
248 }
249 }
250@@ -920,9 +943,33 @@
251 }
252 }
253
254- function focusAddressBar() {
255+ function switchToTab(index) {
256+ var tab = tabsModel.get(index)
257+ if (tab) {
258+ tab.load()
259+ tabslist.model.setCurrent(index)
260+ if (tab.initialUrl == "" && formFactor == "desktop") focusAddressBar()
261+ else tab.forceActiveFocus()
262+ }
263+ }
264+
265+ function closeCurrentTab() {
266+ if (tabsModel.count > 0) {
267+ var tab = tabsModel.remove(0)
268+ if (tab) tab.close()
269+
270+ if (tabsModel.count === 0) {
271+ browser.openUrlInNewTab("", true)
272+ } else {
273+ internal.switchToTab(0)
274+ }
275+ }
276+ }
277+
278+ function focusAddressBar(selectContent) {
279 chrome.forceActiveFocus()
280 Qt.inputMethod.show() // work around http://pad.lv/1316057
281+ if (selectContent) chrome.addressBarSelectAll()
282 }
283
284 function resetFocus() {
285@@ -959,6 +1006,20 @@
286 }
287 return false
288 }
289+
290+ function historyGoBack() {
291+ if (currentWebview && currentWebview.canGoBack) {
292+ internal.resetFocus()
293+ currentWebview.goBack()
294+ }
295+ }
296+
297+ function historyGoForward() {
298+ if (currentWebview && currentWebview.canGoForward) {
299+ internal.resetFocus()
300+ currentWebview.goForward()
301+ }
302+ }
303 }
304
305 function openUrlInNewTab(url, setCurrent, load) {
306@@ -1128,4 +1189,135 @@
307 }
308 }
309 }
310+
311+ Keys.onPressed: if (shortcuts.processKey(event.key, event.modifiers)) event.accepted = true
312+ KeyboardShortcuts {
313+ id: shortcuts
314+
315+ // Ctrl + Tab: pull the tab from the bottom of the stack to the
316+ // top (i.e. make it current)
317+ KeyboardShortcut {
318+ modifiers: Qt.ControlModifier
319+ key: Qt.Key_Tab
320+ enabled: chrome.visible || recentView.visible
321+ onTriggered: {
322+ internal.switchToTab(tabsModel.count - 1)
323+ if (chrome.visible) recentView.reset()
324+ else if (recentView.visible) recentView.focus = true
325+ }
326+ }
327+
328+ // Ctrl + w or Ctrl+F4: Close the current tab
329+ KeyboardShortcut {
330+ modifiers: Qt.ControlModifier
331+ key: Qt.Key_W
332+ enabled: chrome.visible || recentView.visible
333+ onTriggered: internal.closeCurrentTab()
334+ }
335+ KeyboardShortcut {
336+ modifiers: Qt.ControlModifier
337+ key: Qt.Key_F4
338+ enabled: chrome.visible || recentView.visible
339+ onTriggered: internal.closeCurrentTab()
340+ }
341+
342+ // Ctrl + t: Open a new Tab
343+ KeyboardShortcut {
344+ modifiers: Qt.ControlModifier
345+ key: Qt.Key_T
346+ enabled: chrome.visible || recentView.visible
347+ onTriggered: {
348+ openUrlInNewTab("", true)
349+ if (recentView.visible) recentView.reset()
350+ }
351+ }
352+
353+ // F6 or Ctrl + L or Alt + D: Select the content in the address bar
354+ KeyboardShortcut {
355+ modifiers: Qt.ControlModifier
356+ key: Qt.Key_L
357+ enabled: chrome.visible
358+ onTriggered: internal.focusAddressBar(true)
359+ }
360+ KeyboardShortcut {
361+ modifiers: Qt.AltModifier
362+ key: Qt.Key_D
363+ enabled: chrome.visible
364+ onTriggered: internal.focusAddressBar(true)
365+ }
366+ KeyboardShortcut {
367+ key: Qt.Key_F6
368+ enabled: chrome.visible
369+ onTriggered: internal.focusAddressBar(true)
370+ }
371+
372+ // Ctrl + D: Toggle bookmarked state on current Tab
373+ KeyboardShortcut {
374+ modifiers: Qt.ControlModifier
375+ key: Qt.Key_D
376+ enabled: chrome.visible
377+ onTriggered: {
378+ if (currentWebview) {
379+ if (bookmarksModel.contains(currentWebview.url)) {
380+ bookmarksModel.remove(currentWebview.url)
381+ } else {
382+ bookmarksModel.add(currentWebview.url, currentWebview.title, currentWebview.icon)
383+ }
384+ }
385+ }
386+ }
387+
388+ // Ctrl + H: Show History
389+ KeyboardShortcut {
390+ modifiers: Qt.ControlModifier
391+ key: Qt.Key_H
392+ enabled: chrome.visible
393+ onTriggered: {
394+ if (historyViewContainer.children.length === 0) {
395+ historyViewComponent.createObject(historyViewContainer)
396+ historyViewContainer.focus = true
397+ }
398+ }
399+ }
400+
401+ // Alt + Left Arrow or Backspace: Goes to the previous page in history
402+ KeyboardShortcut {
403+ modifiers: Qt.AltModifier
404+ key: Qt.Key_Left
405+ enabled: chrome.visible
406+ onTriggered: internal.historyGoBack()
407+ }
408+ KeyboardShortcut {
409+ key: Qt.Key_Backspace
410+ enabled: chrome.visible
411+ onTriggered: internal.historyGoBack()
412+ }
413+
414+ // Alt + Right Arrow or Shift + Backspace: Goes to the next page in history
415+ KeyboardShortcut {
416+ modifiers: Qt.AltModifier
417+ key: Qt.Key_Right
418+ enabled: chrome.visible
419+ onTriggered: internal.historyGoForward()
420+ }
421+ KeyboardShortcut {
422+ modifiers: Qt.ShiftModifier
423+ key: Qt.Key_Backspace
424+ enabled: chrome.visible
425+ onTriggered: internal.historyGoForward()
426+ }
427+
428+ // F5 or Ctrl + R: Reload current Tab
429+ KeyboardShortcut {
430+ key: Qt.Key_F5
431+ enabled: chrome.visible
432+ onTriggered: if (currentWebview) currentWebview.reload()
433+ }
434+ KeyboardShortcut {
435+ modifiers: Qt.ControlModifier
436+ key: Qt.Key_R
437+ enabled: chrome.visible
438+ onTriggered: if (currentWebview) currentWebview.reload()
439+ }
440+ }
441 }
442
443=== modified file 'src/app/webbrowser/Chrome.qml'
444--- src/app/webbrowser/Chrome.qml 2015-05-26 21:42:54 +0000
445+++ src/app/webbrowser/Chrome.qml 2015-06-16 16:15:45 +0000
446@@ -29,10 +29,13 @@
447 property list<Action> drawerActions
448 readonly property bool drawerOpen: internal.openDrawer
449 property alias requestedUrl: addressbar.requestedUrl
450+ property alias addressBarCanSimplifyText: addressbar.canSimplifyText
451 property alias incognito: addressbar.incognito
452
453 backgroundColor: incognito ? UbuntuColors.darkGrey : Theme.palette.normal.background
454
455+ function addressBarSelectAll() { addressbar.selectAll() }
456+
457 FocusScope {
458 anchors {
459 fill: parent
460
461=== added file 'src/app/webbrowser/KeyboardShortcut.qml'
462--- src/app/webbrowser/KeyboardShortcut.qml 1970-01-01 00:00:00 +0000
463+++ src/app/webbrowser/KeyboardShortcut.qml 2015-06-16 16:15:45 +0000
464@@ -0,0 +1,25 @@
465+/*
466+ * Copyright 2015 Canonical Ltd.
467+ *
468+ * This file is part of webbrowser-app.
469+ *
470+ * webbrowser-app is free software; you can redistribute it and/or modify
471+ * it under the terms of the GNU General Public License as published by
472+ * the Free Software Foundation; version 3.
473+ *
474+ * webbrowser-app is distributed in the hope that it will be useful,
475+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
476+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
477+ * GNU General Public License for more details.
478+ *
479+ * You should have received a copy of the GNU General Public License
480+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
481+ */
482+
483+import QtQuick 2.0
484+import Ubuntu.Components 1.1
485+
486+Action {
487+ property int key
488+ property int modifiers: Qt.NoModifier
489+}
490
491=== added file 'src/app/webbrowser/KeyboardShortcuts.qml'
492--- src/app/webbrowser/KeyboardShortcuts.qml 1970-01-01 00:00:00 +0000
493+++ src/app/webbrowser/KeyboardShortcuts.qml 2015-06-16 16:15:45 +0000
494@@ -0,0 +1,41 @@
495+/*
496+ * Copyright 2015 Canonical Ltd.
497+ *
498+ * This file is part of webbrowser-app.
499+ *
500+ * webbrowser-app is free software; you can redistribute it and/or modify
501+ * it under the terms of the GNU General Public License as published by
502+ * the Free Software Foundation; version 3.
503+ *
504+ * webbrowser-app is distributed in the hope that it will be useful,
505+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
506+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
507+ * GNU General Public License for more details.
508+ *
509+ * You should have received a copy of the GNU General Public License
510+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
511+ */
512+
513+import QtQuick 2.0
514+
515+Item {
516+ function processKey(key, modifiers) {
517+ for (var i = 0; i < data.length; i++) {
518+ var shortcut = data[i];
519+
520+ if (!shortcut.enabled) continue
521+ if (key !== shortcut.key) continue
522+
523+ if (shortcut.modifiers === Qt.NoModifier) {
524+ if (modifiers === Qt.NoModifier) {
525+ shortcut.trigger()
526+ return true
527+ }
528+ } else if ((modifiers & shortcut.modifiers) === shortcut.modifiers) {
529+ shortcut.trigger()
530+ return true
531+ }
532+ }
533+ return false
534+ }
535+}
536
537=== modified file 'src/app/webbrowser/Suggestion.qml'
538--- src/app/webbrowser/Suggestion.qml 2015-04-22 11:00:02 +0000
539+++ src/app/webbrowser/Suggestion.qml 2015-06-16 16:15:45 +0000
540@@ -28,7 +28,7 @@
541 property alias icon: icon.name
542 property url url
543
544- signal selected(url url)
545+ signal activated(url url)
546
547 __height: Math.max(middleVisuals.height, units.gu(6))
548 // disable focus handling
549@@ -77,8 +77,9 @@
550 fontSize: "small"
551 elide: Text.ElideRight
552 visible: text !== ""
553+ color: selected ? "#DB4923" : "black"
554 }
555 }
556
557- onClicked: selected(url)
558+ onClicked: activated(url)
559 }
560
561=== modified file 'src/app/webbrowser/Suggestions.qml'
562--- src/app/webbrowser/Suggestions.qml 2015-05-12 09:56:40 +0000
563+++ src/app/webbrowser/Suggestions.qml 2015-06-16 16:15:45 +0000
564@@ -19,20 +19,24 @@
565 import QtQuick 2.0
566 import Ubuntu.Components 1.1
567
568-Rectangle {
569+FocusScope {
570 id: suggestions
571
572 property var searchTerms
573 property var models
574+
575 readonly property int count: models.reduce(countItems, 0)
576 readonly property alias contentHeight: suggestionsList.contentHeight
577
578- signal selected(url url)
579+ signal activated(url url)
580
581- radius: units.gu(0.5)
582- border {
583- color: "#dedede"
584- width: 1
585+ Rectangle {
586+ anchors.fill: parent
587+ radius: units.gu(0.5)
588+ border {
589+ color: "#dedede"
590+ width: 1
591+ }
592 }
593
594 clip: true
595@@ -40,39 +44,37 @@
596 ListView {
597 id: suggestionsList
598 anchors.fill: parent
599-
600- model: suggestions.models
601- delegate: Column {
602- id: suggestionsSection
603+ focus: true
604+
605+ model: models.reduce(function(list, model) {
606+ var modelItems = [];
607+
608+ // Models inheriting from QAbstractItemModel and JS arrays expose their
609+ // data differently, so we need to collect their items differently
610+ if (model.forEach) {
611+ model.forEach(function(item) { modelItems.push(item) })
612+ } else {
613+ for (var i = 0; i < model.count; i++) modelItems.push(model.get(i))
614+ }
615+
616+ modelItems.forEach(function(item) {
617+ item["icon"] = model.icon
618+ item["displayUrl"] = model.displayUrl
619+ list.push(item);
620+ })
621+ return list;
622+ }, [])
623+
624+ delegate: Suggestion {
625 width: suggestionsList.width
626- height: childrenRect.height
627-
628- property string icon: models[index].icon
629- property bool displayUrl: models[index].displayUrl
630- property int firstItemIndex: models.slice(0, index).reduce(countItems, 0)
631-
632- Repeater {
633- id: suggestionsSource
634- model: modelData
635-
636- delegate: Suggestion {
637- id: suggestion
638- width: suggestionsList.width
639- showDivider: suggestionsSection.firstItemIndex + index <
640- suggestions.count - 1
641-
642- // Necessary to support both using objects inheriting from
643- // QAbstractItemModel and JS arrays as models, since they
644- // expose their data differently
645- property var item: (model.modelData) ? model.modelData : model
646-
647- title: highlightTerms(item.title)
648- subtitle: suggestionsSection.displayUrl ? highlightTerms(item.url) : ""
649- icon: suggestionsSection.icon
650-
651- onSelected: suggestions.selected(item.url)
652- }
653- }
654+ showDivider: index < model.length - 1
655+
656+ title: highlightTerms(modelData.title)
657+ subtitle: modelData.displayUrl ? highlightTerms(modelData.url) : ""
658+ icon: modelData.icon
659+ selected: suggestionsList.activeFocus && ListView.isCurrentItem
660+
661+ onActivated: suggestions.activated(modelData.url)
662 }
663 }
664
665
666=== modified file 'src/app/webbrowser/limit-proxy-model.cpp'
667--- src/app/webbrowser/limit-proxy-model.cpp 2014-07-04 12:30:09 +0000
668+++ src/app/webbrowser/limit-proxy-model.cpp 2015-06-16 16:15:45 +0000
669@@ -266,3 +266,19 @@
670 m_dataChangedEnd = -1;
671 }
672 }
673+
674+QVariantMap LimitProxyModel::get(int i) const
675+{
676+ QVariantMap item;
677+ QHash<int, QByteArray> roles = roleNames();
678+
679+ QModelIndex modelIndex = index(i, 0);
680+ if (modelIndex.isValid()) {
681+ Q_FOREACH(int role, roles.keys()) {
682+ QString roleName = QString::fromUtf8(roles.value(role));
683+ item.insert(roleName, data(modelIndex, role));
684+ }
685+ }
686+ return item;
687+}
688+
689
690=== modified file 'src/app/webbrowser/limit-proxy-model.h'
691--- src/app/webbrowser/limit-proxy-model.h 2014-06-27 20:29:52 +0000
692+++ src/app/webbrowser/limit-proxy-model.h 2015-06-16 16:15:45 +0000
693@@ -45,6 +45,8 @@
694 int rowCount(const QModelIndex &parent = QModelIndex()) const;
695 int unlimitedRowCount(const QModelIndex &parent = QModelIndex()) const;
696
697+ Q_INVOKABLE QVariantMap get(int index) const;
698+
699 Q_SIGNALS:
700 void sourceModelChanged() const;
701 void limitChanged() const;
702
703=== modified file 'tests/autopilot/webbrowser_app/emulators/browser.py'
704--- tests/autopilot/webbrowser_app/emulators/browser.py 2015-05-28 13:56:52 +0000
705+++ tests/autopilot/webbrowser_app/emulators/browser.py 2015-06-16 16:15:45 +0000
706@@ -19,7 +19,7 @@
707 import autopilot.logging
708 import ubuntuuitoolkit as uitk
709 from autopilot import exceptions
710-
711+from autopilot import input
712
713 logger = logging.getLogger(__name__)
714
715@@ -30,6 +30,7 @@
716 super().__init__(*args)
717 self.chrome = self._get_chrome()
718 self.address_bar = self.chrome.address_bar
719+ self.keyboard = input.Keyboard.create()
720
721 def _get_chrome(self):
722 return self.select_single(Chrome)
723@@ -150,6 +151,17 @@
724 def get_bottom_edge_hint(self):
725 return self.select_single("QQuickImage", objectName="bottomEdgeHint")
726
727+ # The history view is dynamically created, so it might or might not be
728+ # available
729+ def get_history_view(self):
730+ try:
731+ return self.select_single("HistoryView")
732+ except exceptions.StateNotFoundError:
733+ return None
734+
735+ def press_key(self, key):
736+ self.keyboard.press_and_release(key)
737+
738
739 class Chrome(uitk.UbuntuUIToolkitCustomProxyObjectBase):
740
741@@ -224,11 +236,14 @@
742 @autopilot.logging.log_action(logger.info)
743 def go_to_url(self, url):
744 self.write(url)
745- self.text_field.keyboard.press_and_release('Enter')
746+ self.press_key('Enter')
747
748 def write(self, text, clear=True):
749 self.text_field.write(text, clear)
750
751+ def press_key(self, key):
752+ self.text_field.keyboard.press_and_release(key)
753+
754 @autopilot.logging.log_action(logger.info)
755 def click_action_button(self):
756 button = self.select_single("QQuickMouseArea",
757
758=== modified file 'tests/autopilot/webbrowser_app/tests/__init__.py'
759--- tests/autopilot/webbrowser_app/tests/__init__.py 2015-05-25 19:17:25 +0000
760+++ tests/autopilot/webbrowser_app/tests/__init__.py 2015-06-16 16:15:45 +0000
761@@ -169,6 +169,14 @@
762 self.pointing_device.click_object(settings_action)
763 return self.main_window.get_settings_page()
764
765+ def open_history(self):
766+ chrome = self.main_window.chrome
767+ drawer_button = chrome.get_drawer_button()
768+ self.pointing_device.click_object(drawer_button)
769+ chrome.get_drawer()
770+ settings_action = chrome.get_drawer_action("history")
771+ self.pointing_device.click_object(settings_action)
772+
773 def assert_number_webviews_eventually(self, count):
774 self.assertThat(lambda: len(self.main_window.get_webviews()),
775 Eventually(Equals(count)))
776@@ -195,7 +203,7 @@
777 are executed, thus making them more robust.
778 """
779
780- def setUp(self):
781+ def setUp(self, path="/test1"):
782 self.http_server = http_server.HTTPServerInAThread()
783 self.ping_server(self.http_server)
784 self.addCleanup(self.http_server.cleanup)
785@@ -203,7 +211,7 @@
786 'UBUNTU_WEBVIEW_HOST_MAPPING_RULES',
787 "MAP test:80 localhost:{}".format(self.http_server.port)))
788 self.base_url = "http://test"
789- self.url = self.base_url + "/test1"
790+ self.url = self.base_url + path
791 self.ARGS = self.ARGS + [self.url]
792 super(StartOpenRemotePageTestCaseBase, self).setUp()
793 self.assert_home_page_eventually_loaded()
794
795=== modified file 'tests/autopilot/webbrowser_app/tests/http_server.py'
796--- tests/autopilot/webbrowser_app/tests/http_server.py 2015-04-23 15:34:16 +0000
797+++ tests/autopilot/webbrowser_app/tests/http_server.py 2015-06-16 16:15:45 +0000
798@@ -140,6 +140,10 @@
799 if query in self.suggestions_data:
800 suggestions = self.suggestions_data[query]
801 self.wfile.write(json.dumps(suggestions).encode())
802+ elif self.path.startswith("/tab/"):
803+ self.send_response(200)
804+ name = self.path[len("/tab/"):]
805+ self.send_html('<html><body>' + name + '</body></html>')
806 else:
807 self.send_error(404)
808
809
810=== modified file 'tests/autopilot/webbrowser_app/tests/test_addressbar_bookmark.py'
811--- tests/autopilot/webbrowser_app/tests/test_addressbar_bookmark.py 2015-04-27 03:23:00 +0000
812+++ tests/autopilot/webbrowser_app/tests/test_addressbar_bookmark.py 2015-06-16 16:15:45 +0000
813@@ -60,12 +60,10 @@
814 self.pointing_device.click_object(webview)
815 address_bar = self.main_window.address_bar
816 bookmark_toggle = address_bar.get_bookmark_toggle()
817- self.assertThat(address_bar.activeFocus, Eventually(Equals(False)))
818 self.assertThat(bookmark_toggle.visible, Eventually(Equals(True)))
819
820 self.open_tabs_view()
821 tabs_view = self.main_window.get_tabs_view()
822 self.main_window.get_tabs_view().get_previews()[1].select()
823 tabs_view.visible.wait_for(False)
824- self.assertThat(address_bar.activeFocus, Equals(False))
825 self.assertThat(bookmark_toggle.visible, Eventually(Equals(False)))
826
827=== added file 'tests/autopilot/webbrowser_app/tests/test_keyboard.py'
828--- tests/autopilot/webbrowser_app/tests/test_keyboard.py 1970-01-01 00:00:00 +0000
829+++ tests/autopilot/webbrowser_app/tests/test_keyboard.py 2015-06-16 16:15:45 +0000
830@@ -0,0 +1,276 @@
831+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
832+#
833+# Copyright 2015 Canonical
834+#
835+# This program is free software: you can redistribute it and/or modify it
836+# under the terms of the GNU General Public License version 3, as published
837+# by the Free Software Foundation.
838+#
839+# This program is distributed in the hope that it will be useful,
840+# but WITHOUT ANY WARRANTY; without even the implied warranty of
841+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
842+# GNU General Public License for more details.
843+#
844+# You should have received a copy of the GNU General Public License
845+# along with this program. If not, see <http://www.gnu.org/licenses/>.
846+
847+import os
848+import sqlite3
849+import time
850+import testtools
851+
852+from testtools.matchers import Equals, NotEquals, GreaterThan
853+from autopilot.matchers import Eventually
854+from autopilot.platform import model
855+
856+from webbrowser_app.tests import StartOpenRemotePageTestCaseBase
857+
858+
859+class PrepopulatedDatabaseTestCaseBase(StartOpenRemotePageTestCaseBase):
860+
861+ """Helper test class that pre-populates history and bookmarks databases."""
862+
863+ def setUp(self):
864+ self.create_temporary_profile()
865+ self.populate_bookmarks()
866+ super(PrepopulatedDatabaseTestCaseBase, self).setUp("/tab/0")
867+
868+ def populate_bookmarks(self):
869+ db_path = os.path.join(self.data_location, "bookmarks.sqlite")
870+ connection = sqlite3.connect(db_path)
871+ connection.execute("""CREATE TABLE IF NOT EXISTS bookmarks
872+ (url VARCHAR, title VARCHAR, icon VARCHAR,
873+ created INTEGER);""")
874+ rows = [
875+ ("http://www.rsc.org/periodic-table/element/77/iridium",
876+ "Iridium - Element Information")
877+ ]
878+
879+ for i, row in enumerate(rows):
880+ timestamp = int(time.time()) - i * 10
881+ query = "INSERT INTO bookmarks \
882+ VALUES ('{}', '{}', '', {});"
883+ query = query.format(row[0], row[1], timestamp)
884+ connection.execute(query)
885+
886+ connection.commit()
887+ connection.close()
888+
889+
890+@testtools.skipIf(model() != "Desktop", "on desktop only")
891+class TestKeyboard(PrepopulatedDatabaseTestCaseBase):
892+
893+ """Test keyboard interaction"""
894+
895+ def setUp(self):
896+ super(TestKeyboard, self).setUp()
897+ self.address_bar = self.main_window.address_bar
898+
899+ def open_tab(self, url):
900+ self.main_window.press_key('Ctrl+T')
901+ new_tab_view = self.main_window.get_new_tab_view()
902+ self.address_bar.go_to_url(url)
903+ new_tab_view.wait_until_destroyed()
904+ self.assertThat(lambda: self.main_window.get_current_webview().url,
905+ Eventually(Equals(url)))
906+
907+ # Name tabs starting from 1 by default because tab 0 has been opened
908+ # already via StartOpenRemotePageTestCaseBase
909+ def open_tabs(self, count, base=1):
910+ for i in range(0, count):
911+ self.open_tab(self.base_url + "/tab/" + str(i + base))
912+
913+ def check_tab_number(self, number):
914+ url = self.base_url + "/tab/" + str(number)
915+ self.assertThat(lambda: self.main_window.get_current_webview().url,
916+ Eventually(Equals(url)))
917+
918+ def test_new_tab(self):
919+ self.main_window.press_key('Ctrl+T')
920+
921+ webview = self.main_window.get_current_webview()
922+ self.assertThat(webview.url, Equals(""))
923+ new_tab_view = self.main_window.get_new_tab_view()
924+ self.assertThat(new_tab_view.visible, Eventually(Equals(True)))
925+
926+ def test_switch_tabs(self):
927+ self.open_tabs(2)
928+ self.check_tab_number(2)
929+ self.main_window.press_key('Ctrl+Tab')
930+ self.check_tab_number(0)
931+ self.main_window.press_key('Ctrl+Tab')
932+ self.check_tab_number(1)
933+ self.main_window.press_key('Ctrl+Tab')
934+ self.check_tab_number(2)
935+
936+ def test_can_switch_tabs_after_suggestions_escape(self):
937+ self.open_tabs(1)
938+ self.check_tab_number(1)
939+
940+ suggestions = self.main_window.get_suggestions()
941+ self.address_bar.write('el')
942+ self.assertThat(suggestions.opacity, Eventually(Equals(1)))
943+ self.main_window.press_key('Down')
944+ self.assertThat(suggestions.activeFocus, Eventually(Equals(True)))
945+
946+ self.main_window.press_key('Escape')
947+ self.assertThat(suggestions.opacity, Eventually(Equals(0)))
948+
949+ self.main_window.press_key('Ctrl+Tab')
950+ self.check_tab_number(0)
951+
952+ def test_switch_tabs_from_tabs_view(self):
953+ self.open_tabs(1)
954+ self.check_tab_number(1)
955+ tabs = self.open_tabs_view()
956+ self.main_window.press_key('Ctrl+Tab')
957+ self.check_tab_number(0)
958+ self.main_window.press_key('Ctrl+Tab')
959+ self.check_tab_number(1)
960+ self.main_window.press_key('Ctrl+Tab')
961+ self.check_tab_number(0)
962+ self.main_window.press_key('Escape')
963+ self.assertThat(tabs.visible, Eventually(Equals(False)))
964+ self.check_tab_number(0)
965+
966+ def test_close_tabs_ctrl_f4(self):
967+ self.open_tabs(1)
968+ self.check_tab_number(1)
969+ self.main_window.press_key('Ctrl+F4')
970+ self.check_tab_number(0)
971+ self.main_window.press_key('Ctrl+F4')
972+ webview = self.main_window.get_current_webview()
973+ self.assertThat(webview.url, Equals(""))
974+
975+ def test_close_tabs_ctrl_w(self):
976+ self.open_tabs(1)
977+ self.check_tab_number(1)
978+ self.main_window.press_key('Ctrl+w')
979+ self.check_tab_number(0)
980+ self.main_window.press_key('Ctrl+w')
981+ webview = self.main_window.get_current_webview()
982+ self.assertThat(webview.url, Equals(""))
983+
984+ def test_close_tabs_tabs_view(self):
985+ self.open_tabs(1)
986+ self.check_tab_number(1)
987+ self.open_tabs_view()
988+ self.main_window.press_key('Ctrl+w')
989+ self.check_tab_number(0)
990+ self.main_window.press_key('Ctrl+F4')
991+ webview = self.main_window.get_current_webview()
992+ self.assertThat(webview.url, Equals(""))
993+
994+ def test_select_address_bar_ctrl_l(self):
995+ self.main_window.press_key('Ctrl+L')
996+ self.assertThat(self.address_bar.text_field.selectedText,
997+ Eventually(Equals(self.address_bar.text_field.text)))
998+
999+ def test_select_address_bar_alt_d(self):
1000+ self.main_window.press_key('Alt+D')
1001+ self.assertThat(self.address_bar.text_field.selectedText,
1002+ Eventually(Equals(self.address_bar.text_field.text)))
1003+
1004+ def test_select_address_bar_f6(self):
1005+ self.main_window.press_key('F6')
1006+ self.assertThat(self.address_bar.text_field.selectedText,
1007+ Eventually(Equals(self.address_bar.text_field.text)))
1008+
1009+ def test_escape_from_address_bar(self):
1010+ self.main_window.press_key('Alt+D')
1011+ self.assertThat(self.address_bar.text_field.selectedText,
1012+ Eventually(Equals(self.address_bar.text_field.text)))
1013+ self.main_window.press_key('Escape')
1014+ self.assertThat(self.address_bar.text_field.selectedText,
1015+ Eventually(Equals("")))
1016+ self.assertThat(self.address_bar.activeFocus,
1017+ Eventually(Equals(False)))
1018+ webview = self.main_window.get_current_webview()
1019+ self.assertThat(webview.activeFocus, Eventually(Equals(True)))
1020+
1021+ def test_reload(self):
1022+ webview = self.main_window.get_current_webview()
1023+ self.assertThat(webview.loading, Eventually(Equals(False)))
1024+
1025+ watcher = webview.watch_signal('loadingStateChanged()')
1026+ previous = watcher.num_emissions
1027+
1028+ self.main_window.press_key('Ctrl+R')
1029+ self.assertThat(
1030+ lambda: watcher.num_emissions,
1031+ Eventually(GreaterThan(previous)))
1032+
1033+ self.assertThat(webview.loading, Eventually(Equals(False)))
1034+
1035+ previous = watcher.num_emissions
1036+
1037+ self.main_window.press_key('F5')
1038+ self.assertThat(
1039+ lambda: watcher.num_emissions,
1040+ Eventually(GreaterThan(previous)))
1041+
1042+ self.assertThat(webview.loading, Eventually(Equals(False)))
1043+
1044+ def test_bookmark(self):
1045+ chrome = self.main_window.chrome
1046+ self.assertThat(chrome.bookmarked, Equals(False))
1047+ self.main_window.press_key('Ctrl+D')
1048+ self.assertThat(chrome.bookmarked, Eventually(Equals(True)))
1049+ self.main_window.press_key('Ctrl+D')
1050+ self.assertThat(chrome.bookmarked, Eventually(Equals(False)))
1051+
1052+ def test_history_navigation_with_alt_arrows(self):
1053+ previous = self.main_window.get_current_webview().url
1054+ url = self.base_url + "/test2"
1055+ self.main_window.go_to_url(url)
1056+ self.main_window.wait_until_page_loaded(url)
1057+
1058+ self.main_window.press_key('Alt+Left')
1059+ self.assertThat(lambda: self.main_window.get_current_webview().url,
1060+ Eventually(Equals(previous)))
1061+
1062+ self.main_window.press_key('Alt+Right')
1063+ self.assertThat(lambda: self.main_window.get_current_webview().url,
1064+ Eventually(Equals(url)))
1065+
1066+ def test_history_navigation_with_backspace(self):
1067+ previous = self.main_window.get_current_webview().url
1068+ url = self.base_url + "/test2"
1069+ self.main_window.go_to_url(url)
1070+ self.main_window.wait_until_page_loaded(url)
1071+
1072+ self.main_window.press_key('Backspace')
1073+ self.assertThat(lambda: self.main_window.get_current_webview().url,
1074+ Eventually(Equals(previous)))
1075+
1076+ self.main_window.press_key('Shift+Backspace')
1077+ self.assertThat(lambda: self.main_window.get_current_webview().url,
1078+ Eventually(Equals(url)))
1079+
1080+ def test_toggle_history(self):
1081+ self.assertThat(self.main_window.get_history_view(), Equals(None))
1082+ self.main_window.press_key('Ctrl+H')
1083+ self.assertThat(lambda: self.main_window.get_history_view(),
1084+ Eventually(NotEquals(None)))
1085+ history_view = self.main_window.get_history_view()
1086+
1087+ self.main_window.press_key('Escape')
1088+ history_view.wait_until_destroyed()
1089+ webview = self.main_window.get_current_webview()
1090+ self.assertThat(webview.activeFocus, Eventually(Equals(True)))
1091+
1092+ def test_toggle_history_from_menu(self):
1093+ self.assertThat(self.main_window.get_history_view(), Equals(None))
1094+ self.open_history()
1095+ history_view = self.main_window.get_history_view()
1096+ self.assertThat(history_view.activeFocus, Eventually(Equals(True)))
1097+
1098+ self.main_window.press_key('Escape')
1099+ history_view.wait_until_destroyed()
1100+
1101+ def test_escape_settings(self):
1102+ settings = self.open_settings()
1103+ self.main_window.press_key('Escape')
1104+ settings.wait_until_destroyed()
1105+ webview = self.main_window.get_current_webview()
1106+ self.assertThat(webview.activeFocus, Eventually(Equals(True)))
1107
1108=== modified file 'tests/autopilot/webbrowser_app/tests/test_suggestions.py'
1109--- tests/autopilot/webbrowser_app/tests/test_suggestions.py 2015-05-20 05:45:29 +0000
1110+++ tests/autopilot/webbrowser_app/tests/test_suggestions.py 2015-06-16 16:15:45 +0000
1111@@ -18,9 +18,11 @@
1112 import random
1113 import sqlite3
1114 import time
1115+import unittest
1116
1117-from testtools.matchers import Contains, Equals
1118+from testtools.matchers import Contains, Equals, GreaterThan
1119 from autopilot.matchers import Eventually
1120+from autopilot.platform import model
1121
1122 from webbrowser_app.tests import StartOpenRemotePageTestCaseBase
1123 from . import http_server
1124@@ -170,10 +172,9 @@
1125 def test_show_list_of_suggestions(self):
1126 suggestions = self.main_window.get_suggestions()
1127 self.assert_suggestions_eventually_hidden()
1128- self.assert_suggestions_eventually_hidden()
1129 self.address_bar.focus()
1130 self.assert_suggestions_eventually_shown()
1131- self.assertThat(suggestions.count, Eventually(Equals(1)))
1132+ self.assertThat(suggestions.count, Eventually(GreaterThan(0)))
1133 self.address_bar.clear()
1134 self.assert_suggestions_eventually_hidden()
1135
1136@@ -303,3 +304,67 @@
1137 highlighted = self.highlight_term("highlight", "high")
1138 self.assertThat(entries[0].title, Equals(highlighted))
1139 self.assertThat(entries[0].subtitle, Equals(''))
1140+
1141+ @unittest.skipIf(model() != "Desktop", "on desktop only")
1142+ def test_keyboard_navigation(self):
1143+ suggestions = self.main_window.get_suggestions()
1144+ address_bar = self.address_bar
1145+ address_bar.write('element')
1146+ self.assert_suggestions_eventually_shown()
1147+ self.assertThat(suggestions.count, Eventually(Equals(2)))
1148+ entries = suggestions.get_ordered_entries()
1149+ self.assertThat(entries[0].selected, Equals(False))
1150+ self.assertThat(entries[1].selected, Equals(False))
1151+
1152+ address_bar.press_key('Down')
1153+ self.assertThat(address_bar.activeFocus, Eventually(Equals(False)))
1154+ self.assertThat(suggestions.activeFocus, Eventually(Equals(True)))
1155+ self.assertThat(entries[0].selected, Equals(True))
1156+
1157+ self.main_window.press_key('Down')
1158+ self.assertThat(entries[0].selected, Equals(False))
1159+ self.assertThat(entries[1].selected, Equals(True))
1160+
1161+ # verify that selection does not wrap around
1162+ self.main_window.press_key('Down')
1163+ self.assertThat(entries[0].selected, Equals(False))
1164+ self.assertThat(entries[1].selected, Equals(True))
1165+
1166+ self.main_window.press_key('Up')
1167+ self.assertThat(entries[0].selected, Equals(True))
1168+ self.assertThat(entries[1].selected, Equals(False))
1169+
1170+ self.main_window.press_key('Up')
1171+ self.assertThat(address_bar.activeFocus, Eventually(Equals(True)))
1172+ self.assertThat(suggestions.activeFocus, Eventually(Equals(False)))
1173+ self.assertThat(entries[0].selected, Equals(False))
1174+ self.assertThat(entries[1].selected, Equals(False))
1175+
1176+ @unittest.skipIf(model() != "Desktop", "on desktop only")
1177+ def test_suggestions_escape(self):
1178+ suggestions = self.main_window.get_suggestions()
1179+ previous_text = self.address_bar.text
1180+ self.address_bar.write('element')
1181+ self.assert_suggestions_eventually_shown()
1182+ self.main_window.press_key('Down')
1183+ self.assertThat(suggestions.activeFocus, Eventually(Equals(True)))
1184+ self.assertThat(self.address_bar.text, Equals("element"))
1185+
1186+ self.main_window.press_key('Escape')
1187+ self.assert_suggestions_eventually_hidden()
1188+ self.assertThat(self.address_bar.text, Equals(previous_text))
1189+
1190+ @unittest.skipIf(model() != "Desktop", "on desktop only")
1191+ def test_suggestions_escape_on_addressbar(self):
1192+ suggestions = self.main_window.get_suggestions()
1193+ previous_text = self.address_bar.text
1194+ self.address_bar.write('element')
1195+ self.assert_suggestions_eventually_shown()
1196+ self.main_window.press_key('Down')
1197+ self.assertThat(suggestions.activeFocus, Eventually(Equals(True)))
1198+ self.main_window.press_key('Up')
1199+ self.assertThat(suggestions.activeFocus, Eventually(Equals(False)))
1200+
1201+ self.main_window.press_key('Escape')
1202+ self.assert_suggestions_eventually_hidden()
1203+ self.assertThat(self.address_bar.text, Equals(previous_text))
1204
1205=== modified file 'tests/unittests/limit-proxy-model/tst_LimitProxyModelTests.cpp'
1206--- tests/unittests/limit-proxy-model/tst_LimitProxyModelTests.cpp 2015-05-19 10:58:04 +0000
1207+++ tests/unittests/limit-proxy-model/tst_LimitProxyModelTests.cpp 2015-06-16 16:15:45 +0000
1208@@ -157,6 +157,28 @@
1209 QCOMPARE(model->unlimitedRowCount(), 3);
1210 QCOMPARE(model->rowCount(), 2);
1211 }
1212+
1213+ void shouldGetItemWithCorrectValues()
1214+ {
1215+ history->add(QUrl("http://example1.org/"), "Example 1 Domain", QUrl());
1216+
1217+ QVariantMap item = model->get(0);
1218+ QHash<int, QByteArray> roles = model->roleNames();
1219+
1220+ QCOMPARE(roles.count(), item.count());
1221+
1222+ Q_FOREACH(int role, roles.keys()) {
1223+ QString roleName = QString::fromUtf8(roles.value(role));
1224+ QCOMPARE(model->data(model->index(0, 0), role), item.value(roleName));
1225+ }
1226+ }
1227+
1228+ void shouldReturnEmptyItemIfGetOutOfBounds()
1229+ {
1230+ QVariantMap item = model->get(1);
1231+ QVERIFY(item.isEmpty());
1232+ }
1233+
1234 };
1235
1236 QTEST_MAIN(LimitProxyModelTests)

Subscribers

People subscribed via source and target branches

to status/vote changes: