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
=== modified file 'src/app/webbrowser/AddressBar.qml'
--- src/app/webbrowser/AddressBar.qml 2015-05-26 21:42:54 +0000
+++ src/app/webbrowser/AddressBar.qml 2015-06-16 16:15:45 +0000
@@ -37,6 +37,7 @@
37 signal requestReload()37 signal requestReload()
38 signal requestStop()38 signal requestStop()
39 property string searchUrl39 property string searchUrl
40 property bool canSimplifyText: true
4041
41 property var securityStatus: null42 property var securityStatus: null
4243
@@ -48,6 +49,8 @@
4849
49 height: textField.height50 height: textField.height
5051
52 function selectAll() { textField.selectAll() }
53
51 TextField {54 TextField {
52 id: textField55 id: textField
53 objectName: "addressBarTextField"56 objectName: "addressBarTextField"
@@ -84,7 +87,7 @@
84 height: parent.height87 height: parent.height
85 width: height88 width: height
8689
87 visible: addressbar.activeFocus || addressbar.loading || !addressbar.text90 visible: addressbar.activeFocus || addressbar.loading || !addressbar.text || !canSimplifyText
8891
89 enabled: addressbar.text92 enabled: addressbar.text
90 opacity: enabled ? 1.0 : 0.393 opacity: enabled ? 1.0 : 0.3
@@ -229,7 +232,7 @@
229 QtObject {232 QtObject {
230 id: internal233 id: internal
231234
232 readonly property bool idle: !addressbar.loading && !addressbar.activeFocus235 readonly property bool idle: !addressbar.loading && !addressbar.activeFocus && addressbar.canSimplifyText
233236
234 readonly property int securityLevel: addressbar.securityStatus ? addressbar.securityStatus.securityLevel : Oxide.SecurityStatus.SecurityLevelNone237 readonly property int securityLevel: addressbar.securityStatus ? addressbar.securityStatus.securityLevel : Oxide.SecurityStatus.SecurityLevelNone
235 readonly property bool secureConnection: addressbar.securityStatus ? (securityLevel == Oxide.SecurityStatus.SecurityLevelSecure || securityLevel == Oxide.SecurityStatus.SecurityLevelSecureEV || securityLevel == Oxide.SecurityStatus.SecurityLevelWarning) : false238 readonly property bool secureConnection: addressbar.securityStatus ? (securityLevel == Oxide.SecurityStatus.SecurityLevelSecure || securityLevel == Oxide.SecurityStatus.SecurityLevelSecureEV || securityLevel == Oxide.SecurityStatus.SecurityLevelWarning) : false
@@ -289,16 +292,21 @@
289 return url292 return url
290 }293 }
291 }294 }
292 }
293295
294 onActiveFocusChanged: {296 function updateUrlFromFocus() {
295 if (activeFocus) {297 if (canSimplifyText) {
296 text = actualUrl298 if (addressbar.activeFocus) {
297 } else if (!loading && actualUrl.toString()) {299 text = actualUrl
298 text = internal.simplifyUrl(actualUrl)300 } else if (!loading && actualUrl.toString()) {
301 text = internal.simplifyUrl(actualUrl)
302 }
303 }
299 }304 }
300 }305 }
301306
307 onActiveFocusChanged: internal.updateUrlFromFocus()
308 onCanSimplifyTextChanged: internal.updateUrlFromFocus()
309
302 onActualUrlChanged: {310 onActualUrlChanged: {
303 if (!activeFocus || !actualUrl.toString()) {311 if (!activeFocus || !actualUrl.toString()) {
304 text = internal.simplifyUrl(actualUrl)312 text = internal.simplifyUrl(actualUrl)
305313
=== modified file 'src/app/webbrowser/Browser.qml'
--- src/app/webbrowser/Browser.qml 2015-06-02 14:26:38 +0000
+++ src/app/webbrowser/Browser.qml 2015-06-16 16:15:45 +0000
@@ -303,7 +303,10 @@
303 text: i18n.tr("History")303 text: i18n.tr("History")
304 iconName: "history"304 iconName: "history"
305 enabled: browser.historyModel305 enabled: browser.historyModel
306 onTriggered: historyViewComponent.createObject(historyViewContainer)306 onTriggered: {
307 historyViewComponent.createObject(historyViewContainer)
308 historyViewContainer.focus = true
309 }
307 },310 },
308 Action {311 Action {
309 objectName: "tabs"312 objectName: "tabs"
@@ -313,6 +316,7 @@
313 onTriggered: {316 onTriggered: {
314 recentView.state = "shown"317 recentView.state = "shown"
315 recentToolbar.state = "shown"318 recentToolbar.state = "shown"
319 recentView.focus = true
316 }320 }
317 },321 },
318 Action {322 Action {
@@ -326,7 +330,10 @@
326 objectName: "settings"330 objectName: "settings"
327 text: i18n.tr("Settings")331 text: i18n.tr("Settings")
328 iconName: "settings"332 iconName: "settings"
329 onTriggered: settingsComponent.createObject(settingsContainer)333 onTriggered: {
334 settingsComponent.createObject(settingsContainer)
335 settingsContainer.focus = true
336 }
330 },337 },
331 Action {338 Action {
332 objectName: "privatemode"339 objectName: "privatemode"
@@ -346,6 +353,10 @@
346 }353 }
347 }354 }
348 ]355 ]
356
357 addressBarCanSimplifyText: !(activeFocus || suggestionsList.activeFocus)
358 Keys.onDownPressed: if (suggestionsList.count) suggestionsList.focus = true
359 Keys.onEscapePressed: internal.resetFocus()
349 }360 }
350361
351 ChromeController {362 ChromeController {
@@ -358,7 +369,7 @@
358369
359 Suggestions {370 Suggestions {
360 id: suggestionsList371 id: suggestionsList
361 opacity: ((chrome.state == "shown") && chrome.activeFocus && (count > 0) && !chrome.drawerOpen) ? 1.0 : 0.0372 opacity: ((chrome.state == "shown") && (activeFocus || chrome.activeFocus) && count > 0 && !chrome.drawerOpen) ? 1.0 : 0.0
362 Behavior on opacity {373 Behavior on opacity {
363 UbuntuNumberAnimation {}374 UbuntuNumberAnimation {}
364 }375 }
@@ -372,6 +383,9 @@
372383
373 searchTerms: chrome.text.split(/\s+/g).filter(function(term) { return term.length > 0 })384 searchTerms: chrome.text.split(/\s+/g).filter(function(term) { return term.length > 0 })
374385
386 Keys.onUpPressed: chrome.focus = true
387 Keys.onEscapePressed: internal.resetFocus()
388
375 models: [historySuggestions,389 models: [historySuggestions,
376 bookmarksSuggestions,390 bookmarksSuggestions,
377 searchSuggestions.limit(4)]391 searchSuggestions.limit(4)]
@@ -404,7 +418,7 @@
404 id: searchSuggestions418 id: searchSuggestions
405 terms: suggestionsList.searchTerms419 terms: suggestionsList.searchTerms
406 searchEngine: currentSearchEngine420 searchEngine: currentSearchEngine
407 active: chrome.activeFocus &&421 active: (chrome.activeFocus || suggestionsList.activeFocus) &&
408 !browser.incognito &&422 !browser.incognito &&
409 !UrlManagement.looksLikeAUrl(chrome.text.replace(/ /g, "+"))423 !UrlManagement.looksLikeAUrl(chrome.text.replace(/ /g, "+"))
410424
@@ -416,7 +430,7 @@
416 }430 }
417 }431 }
418432
419 onSelected: {433 onActivated: {
420 browser.currentWebview.url = url434 browser.currentWebview.url = url
421 browser.currentWebview.forceActiveFocus()435 browser.currentWebview.forceActiveFocus()
422 chrome.requestedUrl = url436 chrome.requestedUrl = url
@@ -426,6 +440,7 @@
426440
427 FocusScope {441 FocusScope {
428 id: recentView442 id: recentView
443 objectName: "recentView"
429444
430 anchors.fill: parent445 anchors.fill: parent
431 visible: bottomEdgeHandle.dragging || tabslist.animating || (state == "shown")446 visible: bottomEdgeHandle.dragging || tabslist.animating || (state == "shown")
@@ -434,6 +449,13 @@
434 name: "shown"449 name: "shown"
435 }450 }
436451
452 function closeAndSwitchToTab(index) {
453 recentView.reset()
454 internal.switchToTab(index)
455 }
456
457 Keys.onEscapePressed: closeAndSwitchToTab(0)
458
437 TabsList {459 TabsList {
438 id: tabslist460 id: tabslist
439 anchors.fill: parent461 anchors.fill: parent
@@ -453,15 +475,7 @@
453 }475 }
454 }476 }
455 chromeOffset: chrome.height - invisibleTabChrome.height477 chromeOffset: chrome.height - invisibleTabChrome.height
456 onTabSelected: {478 onTabSelected: recentView.closeAndSwitchToTab(index)
457 var tab = tabsModel.get(index)
458 if (tab) {
459 tab.load()
460 tab.forceActiveFocus()
461 tabslist.model.setCurrent(index)
462 }
463 recentView.reset()
464 }
465 onTabClosed: {479 onTabClosed: {
466 var tab = tabsModel.remove(index)480 var tab = tabsModel.remove(index)
467 if (tab) {481 if (tab) {
@@ -497,10 +511,7 @@
497511
498 text: i18n.tr("Done")512 text: i18n.tr("Done")
499513
500 onClicked: {514 onClicked: recentView.closeAndSwitchToTab(0)
501 recentView.reset()
502 tabsModel.currentTab.load()
503 }
504 }515 }
505516
506 ToolbarAction {517 ToolbarAction {
@@ -600,8 +611,9 @@
600 }611 }
601 }612 }
602613
603 Item {614 FocusScope {
604 id: historyViewContainer615 id: historyViewContainer
616 objectName: "historyView"
605617
606 visible: children.length > 0618 visible: children.length > 0
607 anchors.fill: parent619 anchors.fill: parent
@@ -612,6 +624,12 @@
612 HistoryView {624 HistoryView {
613 anchors.fill: parent625 anchors.fill: parent
614 visible: historyViewContainer.children.length == 1626 visible: historyViewContainer.children.length == 1
627 focus: true
628
629 Keys.onEscapePressed: {
630 destroy()
631 internal.resetFocus()
632 }
615633
616 Timer {634 Timer {
617 // Set the model asynchronously to ensure635 // Set the model asynchronously to ensure
@@ -650,7 +668,7 @@
650 }668 }
651 }669 }
652670
653 Item {671 FocusScope {
654 id: settingsContainer672 id: settingsContainer
655673
656 visible: children.length > 0674 visible: children.length > 0
@@ -661,9 +679,14 @@
661679
662 SettingsPage {680 SettingsPage {
663 anchors.fill: parent681 anchors.fill: parent
682 focus: true
664 historyModel: browser.historyModel683 historyModel: browser.historyModel
665 settingsObject: settings684 settingsObject: settings
666 onDone: destroy()685 onDone: destroy()
686 Keys.onEscapePressed: {
687 destroy()
688 internal.resetFocus()
689 }
667 }690 }
668 }691 }
669 }692 }
@@ -920,9 +943,33 @@
920 }943 }
921 }944 }
922945
923 function focusAddressBar() {946 function switchToTab(index) {
947 var tab = tabsModel.get(index)
948 if (tab) {
949 tab.load()
950 tabslist.model.setCurrent(index)
951 if (tab.initialUrl == "" && formFactor == "desktop") focusAddressBar()
952 else tab.forceActiveFocus()
953 }
954 }
955
956 function closeCurrentTab() {
957 if (tabsModel.count > 0) {
958 var tab = tabsModel.remove(0)
959 if (tab) tab.close()
960
961 if (tabsModel.count === 0) {
962 browser.openUrlInNewTab("", true)
963 } else {
964 internal.switchToTab(0)
965 }
966 }
967 }
968
969 function focusAddressBar(selectContent) {
924 chrome.forceActiveFocus()970 chrome.forceActiveFocus()
925 Qt.inputMethod.show() // work around http://pad.lv/1316057971 Qt.inputMethod.show() // work around http://pad.lv/1316057
972 if (selectContent) chrome.addressBarSelectAll()
926 }973 }
927974
928 function resetFocus() {975 function resetFocus() {
@@ -959,6 +1006,20 @@
959 }1006 }
960 return false1007 return false
961 }1008 }
1009
1010 function historyGoBack() {
1011 if (currentWebview && currentWebview.canGoBack) {
1012 internal.resetFocus()
1013 currentWebview.goBack()
1014 }
1015 }
1016
1017 function historyGoForward() {
1018 if (currentWebview && currentWebview.canGoForward) {
1019 internal.resetFocus()
1020 currentWebview.goForward()
1021 }
1022 }
962 }1023 }
9631024
964 function openUrlInNewTab(url, setCurrent, load) {1025 function openUrlInNewTab(url, setCurrent, load) {
@@ -1128,4 +1189,135 @@
1128 }1189 }
1129 }1190 }
1130 }1191 }
1192
1193 Keys.onPressed: if (shortcuts.processKey(event.key, event.modifiers)) event.accepted = true
1194 KeyboardShortcuts {
1195 id: shortcuts
1196
1197 // Ctrl + Tab: pull the tab from the bottom of the stack to the
1198 // top (i.e. make it current)
1199 KeyboardShortcut {
1200 modifiers: Qt.ControlModifier
1201 key: Qt.Key_Tab
1202 enabled: chrome.visible || recentView.visible
1203 onTriggered: {
1204 internal.switchToTab(tabsModel.count - 1)
1205 if (chrome.visible) recentView.reset()
1206 else if (recentView.visible) recentView.focus = true
1207 }
1208 }
1209
1210 // Ctrl + w or Ctrl+F4: Close the current tab
1211 KeyboardShortcut {
1212 modifiers: Qt.ControlModifier
1213 key: Qt.Key_W
1214 enabled: chrome.visible || recentView.visible
1215 onTriggered: internal.closeCurrentTab()
1216 }
1217 KeyboardShortcut {
1218 modifiers: Qt.ControlModifier
1219 key: Qt.Key_F4
1220 enabled: chrome.visible || recentView.visible
1221 onTriggered: internal.closeCurrentTab()
1222 }
1223
1224 // Ctrl + t: Open a new Tab
1225 KeyboardShortcut {
1226 modifiers: Qt.ControlModifier
1227 key: Qt.Key_T
1228 enabled: chrome.visible || recentView.visible
1229 onTriggered: {
1230 openUrlInNewTab("", true)
1231 if (recentView.visible) recentView.reset()
1232 }
1233 }
1234
1235 // F6 or Ctrl + L or Alt + D: Select the content in the address bar
1236 KeyboardShortcut {
1237 modifiers: Qt.ControlModifier
1238 key: Qt.Key_L
1239 enabled: chrome.visible
1240 onTriggered: internal.focusAddressBar(true)
1241 }
1242 KeyboardShortcut {
1243 modifiers: Qt.AltModifier
1244 key: Qt.Key_D
1245 enabled: chrome.visible
1246 onTriggered: internal.focusAddressBar(true)
1247 }
1248 KeyboardShortcut {
1249 key: Qt.Key_F6
1250 enabled: chrome.visible
1251 onTriggered: internal.focusAddressBar(true)
1252 }
1253
1254 // Ctrl + D: Toggle bookmarked state on current Tab
1255 KeyboardShortcut {
1256 modifiers: Qt.ControlModifier
1257 key: Qt.Key_D
1258 enabled: chrome.visible
1259 onTriggered: {
1260 if (currentWebview) {
1261 if (bookmarksModel.contains(currentWebview.url)) {
1262 bookmarksModel.remove(currentWebview.url)
1263 } else {
1264 bookmarksModel.add(currentWebview.url, currentWebview.title, currentWebview.icon)
1265 }
1266 }
1267 }
1268 }
1269
1270 // Ctrl + H: Show History
1271 KeyboardShortcut {
1272 modifiers: Qt.ControlModifier
1273 key: Qt.Key_H
1274 enabled: chrome.visible
1275 onTriggered: {
1276 if (historyViewContainer.children.length === 0) {
1277 historyViewComponent.createObject(historyViewContainer)
1278 historyViewContainer.focus = true
1279 }
1280 }
1281 }
1282
1283 // Alt + Left Arrow or Backspace: Goes to the previous page in history
1284 KeyboardShortcut {
1285 modifiers: Qt.AltModifier
1286 key: Qt.Key_Left
1287 enabled: chrome.visible
1288 onTriggered: internal.historyGoBack()
1289 }
1290 KeyboardShortcut {
1291 key: Qt.Key_Backspace
1292 enabled: chrome.visible
1293 onTriggered: internal.historyGoBack()
1294 }
1295
1296 // Alt + Right Arrow or Shift + Backspace: Goes to the next page in history
1297 KeyboardShortcut {
1298 modifiers: Qt.AltModifier
1299 key: Qt.Key_Right
1300 enabled: chrome.visible
1301 onTriggered: internal.historyGoForward()
1302 }
1303 KeyboardShortcut {
1304 modifiers: Qt.ShiftModifier
1305 key: Qt.Key_Backspace
1306 enabled: chrome.visible
1307 onTriggered: internal.historyGoForward()
1308 }
1309
1310 // F5 or Ctrl + R: Reload current Tab
1311 KeyboardShortcut {
1312 key: Qt.Key_F5
1313 enabled: chrome.visible
1314 onTriggered: if (currentWebview) currentWebview.reload()
1315 }
1316 KeyboardShortcut {
1317 modifiers: Qt.ControlModifier
1318 key: Qt.Key_R
1319 enabled: chrome.visible
1320 onTriggered: if (currentWebview) currentWebview.reload()
1321 }
1322 }
1131}1323}
11321324
=== modified file 'src/app/webbrowser/Chrome.qml'
--- src/app/webbrowser/Chrome.qml 2015-05-26 21:42:54 +0000
+++ src/app/webbrowser/Chrome.qml 2015-06-16 16:15:45 +0000
@@ -29,10 +29,13 @@
29 property list<Action> drawerActions29 property list<Action> drawerActions
30 readonly property bool drawerOpen: internal.openDrawer30 readonly property bool drawerOpen: internal.openDrawer
31 property alias requestedUrl: addressbar.requestedUrl31 property alias requestedUrl: addressbar.requestedUrl
32 property alias addressBarCanSimplifyText: addressbar.canSimplifyText
32 property alias incognito: addressbar.incognito33 property alias incognito: addressbar.incognito
3334
34 backgroundColor: incognito ? UbuntuColors.darkGrey : Theme.palette.normal.background35 backgroundColor: incognito ? UbuntuColors.darkGrey : Theme.palette.normal.background
3536
37 function addressBarSelectAll() { addressbar.selectAll() }
38
36 FocusScope {39 FocusScope {
37 anchors {40 anchors {
38 fill: parent41 fill: parent
3942
=== added file 'src/app/webbrowser/KeyboardShortcut.qml'
--- src/app/webbrowser/KeyboardShortcut.qml 1970-01-01 00:00:00 +0000
+++ src/app/webbrowser/KeyboardShortcut.qml 2015-06-16 16:15:45 +0000
@@ -0,0 +1,25 @@
1/*
2 * Copyright 2015 Canonical Ltd.
3 *
4 * This file is part of webbrowser-app.
5 *
6 * webbrowser-app is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; version 3.
9 *
10 * webbrowser-app is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19import QtQuick 2.0
20import Ubuntu.Components 1.1
21
22Action {
23 property int key
24 property int modifiers: Qt.NoModifier
25}
026
=== added file 'src/app/webbrowser/KeyboardShortcuts.qml'
--- src/app/webbrowser/KeyboardShortcuts.qml 1970-01-01 00:00:00 +0000
+++ src/app/webbrowser/KeyboardShortcuts.qml 2015-06-16 16:15:45 +0000
@@ -0,0 +1,41 @@
1/*
2 * Copyright 2015 Canonical Ltd.
3 *
4 * This file is part of webbrowser-app.
5 *
6 * webbrowser-app is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; version 3.
9 *
10 * webbrowser-app is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19import QtQuick 2.0
20
21Item {
22 function processKey(key, modifiers) {
23 for (var i = 0; i < data.length; i++) {
24 var shortcut = data[i];
25
26 if (!shortcut.enabled) continue
27 if (key !== shortcut.key) continue
28
29 if (shortcut.modifiers === Qt.NoModifier) {
30 if (modifiers === Qt.NoModifier) {
31 shortcut.trigger()
32 return true
33 }
34 } else if ((modifiers & shortcut.modifiers) === shortcut.modifiers) {
35 shortcut.trigger()
36 return true
37 }
38 }
39 return false
40 }
41}
042
=== modified file 'src/app/webbrowser/Suggestion.qml'
--- src/app/webbrowser/Suggestion.qml 2015-04-22 11:00:02 +0000
+++ src/app/webbrowser/Suggestion.qml 2015-06-16 16:15:45 +0000
@@ -28,7 +28,7 @@
28 property alias icon: icon.name28 property alias icon: icon.name
29 property url url29 property url url
3030
31 signal selected(url url)31 signal activated(url url)
3232
33 __height: Math.max(middleVisuals.height, units.gu(6))33 __height: Math.max(middleVisuals.height, units.gu(6))
34 // disable focus handling34 // disable focus handling
@@ -77,8 +77,9 @@
77 fontSize: "small"77 fontSize: "small"
78 elide: Text.ElideRight78 elide: Text.ElideRight
79 visible: text !== ""79 visible: text !== ""
80 color: selected ? "#DB4923" : "black"
80 }81 }
81 }82 }
8283
83 onClicked: selected(url)84 onClicked: activated(url)
84}85}
8586
=== modified file 'src/app/webbrowser/Suggestions.qml'
--- src/app/webbrowser/Suggestions.qml 2015-05-12 09:56:40 +0000
+++ src/app/webbrowser/Suggestions.qml 2015-06-16 16:15:45 +0000
@@ -19,20 +19,24 @@
19import QtQuick 2.019import QtQuick 2.0
20import Ubuntu.Components 1.120import Ubuntu.Components 1.1
2121
22Rectangle {22FocusScope {
23 id: suggestions23 id: suggestions
2424
25 property var searchTerms25 property var searchTerms
26 property var models26 property var models
27
27 readonly property int count: models.reduce(countItems, 0)28 readonly property int count: models.reduce(countItems, 0)
28 readonly property alias contentHeight: suggestionsList.contentHeight29 readonly property alias contentHeight: suggestionsList.contentHeight
2930
30 signal selected(url url)31 signal activated(url url)
3132
32 radius: units.gu(0.5)33 Rectangle {
33 border {34 anchors.fill: parent
34 color: "#dedede"35 radius: units.gu(0.5)
35 width: 136 border {
37 color: "#dedede"
38 width: 1
39 }
36 }40 }
3741
38 clip: true42 clip: true
@@ -40,39 +44,37 @@
40 ListView {44 ListView {
41 id: suggestionsList45 id: suggestionsList
42 anchors.fill: parent46 anchors.fill: parent
4347 focus: true
44 model: suggestions.models48
45 delegate: Column {49 model: models.reduce(function(list, model) {
46 id: suggestionsSection50 var modelItems = [];
51
52 // Models inheriting from QAbstractItemModel and JS arrays expose their
53 // data differently, so we need to collect their items differently
54 if (model.forEach) {
55 model.forEach(function(item) { modelItems.push(item) })
56 } else {
57 for (var i = 0; i < model.count; i++) modelItems.push(model.get(i))
58 }
59
60 modelItems.forEach(function(item) {
61 item["icon"] = model.icon
62 item["displayUrl"] = model.displayUrl
63 list.push(item);
64 })
65 return list;
66 }, [])
67
68 delegate: Suggestion {
47 width: suggestionsList.width69 width: suggestionsList.width
48 height: childrenRect.height70 showDivider: index < model.length - 1
4971
50 property string icon: models[index].icon72 title: highlightTerms(modelData.title)
51 property bool displayUrl: models[index].displayUrl73 subtitle: modelData.displayUrl ? highlightTerms(modelData.url) : ""
52 property int firstItemIndex: models.slice(0, index).reduce(countItems, 0)74 icon: modelData.icon
5375 selected: suggestionsList.activeFocus && ListView.isCurrentItem
54 Repeater {76
55 id: suggestionsSource77 onActivated: suggestions.activated(modelData.url)
56 model: modelData
57
58 delegate: Suggestion {
59 id: suggestion
60 width: suggestionsList.width
61 showDivider: suggestionsSection.firstItemIndex + index <
62 suggestions.count - 1
63
64 // Necessary to support both using objects inheriting from
65 // QAbstractItemModel and JS arrays as models, since they
66 // expose their data differently
67 property var item: (model.modelData) ? model.modelData : model
68
69 title: highlightTerms(item.title)
70 subtitle: suggestionsSection.displayUrl ? highlightTerms(item.url) : ""
71 icon: suggestionsSection.icon
72
73 onSelected: suggestions.selected(item.url)
74 }
75 }
76 }78 }
77 }79 }
7880
7981
=== modified file 'src/app/webbrowser/limit-proxy-model.cpp'
--- src/app/webbrowser/limit-proxy-model.cpp 2014-07-04 12:30:09 +0000
+++ src/app/webbrowser/limit-proxy-model.cpp 2015-06-16 16:15:45 +0000
@@ -266,3 +266,19 @@
266 m_dataChangedEnd = -1;266 m_dataChangedEnd = -1;
267 }267 }
268}268}
269
270QVariantMap LimitProxyModel::get(int i) const
271{
272 QVariantMap item;
273 QHash<int, QByteArray> roles = roleNames();
274
275 QModelIndex modelIndex = index(i, 0);
276 if (modelIndex.isValid()) {
277 Q_FOREACH(int role, roles.keys()) {
278 QString roleName = QString::fromUtf8(roles.value(role));
279 item.insert(roleName, data(modelIndex, role));
280 }
281 }
282 return item;
283}
284
269285
=== modified file 'src/app/webbrowser/limit-proxy-model.h'
--- src/app/webbrowser/limit-proxy-model.h 2014-06-27 20:29:52 +0000
+++ src/app/webbrowser/limit-proxy-model.h 2015-06-16 16:15:45 +0000
@@ -45,6 +45,8 @@
45 int rowCount(const QModelIndex &parent = QModelIndex()) const;45 int rowCount(const QModelIndex &parent = QModelIndex()) const;
46 int unlimitedRowCount(const QModelIndex &parent = QModelIndex()) const;46 int unlimitedRowCount(const QModelIndex &parent = QModelIndex()) const;
4747
48 Q_INVOKABLE QVariantMap get(int index) const;
49
48Q_SIGNALS:50Q_SIGNALS:
49 void sourceModelChanged() const;51 void sourceModelChanged() const;
50 void limitChanged() const;52 void limitChanged() const;
5153
=== modified file 'tests/autopilot/webbrowser_app/emulators/browser.py'
--- tests/autopilot/webbrowser_app/emulators/browser.py 2015-05-28 13:56:52 +0000
+++ tests/autopilot/webbrowser_app/emulators/browser.py 2015-06-16 16:15:45 +0000
@@ -19,7 +19,7 @@
19import autopilot.logging19import autopilot.logging
20import ubuntuuitoolkit as uitk20import ubuntuuitoolkit as uitk
21from autopilot import exceptions21from autopilot import exceptions
2222from autopilot import input
2323
24logger = logging.getLogger(__name__)24logger = logging.getLogger(__name__)
2525
@@ -30,6 +30,7 @@
30 super().__init__(*args)30 super().__init__(*args)
31 self.chrome = self._get_chrome()31 self.chrome = self._get_chrome()
32 self.address_bar = self.chrome.address_bar32 self.address_bar = self.chrome.address_bar
33 self.keyboard = input.Keyboard.create()
3334
34 def _get_chrome(self):35 def _get_chrome(self):
35 return self.select_single(Chrome)36 return self.select_single(Chrome)
@@ -150,6 +151,17 @@
150 def get_bottom_edge_hint(self):151 def get_bottom_edge_hint(self):
151 return self.select_single("QQuickImage", objectName="bottomEdgeHint")152 return self.select_single("QQuickImage", objectName="bottomEdgeHint")
152153
154 # The history view is dynamically created, so it might or might not be
155 # available
156 def get_history_view(self):
157 try:
158 return self.select_single("HistoryView")
159 except exceptions.StateNotFoundError:
160 return None
161
162 def press_key(self, key):
163 self.keyboard.press_and_release(key)
164
153165
154class Chrome(uitk.UbuntuUIToolkitCustomProxyObjectBase):166class Chrome(uitk.UbuntuUIToolkitCustomProxyObjectBase):
155167
@@ -224,11 +236,14 @@
224 @autopilot.logging.log_action(logger.info)236 @autopilot.logging.log_action(logger.info)
225 def go_to_url(self, url):237 def go_to_url(self, url):
226 self.write(url)238 self.write(url)
227 self.text_field.keyboard.press_and_release('Enter')239 self.press_key('Enter')
228240
229 def write(self, text, clear=True):241 def write(self, text, clear=True):
230 self.text_field.write(text, clear)242 self.text_field.write(text, clear)
231243
244 def press_key(self, key):
245 self.text_field.keyboard.press_and_release(key)
246
232 @autopilot.logging.log_action(logger.info)247 @autopilot.logging.log_action(logger.info)
233 def click_action_button(self):248 def click_action_button(self):
234 button = self.select_single("QQuickMouseArea",249 button = self.select_single("QQuickMouseArea",
235250
=== modified file 'tests/autopilot/webbrowser_app/tests/__init__.py'
--- tests/autopilot/webbrowser_app/tests/__init__.py 2015-05-25 19:17:25 +0000
+++ tests/autopilot/webbrowser_app/tests/__init__.py 2015-06-16 16:15:45 +0000
@@ -169,6 +169,14 @@
169 self.pointing_device.click_object(settings_action)169 self.pointing_device.click_object(settings_action)
170 return self.main_window.get_settings_page()170 return self.main_window.get_settings_page()
171171
172 def open_history(self):
173 chrome = self.main_window.chrome
174 drawer_button = chrome.get_drawer_button()
175 self.pointing_device.click_object(drawer_button)
176 chrome.get_drawer()
177 settings_action = chrome.get_drawer_action("history")
178 self.pointing_device.click_object(settings_action)
179
172 def assert_number_webviews_eventually(self, count):180 def assert_number_webviews_eventually(self, count):
173 self.assertThat(lambda: len(self.main_window.get_webviews()),181 self.assertThat(lambda: len(self.main_window.get_webviews()),
174 Eventually(Equals(count)))182 Eventually(Equals(count)))
@@ -195,7 +203,7 @@
195 are executed, thus making them more robust.203 are executed, thus making them more robust.
196 """204 """
197205
198 def setUp(self):206 def setUp(self, path="/test1"):
199 self.http_server = http_server.HTTPServerInAThread()207 self.http_server = http_server.HTTPServerInAThread()
200 self.ping_server(self.http_server)208 self.ping_server(self.http_server)
201 self.addCleanup(self.http_server.cleanup)209 self.addCleanup(self.http_server.cleanup)
@@ -203,7 +211,7 @@
203 'UBUNTU_WEBVIEW_HOST_MAPPING_RULES',211 'UBUNTU_WEBVIEW_HOST_MAPPING_RULES',
204 "MAP test:80 localhost:{}".format(self.http_server.port)))212 "MAP test:80 localhost:{}".format(self.http_server.port)))
205 self.base_url = "http://test"213 self.base_url = "http://test"
206 self.url = self.base_url + "/test1"214 self.url = self.base_url + path
207 self.ARGS = self.ARGS + [self.url]215 self.ARGS = self.ARGS + [self.url]
208 super(StartOpenRemotePageTestCaseBase, self).setUp()216 super(StartOpenRemotePageTestCaseBase, self).setUp()
209 self.assert_home_page_eventually_loaded()217 self.assert_home_page_eventually_loaded()
210218
=== modified file 'tests/autopilot/webbrowser_app/tests/http_server.py'
--- tests/autopilot/webbrowser_app/tests/http_server.py 2015-04-23 15:34:16 +0000
+++ tests/autopilot/webbrowser_app/tests/http_server.py 2015-06-16 16:15:45 +0000
@@ -140,6 +140,10 @@
140 if query in self.suggestions_data:140 if query in self.suggestions_data:
141 suggestions = self.suggestions_data[query]141 suggestions = self.suggestions_data[query]
142 self.wfile.write(json.dumps(suggestions).encode())142 self.wfile.write(json.dumps(suggestions).encode())
143 elif self.path.startswith("/tab/"):
144 self.send_response(200)
145 name = self.path[len("/tab/"):]
146 self.send_html('<html><body>' + name + '</body></html>')
143 else:147 else:
144 self.send_error(404)148 self.send_error(404)
145149
146150
=== modified file 'tests/autopilot/webbrowser_app/tests/test_addressbar_bookmark.py'
--- tests/autopilot/webbrowser_app/tests/test_addressbar_bookmark.py 2015-04-27 03:23:00 +0000
+++ tests/autopilot/webbrowser_app/tests/test_addressbar_bookmark.py 2015-06-16 16:15:45 +0000
@@ -60,12 +60,10 @@
60 self.pointing_device.click_object(webview)60 self.pointing_device.click_object(webview)
61 address_bar = self.main_window.address_bar61 address_bar = self.main_window.address_bar
62 bookmark_toggle = address_bar.get_bookmark_toggle()62 bookmark_toggle = address_bar.get_bookmark_toggle()
63 self.assertThat(address_bar.activeFocus, Eventually(Equals(False)))
64 self.assertThat(bookmark_toggle.visible, Eventually(Equals(True)))63 self.assertThat(bookmark_toggle.visible, Eventually(Equals(True)))
6564
66 self.open_tabs_view()65 self.open_tabs_view()
67 tabs_view = self.main_window.get_tabs_view()66 tabs_view = self.main_window.get_tabs_view()
68 self.main_window.get_tabs_view().get_previews()[1].select()67 self.main_window.get_tabs_view().get_previews()[1].select()
69 tabs_view.visible.wait_for(False)68 tabs_view.visible.wait_for(False)
70 self.assertThat(address_bar.activeFocus, Equals(False))
71 self.assertThat(bookmark_toggle.visible, Eventually(Equals(False)))69 self.assertThat(bookmark_toggle.visible, Eventually(Equals(False)))
7270
=== added file 'tests/autopilot/webbrowser_app/tests/test_keyboard.py'
--- tests/autopilot/webbrowser_app/tests/test_keyboard.py 1970-01-01 00:00:00 +0000
+++ tests/autopilot/webbrowser_app/tests/test_keyboard.py 2015-06-16 16:15:45 +0000
@@ -0,0 +1,276 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2#
3# Copyright 2015 Canonical
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17import os
18import sqlite3
19import time
20import testtools
21
22from testtools.matchers import Equals, NotEquals, GreaterThan
23from autopilot.matchers import Eventually
24from autopilot.platform import model
25
26from webbrowser_app.tests import StartOpenRemotePageTestCaseBase
27
28
29class PrepopulatedDatabaseTestCaseBase(StartOpenRemotePageTestCaseBase):
30
31 """Helper test class that pre-populates history and bookmarks databases."""
32
33 def setUp(self):
34 self.create_temporary_profile()
35 self.populate_bookmarks()
36 super(PrepopulatedDatabaseTestCaseBase, self).setUp("/tab/0")
37
38 def populate_bookmarks(self):
39 db_path = os.path.join(self.data_location, "bookmarks.sqlite")
40 connection = sqlite3.connect(db_path)
41 connection.execute("""CREATE TABLE IF NOT EXISTS bookmarks
42 (url VARCHAR, title VARCHAR, icon VARCHAR,
43 created INTEGER);""")
44 rows = [
45 ("http://www.rsc.org/periodic-table/element/77/iridium",
46 "Iridium - Element Information")
47 ]
48
49 for i, row in enumerate(rows):
50 timestamp = int(time.time()) - i * 10
51 query = "INSERT INTO bookmarks \
52 VALUES ('{}', '{}', '', {});"
53 query = query.format(row[0], row[1], timestamp)
54 connection.execute(query)
55
56 connection.commit()
57 connection.close()
58
59
60@testtools.skipIf(model() != "Desktop", "on desktop only")
61class TestKeyboard(PrepopulatedDatabaseTestCaseBase):
62
63 """Test keyboard interaction"""
64
65 def setUp(self):
66 super(TestKeyboard, self).setUp()
67 self.address_bar = self.main_window.address_bar
68
69 def open_tab(self, url):
70 self.main_window.press_key('Ctrl+T')
71 new_tab_view = self.main_window.get_new_tab_view()
72 self.address_bar.go_to_url(url)
73 new_tab_view.wait_until_destroyed()
74 self.assertThat(lambda: self.main_window.get_current_webview().url,
75 Eventually(Equals(url)))
76
77 # Name tabs starting from 1 by default because tab 0 has been opened
78 # already via StartOpenRemotePageTestCaseBase
79 def open_tabs(self, count, base=1):
80 for i in range(0, count):
81 self.open_tab(self.base_url + "/tab/" + str(i + base))
82
83 def check_tab_number(self, number):
84 url = self.base_url + "/tab/" + str(number)
85 self.assertThat(lambda: self.main_window.get_current_webview().url,
86 Eventually(Equals(url)))
87
88 def test_new_tab(self):
89 self.main_window.press_key('Ctrl+T')
90
91 webview = self.main_window.get_current_webview()
92 self.assertThat(webview.url, Equals(""))
93 new_tab_view = self.main_window.get_new_tab_view()
94 self.assertThat(new_tab_view.visible, Eventually(Equals(True)))
95
96 def test_switch_tabs(self):
97 self.open_tabs(2)
98 self.check_tab_number(2)
99 self.main_window.press_key('Ctrl+Tab')
100 self.check_tab_number(0)
101 self.main_window.press_key('Ctrl+Tab')
102 self.check_tab_number(1)
103 self.main_window.press_key('Ctrl+Tab')
104 self.check_tab_number(2)
105
106 def test_can_switch_tabs_after_suggestions_escape(self):
107 self.open_tabs(1)
108 self.check_tab_number(1)
109
110 suggestions = self.main_window.get_suggestions()
111 self.address_bar.write('el')
112 self.assertThat(suggestions.opacity, Eventually(Equals(1)))
113 self.main_window.press_key('Down')
114 self.assertThat(suggestions.activeFocus, Eventually(Equals(True)))
115
116 self.main_window.press_key('Escape')
117 self.assertThat(suggestions.opacity, Eventually(Equals(0)))
118
119 self.main_window.press_key('Ctrl+Tab')
120 self.check_tab_number(0)
121
122 def test_switch_tabs_from_tabs_view(self):
123 self.open_tabs(1)
124 self.check_tab_number(1)
125 tabs = self.open_tabs_view()
126 self.main_window.press_key('Ctrl+Tab')
127 self.check_tab_number(0)
128 self.main_window.press_key('Ctrl+Tab')
129 self.check_tab_number(1)
130 self.main_window.press_key('Ctrl+Tab')
131 self.check_tab_number(0)
132 self.main_window.press_key('Escape')
133 self.assertThat(tabs.visible, Eventually(Equals(False)))
134 self.check_tab_number(0)
135
136 def test_close_tabs_ctrl_f4(self):
137 self.open_tabs(1)
138 self.check_tab_number(1)
139 self.main_window.press_key('Ctrl+F4')
140 self.check_tab_number(0)
141 self.main_window.press_key('Ctrl+F4')
142 webview = self.main_window.get_current_webview()
143 self.assertThat(webview.url, Equals(""))
144
145 def test_close_tabs_ctrl_w(self):
146 self.open_tabs(1)
147 self.check_tab_number(1)
148 self.main_window.press_key('Ctrl+w')
149 self.check_tab_number(0)
150 self.main_window.press_key('Ctrl+w')
151 webview = self.main_window.get_current_webview()
152 self.assertThat(webview.url, Equals(""))
153
154 def test_close_tabs_tabs_view(self):
155 self.open_tabs(1)
156 self.check_tab_number(1)
157 self.open_tabs_view()
158 self.main_window.press_key('Ctrl+w')
159 self.check_tab_number(0)
160 self.main_window.press_key('Ctrl+F4')
161 webview = self.main_window.get_current_webview()
162 self.assertThat(webview.url, Equals(""))
163
164 def test_select_address_bar_ctrl_l(self):
165 self.main_window.press_key('Ctrl+L')
166 self.assertThat(self.address_bar.text_field.selectedText,
167 Eventually(Equals(self.address_bar.text_field.text)))
168
169 def test_select_address_bar_alt_d(self):
170 self.main_window.press_key('Alt+D')
171 self.assertThat(self.address_bar.text_field.selectedText,
172 Eventually(Equals(self.address_bar.text_field.text)))
173
174 def test_select_address_bar_f6(self):
175 self.main_window.press_key('F6')
176 self.assertThat(self.address_bar.text_field.selectedText,
177 Eventually(Equals(self.address_bar.text_field.text)))
178
179 def test_escape_from_address_bar(self):
180 self.main_window.press_key('Alt+D')
181 self.assertThat(self.address_bar.text_field.selectedText,
182 Eventually(Equals(self.address_bar.text_field.text)))
183 self.main_window.press_key('Escape')
184 self.assertThat(self.address_bar.text_field.selectedText,
185 Eventually(Equals("")))
186 self.assertThat(self.address_bar.activeFocus,
187 Eventually(Equals(False)))
188 webview = self.main_window.get_current_webview()
189 self.assertThat(webview.activeFocus, Eventually(Equals(True)))
190
191 def test_reload(self):
192 webview = self.main_window.get_current_webview()
193 self.assertThat(webview.loading, Eventually(Equals(False)))
194
195 watcher = webview.watch_signal('loadingStateChanged()')
196 previous = watcher.num_emissions
197
198 self.main_window.press_key('Ctrl+R')
199 self.assertThat(
200 lambda: watcher.num_emissions,
201 Eventually(GreaterThan(previous)))
202
203 self.assertThat(webview.loading, Eventually(Equals(False)))
204
205 previous = watcher.num_emissions
206
207 self.main_window.press_key('F5')
208 self.assertThat(
209 lambda: watcher.num_emissions,
210 Eventually(GreaterThan(previous)))
211
212 self.assertThat(webview.loading, Eventually(Equals(False)))
213
214 def test_bookmark(self):
215 chrome = self.main_window.chrome
216 self.assertThat(chrome.bookmarked, Equals(False))
217 self.main_window.press_key('Ctrl+D')
218 self.assertThat(chrome.bookmarked, Eventually(Equals(True)))
219 self.main_window.press_key('Ctrl+D')
220 self.assertThat(chrome.bookmarked, Eventually(Equals(False)))
221
222 def test_history_navigation_with_alt_arrows(self):
223 previous = self.main_window.get_current_webview().url
224 url = self.base_url + "/test2"
225 self.main_window.go_to_url(url)
226 self.main_window.wait_until_page_loaded(url)
227
228 self.main_window.press_key('Alt+Left')
229 self.assertThat(lambda: self.main_window.get_current_webview().url,
230 Eventually(Equals(previous)))
231
232 self.main_window.press_key('Alt+Right')
233 self.assertThat(lambda: self.main_window.get_current_webview().url,
234 Eventually(Equals(url)))
235
236 def test_history_navigation_with_backspace(self):
237 previous = self.main_window.get_current_webview().url
238 url = self.base_url + "/test2"
239 self.main_window.go_to_url(url)
240 self.main_window.wait_until_page_loaded(url)
241
242 self.main_window.press_key('Backspace')
243 self.assertThat(lambda: self.main_window.get_current_webview().url,
244 Eventually(Equals(previous)))
245
246 self.main_window.press_key('Shift+Backspace')
247 self.assertThat(lambda: self.main_window.get_current_webview().url,
248 Eventually(Equals(url)))
249
250 def test_toggle_history(self):
251 self.assertThat(self.main_window.get_history_view(), Equals(None))
252 self.main_window.press_key('Ctrl+H')
253 self.assertThat(lambda: self.main_window.get_history_view(),
254 Eventually(NotEquals(None)))
255 history_view = self.main_window.get_history_view()
256
257 self.main_window.press_key('Escape')
258 history_view.wait_until_destroyed()
259 webview = self.main_window.get_current_webview()
260 self.assertThat(webview.activeFocus, Eventually(Equals(True)))
261
262 def test_toggle_history_from_menu(self):
263 self.assertThat(self.main_window.get_history_view(), Equals(None))
264 self.open_history()
265 history_view = self.main_window.get_history_view()
266 self.assertThat(history_view.activeFocus, Eventually(Equals(True)))
267
268 self.main_window.press_key('Escape')
269 history_view.wait_until_destroyed()
270
271 def test_escape_settings(self):
272 settings = self.open_settings()
273 self.main_window.press_key('Escape')
274 settings.wait_until_destroyed()
275 webview = self.main_window.get_current_webview()
276 self.assertThat(webview.activeFocus, Eventually(Equals(True)))
0277
=== modified file 'tests/autopilot/webbrowser_app/tests/test_suggestions.py'
--- tests/autopilot/webbrowser_app/tests/test_suggestions.py 2015-05-20 05:45:29 +0000
+++ tests/autopilot/webbrowser_app/tests/test_suggestions.py 2015-06-16 16:15:45 +0000
@@ -18,9 +18,11 @@
18import random18import random
19import sqlite319import sqlite3
20import time20import time
21import unittest
2122
22from testtools.matchers import Contains, Equals23from testtools.matchers import Contains, Equals, GreaterThan
23from autopilot.matchers import Eventually24from autopilot.matchers import Eventually
25from autopilot.platform import model
2426
25from webbrowser_app.tests import StartOpenRemotePageTestCaseBase27from webbrowser_app.tests import StartOpenRemotePageTestCaseBase
26from . import http_server28from . import http_server
@@ -170,10 +172,9 @@
170 def test_show_list_of_suggestions(self):172 def test_show_list_of_suggestions(self):
171 suggestions = self.main_window.get_suggestions()173 suggestions = self.main_window.get_suggestions()
172 self.assert_suggestions_eventually_hidden()174 self.assert_suggestions_eventually_hidden()
173 self.assert_suggestions_eventually_hidden()
174 self.address_bar.focus()175 self.address_bar.focus()
175 self.assert_suggestions_eventually_shown()176 self.assert_suggestions_eventually_shown()
176 self.assertThat(suggestions.count, Eventually(Equals(1)))177 self.assertThat(suggestions.count, Eventually(GreaterThan(0)))
177 self.address_bar.clear()178 self.address_bar.clear()
178 self.assert_suggestions_eventually_hidden()179 self.assert_suggestions_eventually_hidden()
179180
@@ -303,3 +304,67 @@
303 highlighted = self.highlight_term("highlight", "high")304 highlighted = self.highlight_term("highlight", "high")
304 self.assertThat(entries[0].title, Equals(highlighted))305 self.assertThat(entries[0].title, Equals(highlighted))
305 self.assertThat(entries[0].subtitle, Equals(''))306 self.assertThat(entries[0].subtitle, Equals(''))
307
308 @unittest.skipIf(model() != "Desktop", "on desktop only")
309 def test_keyboard_navigation(self):
310 suggestions = self.main_window.get_suggestions()
311 address_bar = self.address_bar
312 address_bar.write('element')
313 self.assert_suggestions_eventually_shown()
314 self.assertThat(suggestions.count, Eventually(Equals(2)))
315 entries = suggestions.get_ordered_entries()
316 self.assertThat(entries[0].selected, Equals(False))
317 self.assertThat(entries[1].selected, Equals(False))
318
319 address_bar.press_key('Down')
320 self.assertThat(address_bar.activeFocus, Eventually(Equals(False)))
321 self.assertThat(suggestions.activeFocus, Eventually(Equals(True)))
322 self.assertThat(entries[0].selected, Equals(True))
323
324 self.main_window.press_key('Down')
325 self.assertThat(entries[0].selected, Equals(False))
326 self.assertThat(entries[1].selected, Equals(True))
327
328 # verify that selection does not wrap around
329 self.main_window.press_key('Down')
330 self.assertThat(entries[0].selected, Equals(False))
331 self.assertThat(entries[1].selected, Equals(True))
332
333 self.main_window.press_key('Up')
334 self.assertThat(entries[0].selected, Equals(True))
335 self.assertThat(entries[1].selected, Equals(False))
336
337 self.main_window.press_key('Up')
338 self.assertThat(address_bar.activeFocus, Eventually(Equals(True)))
339 self.assertThat(suggestions.activeFocus, Eventually(Equals(False)))
340 self.assertThat(entries[0].selected, Equals(False))
341 self.assertThat(entries[1].selected, Equals(False))
342
343 @unittest.skipIf(model() != "Desktop", "on desktop only")
344 def test_suggestions_escape(self):
345 suggestions = self.main_window.get_suggestions()
346 previous_text = self.address_bar.text
347 self.address_bar.write('element')
348 self.assert_suggestions_eventually_shown()
349 self.main_window.press_key('Down')
350 self.assertThat(suggestions.activeFocus, Eventually(Equals(True)))
351 self.assertThat(self.address_bar.text, Equals("element"))
352
353 self.main_window.press_key('Escape')
354 self.assert_suggestions_eventually_hidden()
355 self.assertThat(self.address_bar.text, Equals(previous_text))
356
357 @unittest.skipIf(model() != "Desktop", "on desktop only")
358 def test_suggestions_escape_on_addressbar(self):
359 suggestions = self.main_window.get_suggestions()
360 previous_text = self.address_bar.text
361 self.address_bar.write('element')
362 self.assert_suggestions_eventually_shown()
363 self.main_window.press_key('Down')
364 self.assertThat(suggestions.activeFocus, Eventually(Equals(True)))
365 self.main_window.press_key('Up')
366 self.assertThat(suggestions.activeFocus, Eventually(Equals(False)))
367
368 self.main_window.press_key('Escape')
369 self.assert_suggestions_eventually_hidden()
370 self.assertThat(self.address_bar.text, Equals(previous_text))
306371
=== modified file 'tests/unittests/limit-proxy-model/tst_LimitProxyModelTests.cpp'
--- tests/unittests/limit-proxy-model/tst_LimitProxyModelTests.cpp 2015-05-19 10:58:04 +0000
+++ tests/unittests/limit-proxy-model/tst_LimitProxyModelTests.cpp 2015-06-16 16:15:45 +0000
@@ -157,6 +157,28 @@
157 QCOMPARE(model->unlimitedRowCount(), 3);157 QCOMPARE(model->unlimitedRowCount(), 3);
158 QCOMPARE(model->rowCount(), 2);158 QCOMPARE(model->rowCount(), 2);
159 }159 }
160
161 void shouldGetItemWithCorrectValues()
162 {
163 history->add(QUrl("http://example1.org/"), "Example 1 Domain", QUrl());
164
165 QVariantMap item = model->get(0);
166 QHash<int, QByteArray> roles = model->roleNames();
167
168 QCOMPARE(roles.count(), item.count());
169
170 Q_FOREACH(int role, roles.keys()) {
171 QString roleName = QString::fromUtf8(roles.value(role));
172 QCOMPARE(model->data(model->index(0, 0), role), item.value(roleName));
173 }
174 }
175
176 void shouldReturnEmptyItemIfGetOutOfBounds()
177 {
178 QVariantMap item = model->get(1);
179 QVERIFY(item.isEmpty());
180 }
181
160};182};
161183
162QTEST_MAIN(LimitProxyModelTests)184QTEST_MAIN(LimitProxyModelTests)

Subscribers

People subscribed via source and target branches

to status/vote changes: