Merge lp:~uriboni/webbrowser-app/topsite-previews into lp:webbrowser-app
- topsite-previews
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Olivier Tilloy |
Approved revision: | 1204 |
Merged at revision: | 1228 |
Proposed branch: | lp:~uriboni/webbrowser-app/topsite-previews |
Merge into: | lp:webbrowser-app |
Diff against target: |
1917 lines (+957/-198) 25 files modified
src/app/webbrowser/Browser.qml (+37/-29) src/app/webbrowser/BrowserTab.qml (+23/-19) src/app/webbrowser/CMakeLists.txt (+1/-1) src/app/webbrowser/HistoryModel.qml (+0/-24) src/app/webbrowser/HistoryView.qml (+12/-4) src/app/webbrowser/HistoryViewWide.qml (+19/-10) src/app/webbrowser/NewTabView.qml (+55/-18) src/app/webbrowser/NewTabViewWide.qml (+15/-28) src/app/webbrowser/PreviewManager.qml (+91/-0) src/app/webbrowser/SettingsPage.qml (+2/-3) src/app/webbrowser/UrlPreviewDelegate.qml (+123/-0) src/app/webbrowser/UrlPreviewGrid.qml (+90/-0) src/app/webbrowser/file-operations.cpp (+7/-0) src/app/webbrowser/file-operations.h (+3/-0) src/app/webbrowser/qmldir (+1/-0) src/app/webbrowser/webbrowser-app.cpp (+8/-1) tests/autopilot/webbrowser_app/emulators/browser.py (+41/-8) tests/autopilot/webbrowser_app/tests/__init__.py (+12/-6) tests/autopilot/webbrowser_app/tests/test_new_tab_view.py (+5/-6) tests/autopilot/webbrowser_app/tests/test_site_previews.py (+168/-0) tests/unittests/qml/tst_BrowserTab.qml (+64/-1) tests/unittests/qml/tst_HistoryViewWide.qml (+12/-6) tests/unittests/qml/tst_NewTabViewWide.qml (+27/-32) tests/unittests/qml/tst_PreviewManager.qml (+117/-0) tests/unittests/qml/tst_QmlTests.cpp (+24/-2) |
To merge this branch: | bzr merge lp:~uriboni/webbrowser-app/topsite-previews |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Olivier Tilloy | Approve | ||
PS Jenkins bot | continuous-integration | Needs Fixing | |
Review via email: mp+269771@code.launchpad.net |
Commit message
Reimplement the top sites list to use a grid of previews in all form factors.
Description of the change
Reimplement the top sites list to use a grid of previews in all form factors.
PS Jenkins bot (ps-jenkins) wrote : | # |
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1160
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1160
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Olivier Tilloy (osomon) wrote : | # |
Please revert the changes to po/webbrowser-
Additionally, this branch has conflicts when merging into the latest trunk. Can you please resolve them?
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1162
http://
Executed test runs:
FAILURE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
FAILURE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1162
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1169
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1170
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1162
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1172
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1172
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1176
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1177
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1178
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1180
http://
Executed test runs:
UNSTABLE: http://
FAILURE: http://
SUCCESS: http://
deb: http://
FAILURE: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Olivier Tilloy (osomon) wrote : | # |
A first incomplete round of (mostly) functional review, with comments in no particular order:
In UrlPreviewDeleg
When launching the app, I’m seeing the following error in the console:
src/app/
When opening a new tab, I’m seeing the following error in the console:
src/app/
I don’t think this is a bug introduced by your changes, rather an issue that’s always been there, but it is now more visible: initially, the captures for my top sites grid are blank (expected). If from there I click on one top site to open it, wait for the page to load, then close the tab, and open a new tab again, there is no capture for the page that I just viewed. This is because a capture is not made when closing a tab (only when switching tabs). Not sure how much work this would require to implement, but worth taking a look anyway.
On a related note, can captures be removed when closing tabs? I thought this was implemented already, but it doesn’t seem to work.
The captures in the grid view look very blurry, is this because of downscaling, and if so can we do something to improve it?
In narrow mode, when dragging the grid view upwards, it overlaps with the list of bookmarks above. It needs to be clipped.
The context menu that appears on grid items when long-pressing/
Not a big deal as (AFAIK) keyboard navigation was never implemented in narrow views, but since the grid view handles it correctly, can the narrow new tab view be fixed to fully support keyboard nav? If it would require too many changes, I’m fine with leaving it for another branch.
Olivier Tilloy (osomon) wrote : | # |
I expressed a concern about app startup performance for the branch where you made the bookmarks model a singleton, and this holds true for the changes done to the history model in this branch. Probably even more so, because the history model is likely to grow substantially over time. Please do some profiling to confirm/invalidate.
In BrowserTab.qml, there are extraneous semi-colons at line ends in JS code, please remove them for consistency.
Olivier Tilloy (osomon) wrote : | # |
There used to be a good reason for making the name of the preview file a unique ID as opposed to a hash of the URL, but at the moment I fail to remember that good reason. The relevant revision is http://
Olivier Tilloy (osomon) wrote : | # |
I think I remember now why using a hash of the URl wasn’t a good idea: if in a given tab I browse to n different URLs (either by entering a new address or by clicking on links), and if in between each new navigation I switch to another tab and back, this is going to generate n different previews, which are not deleted until the next time the browser is started. The preview cache can easily grow big with unused previews that way, and this is not desirable on devices with a limited amount of storage.
Could the PreviewManager be made more flexible by accepting any string identifier instead of a URL? Previews for top sites could use the corresponding URL, while previews for tabs would use the tab’s unique ID.
Olivier Tilloy (osomon) wrote : | # |
In BrowserTab.qml, 'topSites' and 'cachedPreviewU
In HistoryViewWide
In NewTabView.qml, the comment to explain why there is a negative right margin makes sense, but instead of hardcoding it, couldn’t it be bound to "-contentColumn
In UrlPreviewGrid.qml, do you really need to implement keyboard navigation yourself? I thought GridView already implemented it.
PreviewManager.qml should import Ubuntu.Web 0.2, as it has a reference to cacheLocation.
src/app/
In tests/autopilot
In test_cleanup_
In tst_BrowserTab.qml, is the additional import of Ubuntu.Test really needed?
3 out of the 4 tests in webbrowser_
Ugo Riboni (uriboni) wrote : | # |
> A first incomplete round of (mostly) functional review, with comments in no
> particular order:
Fixed unless noted below:
> In UrlPreviewDeleg
> which has been fixed in the UITK, so the hack can now be cleaned up. Same in
> test_new_
We still can't use the CPO's method to click on an action because of another bug.
We can however get the action button by objectName, so I fixed that at least.
> I don’t think this is a bug introduced by your changes, rather an issue that’s
> always been there, but it is now more visible: initially, the captures for my
> top sites grid are blank (expected). If from there IÂ click on one top site to
> open it, wait for the page to load, then close the tab, and open a new tab
> again, there is no capture for the page that IÂ just viewed. This is because a
> capture is not made when closing a tab (only when switching tabs). Not sure
> how much work this would require to implement, but worth taking a look anyway.
The other case when the preview is not captured is when we navigate somewhere else, and while we might be able to delay the unloading of the tab, I don't know if we can't easily delay navigating away on a link click.
This idea of delaying hiding the tab is also making me a bit nervous anyway. Would it be too expensive CPU-wise to periodically call grabToImage without saving to disk, and saving to disk only when the tab stops being visible or the URL changes ?
> On a related note, can captures be removed when closing tabs? IÂ thought this
> was implemented already, but it doesn’t seem to work.
It is implemented and I tested it again and it works here.
Can you give me steps to reproduce this problem ?
Please keep in mind that when closing a tab, if the site is in the top sites its preview will not be deleted.
> The context menu that appears on grid items when long-pressing/
> can be dismissed when clicked outside, but the click is propagated to
> whatever’s underneath, so if I click anywhere to dismiss the menu I might
> activate a top site.
This sounds like a bug in the ActionSelection
I will investigate more and file a bug.
> Not a big deal as (AFAIK) keyboard navigation was never implemented in narrow
> views, but since the grid view handles it correctly, can the narrow new tab
> view be fixed to fully support keyboard nav? If it would require too many
> changes, I’m fine with leaving it for another branch.
The real problem with the narrow mode version is that it uses a Column+Repeater setup. I plan to convert that into a proper ListView to get better performance, and keyboard navigation will work almost "for free" after that.
So let's keep it for that other MR.
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1185
http://
Executed test runs:
UNSTABLE: http://
FAILURE: http://
SUCCESS: http://
deb: http://
FAILURE: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Ugo Riboni (uriboni) wrote : | # |
> In UrlPreviewGrid.qml, do you really need to implement keyboard navigation
> yourself? I thought GridView already implemented it.
What it does not implement is telling me when we are on item zero and pressing LEFT or when we are on any item on row zero and pressing UP. In these cases we need to signal that we want to give up focus so that it can go back to the address bar.
But the handlers for DOWN and RIGHT were indeed redundant so I removed them.
> PreviewManager.qml should import Ubuntu.Web 0.2, as it has a reference to
> cacheLocation.
I guess the reason why this worked anyway was that the context object exported by the Ubuntu.Web plugin is attached the first time the plugin is imported, and then accessible globally since all QML files in webbrowser-app share the same context.
I personally think that we should re-think our usage of context properties and context objects. They make things harder to unit test, since there does not seem to be a way to set up the same environment (with mocks replacing the original context properties) while setting up a unit test. Wouldn't it be better if instead the plugins would export their globals as part of explicit singletons, which can then easily be extended and/or replaced while unit testing ?
Referencing cacheLocation or globals.
Anyway, fixed here, but we should discuss this.
> In test_cleanup_
> can we be sure that 0.5 seconds is enough?
We really can't. Like all tests that are checking that something does *not* happen, we have to either make a guess, or just decide that we can't test this particular feature.
I am happy to take your guess over mine, if you want to suggest a different value, or have a more rationally explainable value to propose.
> 3 out of the 4 tests in webbrowser_
> mode because they silently assume wide mode (in narrow mode, before calling
> self.open_
In the process of fixing this
Olivier Tilloy (osomon) wrote : | # |
In narrow mode, the highlight for top site previews is cut off on the left hand side for the leftmost column.
Also, keyboard navigation with arrows doesn’t ensure that the currently highlighted preview is visible (if e.g. the last row is out of sight and I navigate down to it with the down arrow key, it should come into view).
Olivier Tilloy (osomon) wrote : | # |
I can reliably reproduce the following issue:
- ensure that webbrowser-app is not running
- rm $HOME/.
- launch webbrowser-app, open a new tab, verify that there are no captures
- open each top site in a new tab, and ensure that they all have a capture displayed
- close all tabs, when the last tab is closed, the app quits
- verify that you have (at least) 10 captures in $HOME/.
- launch webbrowser-app again, and open a new tab
- the captures are gone, for some (wrong) reason they’ve been purged from the cache
Olivier Tilloy (osomon) wrote : | # |
Revision 1195 fixes the issue indeed, but is a bit convoluted. Given that the session restore code is already itself triggered by a Timer with a 1msec interval to ensure it’s out of the critical startup path, maybe the databasePath of the history model could be set in that same timer’s onTriggered (after restoring the open tabs). That would avoid the need for a 'sessionRestore
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1194
http://
Executed test runs:
UNSTABLE: http://
FAILURE: http://
SUCCESS: http://
deb: http://
FAILURE: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Ugo Riboni (uriboni) wrote : | # |
> Revision 1195 fixes the issue indeed, but is a bit convoluted. Given that the
> session restore code is already itself triggered by a Timer with a 1msec
> interval to ensure it’s out of the critical startup path, maybe the
> databasePath of the history model could be set in that same timer’s
> onTriggered (after restoring the open tabs). That would avoid the need for a
> 'sessionRestore
> initialCaptures
I fixed this but we still need getOpenPages because the open tabs are a combination of restored tabs and initialTabs (which I think come from the command line)
Ugo Riboni (uriboni) wrote : | # |
> In narrow mode, the highlight for top site previews is cut off on the left
> hand side for the leftmost column.
Was due to clipping. Fixed.
> Also, keyboard navigation with arrows doesn’t ensure that the currently
> highlighted preview is visible (if e.g. the last row is out of sight and I
> navigate down to it with the down arrow key, it should come into view).
The issue here is that we are using a flickable around the GridView, and the GridView height equals its contentHeight, so as far as the GridView is concerned there is nothing to scroll.
I will fix this when adding full support for keyboard nav to the narrow view list (and replacing columns+repeaters with listviews) in a separate MR.
Olivier Tilloy (osomon) wrote : | # |
> I will fix this when adding full support for keyboard nav to the narrow view
> list (and replacing columns+repeaters with listviews) in a separate MR.
Okay.
Olivier Tilloy (osomon) wrote : | # |
A couple of minor comments, we’re definitely getting there!
- Given that internal.
- The interactive property of the grid view in narrow mode should be set to false, otherwise if I start flicking on the grid only the grid moves, whereas I would expect the entire page to move.
I haven’t tested on a device yet (to ensure no functional regressions in narrow mode on touch), will do when CI generates packages for the latest revision.
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1195
http://
Executed test runs:
UNSTABLE: http://
FAILURE: http://
SUCCESS: http://
deb: http://
FAILURE: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1201
http://
Executed test runs:
SUCCESS: http://
FAILURE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Olivier Tilloy (osomon) wrote : | # |
Testing on arale, and I can confirm that the fact that the gridview is flickable inside the containing flickable is annoying, if IÂ scroll all the way down I cannot scroll back up to see the list of bookmarks.
I’m also seeing that the capture images are vertically centered inside the ubuntu shape, which sometimes makes it hard to identify a page by its capture. An extreme example is http://
I would expect the captures to be top-aligned, I think it’s generally easier to recognize a page when seeing its topmost part.
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1203
http://
Executed test runs:
SUCCESS: http://
FAILURE: http://
SUCCESS: http://
deb: http://
FAILURE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Olivier Tilloy (osomon) wrote : | # |
LGTM now, thanks!
Olivier Tilloy (osomon) wrote : | # |
webbrowser_
Olivier Tilloy (osomon) : | # |
Preview Diff
1 | === modified file 'src/app/webbrowser/Browser.qml' |
2 | --- src/app/webbrowser/Browser.qml 2015-09-28 08:15:10 +0000 |
3 | +++ src/app/webbrowser/Browser.qml 2015-10-13 11:21:13 +0000 |
4 | @@ -25,8 +25,9 @@ |
5 | import webbrowserapp.private 0.1 |
6 | import webbrowsercommon.private 0.1 |
7 | import "../actions" as Actions |
8 | +import "../UrlUtils.js" as UrlUtils |
9 | import ".." |
10 | -import "../UrlUtils.js" as UrlUtils |
11 | +import "." |
12 | |
13 | BrowserView { |
14 | id: browser |
15 | @@ -36,7 +37,6 @@ |
16 | |
17 | currentWebview: tabsModel && tabsModel.currentTab ? tabsModel.currentTab.webview : null |
18 | |
19 | - property var historyModel: (historyModelLoader.status == Loader.Ready) ? historyModelLoader.item : null |
20 | property var bookmarksModel: (bookmarksModelLoader.status == Loader.Ready) ? bookmarksModelLoader.item : null |
21 | |
22 | property bool newSession: false |
23 | @@ -99,8 +99,7 @@ |
24 | onTriggered: browser.openUrlInNewTab("", true) |
25 | }, |
26 | Actions.ClearHistory { |
27 | - enabled: browser.historyModel |
28 | - onTriggered: browser.historyModel.clearAll() |
29 | + onTriggered: HistoryModel.clearAll() |
30 | }, |
31 | Actions.FindInPage { |
32 | enabled: !chrome.findInPageMode && !newTabViewLoader.active |
33 | @@ -242,7 +241,6 @@ |
34 | |
35 | NewTabView { |
36 | anchors.fill: parent |
37 | - historyModel: browser.historyModel |
38 | bookmarksModel: browser.bookmarksModel |
39 | settingsObject: settings |
40 | focus: true |
41 | @@ -265,7 +263,6 @@ |
42 | |
43 | NewTabViewWide { |
44 | anchors.fill: parent |
45 | - historyModel: browser.historyModel |
46 | bookmarksModel: browser.bookmarksModel |
47 | settingsObject: settings |
48 | focus: true |
49 | @@ -374,7 +371,6 @@ |
50 | objectName: "history" |
51 | text: i18n.tr("History") |
52 | iconName: "history" |
53 | - enabled: browser.historyModel |
54 | onTriggered: historyViewLoader.active = true |
55 | }, |
56 | Action { |
57 | @@ -492,7 +488,7 @@ |
58 | readonly property string icon: "history" |
59 | readonly property bool displayUrl: true |
60 | sourceModel: TextSearchFilterModel { |
61 | - sourceModel: browser.historyModel |
62 | + sourceModel: HistoryModel |
63 | terms: suggestionsList.searchTerms |
64 | searchFields: ["url", "title"] |
65 | } |
66 | @@ -771,7 +767,7 @@ |
67 | |
68 | onStatusChanged: { |
69 | if (status == Loader.Ready) { |
70 | - historyViewTimer.restart() |
71 | + historyViewLoader.item.loadModel() |
72 | historyViewLoader.item.forceActiveFocus() |
73 | } else { |
74 | internal.resetFocus() |
75 | @@ -782,14 +778,6 @@ |
76 | |
77 | onActiveChanged: if (active) chrome.findInPageMode = false |
78 | |
79 | - Timer { |
80 | - id: historyViewTimer |
81 | - // Set the model asynchronously to ensure |
82 | - // the view is displayed as early as possible. |
83 | - interval: 1 |
84 | - onTriggered: historyViewLoader.item.historyModel = browser.historyModel |
85 | - } |
86 | - |
87 | Component { |
88 | id: historyViewComponent |
89 | |
90 | @@ -822,7 +810,7 @@ |
91 | if (count == 1) { |
92 | done() |
93 | } |
94 | - browser.historyModel.removeEntryByUrl(url) |
95 | + HistoryModel.removeEntryByUrl(url) |
96 | } |
97 | onDone: destroy() |
98 | } |
99 | @@ -846,6 +834,7 @@ |
100 | browser.openUrlInNewTab(url, true) |
101 | done() |
102 | } |
103 | + |
104 | onNewTabRequested: browser.openUrlInNewTab("", true) |
105 | onDone: historyViewLoader.active = false |
106 | } |
107 | @@ -864,7 +853,6 @@ |
108 | SettingsPage { |
109 | anchors.fill: parent |
110 | focus: true |
111 | - historyModel: browser.historyModel |
112 | settingsObject: settings |
113 | onDone: destroy() |
114 | Keys.onEscapePressed: { |
115 | @@ -901,12 +889,6 @@ |
116 | } |
117 | |
118 | Loader { |
119 | - id: historyModelLoader |
120 | - source: "HistoryModel.qml" |
121 | - asynchronous: true |
122 | - } |
123 | - |
124 | - Loader { |
125 | id: bookmarksModelLoader |
126 | source: "BookmarksModel.qml" |
127 | asynchronous: true |
128 | @@ -1128,8 +1110,9 @@ |
129 | return |
130 | } |
131 | |
132 | - if ((event.type == Oxide.LoadEvent.TypeSucceeded) && browser.historyModel && 300 > event.httpStatusCode && event.httpStatusCode >= 200) { |
133 | - browser.historyModel.add(event.url, title, icon) |
134 | + if (event.type == Oxide.LoadEvent.TypeSucceeded && |
135 | + 300 > event.httpStatusCode && event.httpStatusCode >= 200) { |
136 | + HistoryModel.add(event.url, title, icon) |
137 | } |
138 | } |
139 | |
140 | @@ -1227,6 +1210,15 @@ |
141 | QtObject { |
142 | id: internal |
143 | |
144 | + function getOpenPages() { |
145 | + var urls = []; |
146 | + for (var i = 0; i < tabsModel.count; i++) { |
147 | + var url = tabsModel.get(i).url |
148 | + if (url.length > 0) urls.push(url) // exclude "new tab" tabs |
149 | + } |
150 | + return urls; |
151 | + } |
152 | + |
153 | function instantiateShareComponent() { |
154 | var component = Qt.createComponent("../Share.qml") |
155 | if (component.status == Component.Ready) { |
156 | @@ -1490,8 +1482,18 @@ |
157 | } |
158 | } |
159 | |
160 | - // Delay instantiation of the first webview by 1 msec to allow initial |
161 | - // rendering to happen. Clumsy workaround for http://pad.lv/1359911. |
162 | + // Schedule various expensive tasks to a point after the initialization and |
163 | + // first rendering of the application have already happened. |
164 | + // |
165 | + // Scheduling a Timer with the shortest non-zero interval possible (1ms) will |
166 | + // effectively queue its onTriggered function to run immediately after anything |
167 | + // that is currently in the event loop queue at the moment the Timer starts. |
168 | + // |
169 | + // The tasks are: |
170 | + // - creating the webviews for all initial tabs. This should ideally be done |
171 | + // asynchronously via object incubation, but http://pad.lv/1359911 prevents it |
172 | + // - loading the HistoryModel from the database |
173 | + // - deleting any page screenshots that are no longer needed |
174 | Timer { |
175 | running: true |
176 | interval: 1 |
177 | @@ -1499,6 +1501,7 @@ |
178 | if (!browser.newSession && settings.restoreSession) { |
179 | session.restore() |
180 | } |
181 | + |
182 | // Sanity check |
183 | console.assert(tabsModel.count <= browser.maxTabsToRestore, |
184 | "WARNING: too many tabs were restored") |
185 | @@ -1512,6 +1515,11 @@ |
186 | if (!tabsModel.currentTab.url.toString() && !tabsModel.currentTab.restoreState && (formFactor == "desktop")) { |
187 | internal.focusAddressBar() |
188 | } |
189 | + |
190 | + HistoryModel.databasePath = dataLocation + "/history.sqlite" |
191 | + // Note that the property setter for databasePath won't return until |
192 | + // the entire model has been loaded, so it is safe to call this here |
193 | + PreviewManager.cleanUnusedPreviews(internal.getOpenPages()) |
194 | } |
195 | } |
196 | |
197 | |
198 | === modified file 'src/app/webbrowser/BrowserTab.qml' |
199 | --- src/app/webbrowser/BrowserTab.qml 2015-09-23 15:53:01 +0000 |
200 | +++ src/app/webbrowser/BrowserTab.qml 2015-10-13 11:21:13 +0000 |
201 | @@ -20,6 +20,7 @@ |
202 | import Ubuntu.Web 0.2 |
203 | import com.canonical.Oxide 1.4 as Oxide |
204 | import webbrowserapp.private 0.1 |
205 | +import "." |
206 | |
207 | FocusScope { |
208 | id: tab |
209 | @@ -40,6 +41,19 @@ |
210 | property bool current: false |
211 | property bool incognito |
212 | |
213 | + Connections { |
214 | + target: PreviewManager |
215 | + onPreviewSaved: { |
216 | + if (pageUrl !== url) return |
217 | + if (preview == previewUrl) { |
218 | + // Ensure that the preview URL actually changes, |
219 | + // for the image to be reloaded |
220 | + preview = "" |
221 | + } |
222 | + preview = previewUrl |
223 | + } |
224 | + } |
225 | + |
226 | FocusScope { |
227 | id: webviewContainer |
228 | anchors.fill: parent |
229 | @@ -81,9 +95,7 @@ |
230 | |
231 | function close() { |
232 | unload() |
233 | - if (preview && preview.toString()) { |
234 | - FileOperations.remove(preview) |
235 | - } |
236 | + PreviewManager.checkDelete(url) |
237 | destroy() |
238 | } |
239 | |
240 | @@ -110,6 +122,12 @@ |
241 | visible = false |
242 | return |
243 | } |
244 | + |
245 | + if (url.toString().length === 0) { |
246 | + visible = false |
247 | + return |
248 | + } |
249 | + |
250 | internal.hiding = true |
251 | webview.grabToImage(function(result) { |
252 | if (!internal.hiding) { |
253 | @@ -117,22 +135,8 @@ |
254 | } |
255 | internal.hiding = false |
256 | visible = false |
257 | - var capturesDir = cacheLocation + "/captures" |
258 | - if (!FileOperations.exists(Qt.resolvedUrl(capturesDir))) { |
259 | - FileOperations.mkpath(Qt.resolvedUrl(capturesDir)) |
260 | - } |
261 | - var filepath = capturesDir + "/" + uniqueId + ".jpg" |
262 | - if (result.saveToFile(filepath)) { |
263 | - var previewUrl = Qt.resolvedUrl(filepath) |
264 | - if (preview == previewUrl) { |
265 | - // Ensure that the preview URL actually changes, |
266 | - // for the image to be reloaded |
267 | - preview = "" |
268 | - } |
269 | - preview = previewUrl |
270 | - } else { |
271 | - preview = "" |
272 | - } |
273 | + |
274 | + PreviewManager.saveToDisk(result, url) |
275 | }) |
276 | } |
277 | } |
278 | |
279 | === modified file 'src/app/webbrowser/CMakeLists.txt' |
280 | --- src/app/webbrowser/CMakeLists.txt 2015-09-22 01:27:19 +0000 |
281 | +++ src/app/webbrowser/CMakeLists.txt 2015-10-13 11:21:13 +0000 |
282 | @@ -56,7 +56,7 @@ |
283 | install(TARGETS ${WEBBROWSER_APP} |
284 | RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) |
285 | |
286 | -file(GLOB QML_FILES *.qml *.js) |
287 | +file(GLOB QML_FILES *.qml qmldir *.js) |
288 | install(FILES ${QML_FILES} DESTINATION ${CMAKE_INSTALL_DATADIR}/webbrowser-app/webbrowser) |
289 | |
290 | install(DIRECTORY assets |
291 | |
292 | === removed file 'src/app/webbrowser/HistoryModel.qml' |
293 | --- src/app/webbrowser/HistoryModel.qml 2015-08-10 15:22:00 +0000 |
294 | +++ src/app/webbrowser/HistoryModel.qml 1970-01-01 00:00:00 +0000 |
295 | @@ -1,24 +0,0 @@ |
296 | -/* |
297 | - * Copyright 2014-2015 Canonical Ltd. |
298 | - * |
299 | - * This file is part of webbrowser-app. |
300 | - * |
301 | - * webbrowser-app is free software; you can redistribute it and/or modify |
302 | - * it under the terms of the GNU General Public License as published by |
303 | - * the Free Software Foundation; version 3. |
304 | - * |
305 | - * webbrowser-app is distributed in the hope that it will be useful, |
306 | - * but WITHOUT ANY WARRANTY; without even the implied warranty of |
307 | - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
308 | - * GNU General Public License for more details. |
309 | - * |
310 | - * You should have received a copy of the GNU General Public License |
311 | - * along with this program. If not, see <http://www.gnu.org/licenses/>. |
312 | - */ |
313 | - |
314 | -import QtQuick 2.4 |
315 | -import webbrowserapp.private 0.1 |
316 | - |
317 | -HistoryModel { |
318 | - databasePath: dataLocation + "/history.sqlite" |
319 | -} |
320 | |
321 | === modified file 'src/app/webbrowser/HistoryView.qml' |
322 | --- src/app/webbrowser/HistoryView.qml 2015-08-10 15:22:00 +0000 |
323 | +++ src/app/webbrowser/HistoryView.qml 2015-10-13 11:21:13 +0000 |
324 | @@ -23,8 +23,6 @@ |
325 | Item { |
326 | id: historyView |
327 | |
328 | - property alias historyModel: historyTimeframeModel.sourceModel |
329 | - |
330 | signal seeMoreEntriesClicked(var model) |
331 | signal done() |
332 | |
333 | @@ -33,6 +31,16 @@ |
334 | color: "#f6f6f6" |
335 | } |
336 | |
337 | + Timer { |
338 | + // Set the model asynchronously to ensure |
339 | + // the view is displayed as early as possible. |
340 | + id: loadModelTimer |
341 | + interval: 1 |
342 | + onTriggered: historyTimeframeModel.sourceModel = HistoryModel |
343 | + } |
344 | + |
345 | + function loadModel() { loadModelTimer.restart() } |
346 | + |
347 | ListView { |
348 | id: domainsListView |
349 | |
350 | @@ -75,7 +83,7 @@ |
351 | historyView.seeMoreEntriesClicked(model.entries) |
352 | } |
353 | } |
354 | - onRemoved: historyView.historyModel.removeEntriesByDomain(model.domain) |
355 | + onRemoved: HistoryModel.removeEntriesByDomain(model.domain) |
356 | onPressAndHold: { |
357 | selectMode = !selectMode |
358 | if (selectMode) { |
359 | @@ -220,7 +228,7 @@ |
360 | } |
361 | domainsListView.ViewItems.selectMode = false |
362 | for (var j in domains) { |
363 | - historyModel.removeEntriesByDomain(domains[j]) |
364 | + HistoryModel.removeEntriesByDomain(domains[j]) |
365 | } |
366 | } |
367 | } |
368 | |
369 | === modified file 'src/app/webbrowser/HistoryViewWide.qml' |
370 | --- src/app/webbrowser/HistoryViewWide.qml 2015-09-18 20:43:26 +0000 |
371 | +++ src/app/webbrowser/HistoryViewWide.qml 2015-10-13 11:21:13 +0000 |
372 | @@ -25,7 +25,6 @@ |
373 | FocusScope { |
374 | id: historyViewWide |
375 | |
376 | - property alias historyModel: historySearchModel.sourceModel |
377 | property bool searchMode: false |
378 | readonly property bool selectMode: urlsListView.ViewItems.selectMode |
379 | onSearchModeChanged: { |
380 | @@ -57,15 +56,16 @@ |
381 | internal.removeSelected() |
382 | } else { |
383 | if (urlsListView.activeFocus) { |
384 | - historyViewWide.historyModel.removeEntryByUrl(urlsListView.currentItem.siteUrl) |
385 | + HistoryModel.removeEntryByUrl(urlsListView.currentItem.siteUrl) |
386 | + |
387 | if (urlsListView.count == 0) { |
388 | lastVisitDateListView.currentIndex = 0 |
389 | } |
390 | } else { |
391 | if (lastVisitDateListView.currentIndex == 0) { |
392 | - historyViewWide.historyModel.clearAll() |
393 | + HistoryModel.clearAll() |
394 | } else { |
395 | - historyViewWide.historyModel.removeEntriesByDate(lastVisitDateListView.currentItem.lastVisitDate) |
396 | + HistoryModel.removeEntriesByDate(lastVisitDateListView.currentItem.lastVisitDate) |
397 | lastVisitDateListView.currentIndex = 0 |
398 | } |
399 | } |
400 | @@ -82,6 +82,16 @@ |
401 | anchors.fill: parent |
402 | } |
403 | |
404 | + Timer { |
405 | + // Set the model asynchronously to ensure |
406 | + // the view is displayed as early as possible. |
407 | + id: loadModelTimer |
408 | + interval: 1 |
409 | + onTriggered: historySearchModel.sourceModel = HistoryModel |
410 | + } |
411 | + |
412 | + function loadModel() { loadModelTimer.restart() } |
413 | + |
414 | TextSearchFilterModel { |
415 | id: historySearchModel |
416 | searchFields: ["title", "url"] |
417 | @@ -239,8 +249,8 @@ |
418 | // Until a valid HistoryModel is assigned the TextSearchFilterModel |
419 | // will not report role names, and the HistoryLastVisit*Models will emit warnings |
420 | // since they need a dateLastVisit role to be present. |
421 | - // We avoid this by assigning the sourceModel only when HistoryModel is ready. |
422 | - sourceModel: historyModel ? historySearchModel : undefined |
423 | + // We avoid this by delaying assigning the source model until it is ready. |
424 | + sourceModel: historySearchModel.sourceModel ? historySearchModel : undefined |
425 | } |
426 | |
427 | clip: true |
428 | @@ -309,7 +319,7 @@ |
429 | } |
430 | |
431 | onRemoved: { |
432 | - historyViewWide.historyModel.removeEntryByUrl(model.url) |
433 | + HistoryModel.removeEntryByUrl(model.url) |
434 | if (urlsListView.count == 0) { |
435 | lastVisitDateListView.currentIndex = 0 |
436 | } |
437 | @@ -360,8 +370,7 @@ |
438 | ToolbarAction { |
439 | objectName: "backButton" |
440 | |
441 | - visible: urlsListView.ViewItems.selectMode || |
442 | - historyViewWide.searchMode |
443 | + visible: historyViewWide.selectMode || historyViewWide.searchMode |
444 | |
445 | anchors { |
446 | top: parent.top |
447 | @@ -545,7 +554,7 @@ |
448 | |
449 | urlsListView.ViewItems.selectMode = false |
450 | for (var j in urls) { |
451 | - historyViewWide.historyModel.removeEntryByUrl(urls[j]) |
452 | + HistoryModel.removeEntryByUrl(urls[j]) |
453 | } |
454 | |
455 | lastVisitDateListView.forceActiveFocus() |
456 | |
457 | === modified file 'src/app/webbrowser/NewTabView.qml' |
458 | --- src/app/webbrowser/NewTabView.qml 2015-08-10 15:22:00 +0000 |
459 | +++ src/app/webbrowser/NewTabView.qml 2015-10-13 11:21:13 +0000 |
460 | @@ -21,12 +21,12 @@ |
461 | import Ubuntu.Components 1.3 |
462 | import webbrowserapp.private 0.1 |
463 | import ".." |
464 | +import "." |
465 | |
466 | Item { |
467 | id: newTabView |
468 | |
469 | property QtObject bookmarksModel |
470 | - property alias historyModel: historyTimeframeModel.sourceModel |
471 | property Settings settingsObject |
472 | |
473 | signal bookmarkClicked(url url) |
474 | @@ -37,6 +37,7 @@ |
475 | id: topSitesModel |
476 | sourceModel: HistoryTimeframeModel { |
477 | id: historyTimeframeModel |
478 | + sourceModel: HistoryModel |
479 | } |
480 | } |
481 | |
482 | @@ -148,7 +149,7 @@ |
483 | |
484 | active: internal.seeMoreBookmarksView |
485 | sourceComponent: BookmarksFolderListView { |
486 | - model: newTabView.bookmarksModel |
487 | + model: newTabView.bookmarksModel |
488 | |
489 | onBookmarkClicked: newTabView.bookmarkClicked(url) |
490 | onBookmarkRemoved: newTabView.bookmarkRemoved(url) |
491 | @@ -232,7 +233,7 @@ |
492 | height: units.gu(0.1) |
493 | anchors { |
494 | left: parent.left |
495 | - leftMargin: units.gu(1.5) |
496 | + leftMargin: units.gu(2) |
497 | right: parent.right |
498 | } |
499 | color: "#d3d3d3" |
500 | @@ -258,24 +259,60 @@ |
501 | color: UbuntuColors.darkGrey |
502 | } |
503 | |
504 | - UrlsList { |
505 | - objectName: "topSitesList" |
506 | + Item { |
507 | anchors { |
508 | left: parent.left |
509 | right: parent.right |
510 | - } |
511 | - |
512 | - opacity: internal.seeMoreBookmarksView ? 0.0 : 1.0 |
513 | - Behavior on opacity { UbuntuNumberAnimation {} } |
514 | - visible: opacity > 0 |
515 | - |
516 | - limit: 10 |
517 | - spacing: 0 |
518 | - |
519 | - model: topSitesModel |
520 | - |
521 | - onUrlClicked: newTabView.historyEntryClicked(url) |
522 | - onUrlRemoved: newTabView.historyModel.hide(url) |
523 | + |
524 | + // The UrlPreviewGrid's highlight extends to the left of the |
525 | + // grid itself by a margin. |
526 | + // Since we are clipping the parent we need to prevent the |
527 | + // highlight from being clipped away at the left edge. |
528 | + // We do this by shifting the parent left and the contents right |
529 | + // by an amount equal to the highlight's margin. |
530 | + leftMargin: units.gu(2) - grid.horizontalMargin |
531 | + |
532 | + // The right margin should be 2gu, which is set on all cells |
533 | + // of the UrlPreviewGrid already. However the parent Column |
534 | + // has 1.5gu right margin, so we are compensating for that |
535 | + // here instead of removing it from the Column itself and |
536 | + // reassigning it to all Column children except this one. |
537 | + rightMargin: - contentColumn.anchors.rightMargin |
538 | + } |
539 | + height: childrenRect.height |
540 | + clip: true |
541 | + |
542 | + UrlPreviewGrid { |
543 | + id: grid |
544 | + objectName: "topSitesList" |
545 | + anchors { |
546 | + left: parent.left |
547 | + leftMargin: grid.horizontalMargin |
548 | + right: parent.right |
549 | + top: parent.top |
550 | + topMargin: units.gu(2) |
551 | + } |
552 | + |
553 | + horizontalMargin: units.gu(1) |
554 | + verticalMargin: units.gu(1) |
555 | + |
556 | + opacity: internal.seeMoreBookmarksView ? 0.0 : 1.0 |
557 | + Behavior on opacity { UbuntuNumberAnimation {} } |
558 | + visible: opacity > 0 |
559 | + interactive: false |
560 | + |
561 | + model: LimitProxyModel { |
562 | + limit: 10 |
563 | + sourceModel: topSitesModel |
564 | + } |
565 | + showFavicons: false |
566 | + |
567 | + onActivated: newTabView.historyEntryClicked(url) |
568 | + onRemoved: { |
569 | + HistoryModel.hide(url) |
570 | + PreviewManager.checkDelete(url) |
571 | + } |
572 | + } |
573 | } |
574 | } |
575 | } |
576 | |
577 | === modified file 'src/app/webbrowser/NewTabViewWide.qml' |
578 | --- src/app/webbrowser/NewTabViewWide.qml 2015-09-08 09:28:47 +0000 |
579 | +++ src/app/webbrowser/NewTabViewWide.qml 2015-10-13 11:21:13 +0000 |
580 | @@ -21,12 +21,12 @@ |
581 | import Ubuntu.Components 1.3 |
582 | import webbrowserapp.private 0.1 |
583 | import ".." |
584 | +import "." |
585 | |
586 | FocusScope { |
587 | id: newTabViewWide |
588 | |
589 | property QtObject bookmarksModel |
590 | - property alias historyModel: historyTimeframeModel.sourceModel |
591 | property QtObject settingsObject |
592 | property alias selectedIndex: sections.selectedIndex |
593 | readonly property bool inBookmarksView: newTabViewWide.selectedIndex === 1 |
594 | @@ -57,6 +57,7 @@ |
595 | sourceModel: TopSitesModel { |
596 | sourceModel: HistoryTimeframeModel { |
597 | id: historyTimeframeModel |
598 | + sourceModel: HistoryModel |
599 | } |
600 | } |
601 | } |
602 | @@ -233,7 +234,7 @@ |
603 | flickableItem: bookmarksList |
604 | } |
605 | |
606 | - ListView { |
607 | + UrlPreviewGrid { |
608 | id: topSitesList |
609 | objectName: "topSitesList" |
610 | anchors { |
611 | @@ -241,36 +242,22 @@ |
612 | bottom: parent.bottom |
613 | left: parent.left |
614 | right: parent.right |
615 | - topMargin: units.gu(2) |
616 | + topMargin: units.gu(3) |
617 | + leftMargin: units.gu(4) |
618 | } |
619 | |
620 | visible: !inBookmarksView |
621 | - currentIndex: 0 |
622 | |
623 | model: topSitesModel |
624 | - delegate: UrlDelegateWide { |
625 | - objectName: "topSiteItem" |
626 | - clip: true |
627 | - |
628 | - title: model.title |
629 | - icon: model.icon |
630 | - url: model.url |
631 | - highlighted: topSitesList.activeFocus && ListView.isCurrentItem |
632 | - |
633 | - onClicked: newTabViewWide.historyEntryClicked(url) |
634 | - onRemoved: newTabViewWide.historyModel.hide(url) |
635 | - } |
636 | - |
637 | - Keys.onReturnPressed: newTabViewWide.historyEntryClicked(currentItem.url) |
638 | - Keys.onDeletePressed: { |
639 | - newTabViewWide.historyModel.hide(currentItem.url) |
640 | - if (topSitesList.model.count === 0) newTabViewWide.releasingKeyboardFocus() |
641 | - } |
642 | - Keys.onDownPressed: currentIndex = Math.min(currentIndex + 1, model.count - 1) |
643 | - Keys.onUpPressed: { |
644 | - if (currentIndex > 0) currentIndex = Math.max(currentIndex - 1, 0) |
645 | - else newTabViewWide.releasingKeyboardFocus() |
646 | - } |
647 | + showFavicons: true |
648 | + |
649 | + onActivated: newTabViewWide.historyEntryClicked(url) |
650 | + onRemoved: { |
651 | + HistoryModel.hide(url) |
652 | + if (topSitesModel.count === 0) newTabViewWide.releasingKeyboardFocus() |
653 | + PreviewManager.checkDelete(url) |
654 | + } |
655 | + onReleasingKeyboardFocus: newTabViewWide.releasingKeyboardFocus() |
656 | } |
657 | |
658 | Scrollbar { |
659 | @@ -293,7 +280,7 @@ |
660 | anchors { |
661 | left: parent.left |
662 | top: parent.top |
663 | - leftMargin: units.gu(1) |
664 | + leftMargin: units.gu(2) |
665 | } |
666 | |
667 | selectedIndex: settingsObject.newTabDefaultSection |
668 | |
669 | === added file 'src/app/webbrowser/PreviewManager.qml' |
670 | --- src/app/webbrowser/PreviewManager.qml 1970-01-01 00:00:00 +0000 |
671 | +++ src/app/webbrowser/PreviewManager.qml 2015-10-13 11:21:13 +0000 |
672 | @@ -0,0 +1,91 @@ |
673 | +/* |
674 | + * Copyright 2015 Canonical Ltd. |
675 | + * |
676 | + * This file is part of webbrowser-app. |
677 | + * |
678 | + * webbrowser-app is free software; you can redistribute it and/or modify |
679 | + * it under the terms of the GNU General Public License as published by |
680 | + * the Free Software Foundation; version 3. |
681 | + * |
682 | + * webbrowser-app is distributed in the hope that it will be useful, |
683 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
684 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
685 | + * GNU General Public License for more details. |
686 | + * |
687 | + * You should have received a copy of the GNU General Public License |
688 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
689 | + */ |
690 | + |
691 | +pragma Singleton |
692 | + |
693 | +import QtQuick 2.4 |
694 | +import Ubuntu.Web 0.2 |
695 | +import webbrowserapp.private 0.1 |
696 | + |
697 | +Item { |
698 | + property string capturesDir: cacheLocation + "/captures" |
699 | + signal previewSaved(url pageUrl, url previewUrl) |
700 | + |
701 | + LimitProxyModel { |
702 | + id: topSites |
703 | + limit: 10 |
704 | + sourceModel: TopSitesModel { |
705 | + sourceModel: HistoryTimeframeModel { |
706 | + sourceModel: HistoryModel |
707 | + } |
708 | + } |
709 | + function contains(url) { |
710 | + for (var i = 0; i < topSites.count; i++) { |
711 | + if (topSites.get(i).url == url) return true |
712 | + } |
713 | + return false |
714 | + } |
715 | + function containsHash(hash) { |
716 | + for (var i = 0; i < topSites.count; i++) { |
717 | + if (Qt.md5(topSites.get(i).url) == hash) return true |
718 | + } |
719 | + return false |
720 | + } |
721 | + } |
722 | + |
723 | + function previewPathFromUrl(url) { |
724 | + return "%1/%2.jpg".arg(capturesDir).arg(Qt.md5(url)) |
725 | + } |
726 | + |
727 | + function saveToDisk(data, url) { |
728 | + if (!FileOperations.exists(Qt.resolvedUrl(capturesDir))) { |
729 | + FileOperations.mkpath(Qt.resolvedUrl(capturesDir)) |
730 | + } |
731 | + |
732 | + var filepath = previewPathFromUrl(url) |
733 | + var previewUrl = "" |
734 | + if (data.saveToFile(filepath)) previewUrl = Qt.resolvedUrl(filepath) |
735 | + else console.warn("Failed to save preview to disk for %1 (path is %2)".arg(url).arg(filepath)) |
736 | + |
737 | + previewSaved(url, previewUrl) |
738 | + } |
739 | + |
740 | + |
741 | + function checkDelete(url) { |
742 | + if (!topSites.contains(url)) { |
743 | + FileOperations.remove(Qt.resolvedUrl(previewPathFromUrl(url))) |
744 | + } |
745 | + } |
746 | + |
747 | + // Remove all previews stored on disk that are not part of the top sites |
748 | + // and that are not for URLs in the doNotCleanUrls list |
749 | + function cleanUnusedPreviews(doNotCleanUrls) { |
750 | + var dir = Qt.resolvedUrl(capturesDir); |
751 | + var previews = FileOperations.filesInDirectory(dir, ["*.jpg"]) |
752 | + var doNotCleanHashes = doNotCleanUrls.map(function(url) { return Qt.md5(url) }) |
753 | + |
754 | + for (var i = 0; i < previews.length; i++) { |
755 | + var hash = previews[i].replace('.jpg', '') |
756 | + if (!topSites.containsHash(hash) && |
757 | + doNotCleanHashes.indexOf(hash) === -1) { |
758 | + var file = Qt.resolvedUrl("%1/%2.jpg".arg(capturesDir).arg(hash)) |
759 | + FileOperations.remove(file) |
760 | + } |
761 | + } |
762 | + } |
763 | +} |
764 | |
765 | === modified file 'src/app/webbrowser/SettingsPage.qml' |
766 | --- src/app/webbrowser/SettingsPage.qml 2015-08-18 08:49:24 +0000 |
767 | +++ src/app/webbrowser/SettingsPage.qml 2015-10-13 11:21:13 +0000 |
768 | @@ -29,7 +29,6 @@ |
769 | Item { |
770 | id: settingsItem |
771 | |
772 | - property QtObject historyModel |
773 | property Settings settingsObject |
774 | |
775 | signal done() |
776 | @@ -247,10 +246,10 @@ |
777 | ListItem.Standard { |
778 | objectName: "privacy.clearHistory" |
779 | text: i18n.tr("Clear Browsing History") |
780 | - enabled: historyModel.count > 0 |
781 | + enabled: HistoryModel.count > 0 |
782 | onClicked: { |
783 | var dialog = PopupUtils.open(privacyConfirmDialogComponent, privacyItem, {"title": i18n.tr("Clear Browsing History?")}) |
784 | - dialog.confirmed.connect(historyModel.clearAll) |
785 | + dialog.confirmed.connect(HistoryModel.clearAll) |
786 | } |
787 | } |
788 | |
789 | |
790 | === added file 'src/app/webbrowser/UrlPreviewDelegate.qml' |
791 | --- src/app/webbrowser/UrlPreviewDelegate.qml 1970-01-01 00:00:00 +0000 |
792 | +++ src/app/webbrowser/UrlPreviewDelegate.qml 2015-10-13 11:21:13 +0000 |
793 | @@ -0,0 +1,123 @@ |
794 | +/* |
795 | + * Copyright 2015 Canonical Ltd. |
796 | + * |
797 | + * This file is part of webbrowser-app. |
798 | + * |
799 | + * webbrowser-app is free software; you can redistribute it and/or modify |
800 | + * it under the terms of the GNU General Public License as published by |
801 | + * the Free Software Foundation; version 3. |
802 | + * |
803 | + * webbrowser-app is distributed in the hope that it will be useful, |
804 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
805 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
806 | + * GNU General Public License for more details. |
807 | + * |
808 | + * You should have received a copy of the GNU General Public License |
809 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
810 | + */ |
811 | + |
812 | +import QtQuick 2.4 |
813 | +import Ubuntu.Components 1.3 |
814 | +import Ubuntu.Components.Popups 1.3 |
815 | +import webbrowserapp.private 0.1 |
816 | +import ".." |
817 | +import "." |
818 | + |
819 | +AbstractButton { |
820 | + id: preview |
821 | + |
822 | + property url icon |
823 | + property alias title: titleLabel.text |
824 | + property url url |
825 | + property bool showFavicon: true |
826 | + |
827 | + property alias previewHeight: previewShape.height |
828 | + property alias previewWidth: previewShape.width |
829 | + |
830 | + signal removed() |
831 | + |
832 | + onPressAndHold: PopupUtils.open(contextMenuComponent, previewShape) |
833 | + |
834 | + Column { |
835 | + id: contentColumn |
836 | + anchors.left: parent.left |
837 | + anchors.top: parent.top |
838 | + spacing: units.gu(1) |
839 | + |
840 | + Item { |
841 | + anchors.left: parent.left |
842 | + anchors.right: parent.right |
843 | + height: titleLabel.height |
844 | + |
845 | + Loader { |
846 | + id: favicon |
847 | + anchors.left: parent.left |
848 | + anchors.verticalCenter: parent.verticalCenter |
849 | + sourceComponent: Favicon { |
850 | + source: preview.icon |
851 | + anchors.left: parent.left |
852 | + anchors.verticalCenter: parent.verticalCenter |
853 | + } |
854 | + active: preview.showFavicon |
855 | + } |
856 | + |
857 | + Label { |
858 | + id: titleLabel |
859 | + anchors.left: favicon.right |
860 | + anchors.leftMargin: showFavicon ? units.gu(1) : 0 |
861 | + anchors.right: parent.right |
862 | + anchors.top: parent.top |
863 | + text: preview.title |
864 | + elide: Text.ElideRight |
865 | + fontSize: "small" |
866 | + } |
867 | + } |
868 | + |
869 | + UbuntuShape { |
870 | + id: previewShape |
871 | + anchors.left: parent.left |
872 | + width: units.gu(26) |
873 | + height: units.gu(16) |
874 | + |
875 | + source: Image { |
876 | + id: previewImage |
877 | + source: FileOperations.exists(previewShape.previewUrl) ? previewShape.previewUrl : "" |
878 | + sourceSize.width: previewShape.width |
879 | + cache: false |
880 | + } |
881 | + sourceFillMode: UbuntuShape.PreserveAspectCrop |
882 | + sourceHorizontalAlignment: UbuntuShape.AlignLeft |
883 | + sourceVerticalAlignment: UbuntuShape.AlignTop |
884 | + |
885 | + property url previewUrl: Qt.resolvedUrl(PreviewManager.previewPathFromUrl(preview.url)) |
886 | + |
887 | + Connections { |
888 | + target: PreviewManager |
889 | + onPreviewSaved: { |
890 | + if (pageUrl !== preview.url) return |
891 | + previewImage.source = "" |
892 | + previewImage.source = previewShape.previewUrl |
893 | + } |
894 | + } |
895 | + } |
896 | + } |
897 | + |
898 | + MouseArea { |
899 | + anchors.fill: contentColumn |
900 | + acceptedButtons: Qt.RightButton |
901 | + onClicked: PopupUtils.open(contextMenuComponent, previewShape) |
902 | + } |
903 | + |
904 | + Component { |
905 | + id: contextMenuComponent |
906 | + ActionSelectionPopover { |
907 | + actions: ActionList { |
908 | + Action { |
909 | + objectName: "delete" |
910 | + text: i18n.tr("Remove") |
911 | + onTriggered: preview.removed() |
912 | + } |
913 | + } |
914 | + } |
915 | + } |
916 | +} |
917 | |
918 | === added file 'src/app/webbrowser/UrlPreviewGrid.qml' |
919 | --- src/app/webbrowser/UrlPreviewGrid.qml 1970-01-01 00:00:00 +0000 |
920 | +++ src/app/webbrowser/UrlPreviewGrid.qml 2015-10-13 11:21:13 +0000 |
921 | @@ -0,0 +1,90 @@ |
922 | +/* |
923 | + * Copyright 2015 Canonical Ltd. |
924 | + * |
925 | + * This file is part of webbrowser-app. |
926 | + * |
927 | + * webbrowser-app is free software; you can redistribute it and/or modify |
928 | + * it under the terms of the GNU General Public License as published by |
929 | + * the Free Software Foundation; version 3. |
930 | + * |
931 | + * webbrowser-app is distributed in the hope that it will be useful, |
932 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
933 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
934 | + * GNU General Public License for more details. |
935 | + * |
936 | + * You should have received a copy of the GNU General Public License |
937 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
938 | + */ |
939 | + |
940 | +import QtQuick 2.4 |
941 | +import Qt.labs.settings 1.0 |
942 | +import Ubuntu.Components 1.3 |
943 | +import webbrowserapp.private 0.1 |
944 | +import ".." |
945 | + |
946 | +GridView { |
947 | + id: grid |
948 | + |
949 | + property bool showFavicons: true |
950 | + property int horizontalMargin: units.gu(3) |
951 | + property int verticalMargin: units.gu(2.5) |
952 | + property int previewWidth: units.gu(17) |
953 | + property int previewHeight: units.gu(10) |
954 | + |
955 | + signal activated(url url) |
956 | + signal removed(url url) |
957 | + signal releasingKeyboardFocus() |
958 | + |
959 | + currentIndex: 0 |
960 | + |
961 | + cellWidth: previewWidth + horizontalMargin * 2 |
962 | + cellHeight: previewHeight + verticalMargin * 2 + units.gu(4) // height of text + favicon + margins in delegate |
963 | + |
964 | + implicitHeight: contentItem.childrenRect.height |
965 | + |
966 | + delegate: UrlPreviewDelegate { |
967 | + objectName: "topSiteItem" |
968 | + width: grid.cellWidth |
969 | + height: grid.cellHeight |
970 | + |
971 | + title: model.title |
972 | + icon: model.icon |
973 | + url: model.url |
974 | + showFavicon: grid.showFavicons |
975 | + |
976 | + previewHeight: grid.previewHeight |
977 | + previewWidth: grid.previewWidth |
978 | + |
979 | + onClicked: grid.activated(model.url) |
980 | + onRemoved: grid.removed(model.url) |
981 | + } |
982 | + |
983 | + highlight: Component { |
984 | + Item { |
985 | + visible: grid.activeFocus |
986 | + UbuntuShape { |
987 | + anchors.fill: parent |
988 | + anchors.leftMargin: - grid.horizontalMargin |
989 | + anchors.rightMargin: grid.horizontalMargin |
990 | + anchors.topMargin: - grid.verticalMargin |
991 | + anchors.bottomMargin: grid.verticalMargin |
992 | + aspect: UbuntuShape.Flat |
993 | + backgroundColor: Qt.rgba(0, 0, 0, 0.05) |
994 | + } |
995 | + } |
996 | + } |
997 | + |
998 | + Keys.onDeletePressed: removed(currentItem.url) |
999 | + |
1000 | + Keys.onLeftPressed: { |
1001 | + var i = grid.currentIndex |
1002 | + grid.moveCurrentIndexLeft() |
1003 | + if (i === grid.currentIndex) grid.releasingKeyboardFocus() |
1004 | + } |
1005 | + |
1006 | + Keys.onUpPressed: { |
1007 | + var i = grid.currentIndex |
1008 | + grid.moveCurrentIndexUp() |
1009 | + if (i === grid.currentIndex) grid.releasingKeyboardFocus() |
1010 | + } |
1011 | +} |
1012 | |
1013 | === modified file 'src/app/webbrowser/file-operations.cpp' |
1014 | --- src/app/webbrowser/file-operations.cpp 2015-02-18 20:31:37 +0000 |
1015 | +++ src/app/webbrowser/file-operations.cpp 2015-10-13 11:21:13 +0000 |
1016 | @@ -43,3 +43,10 @@ |
1017 | { |
1018 | return QDir::root().mkpath(path.toLocalFile()); |
1019 | } |
1020 | + |
1021 | +QStringList FileOperations::filesInDirectory(const QUrl& directory, |
1022 | + const QStringList& filters) const |
1023 | +{ |
1024 | + return QDir(directory.toLocalFile()).entryList(filters, |
1025 | + QDir::Files, QDir::Unsorted); |
1026 | +} |
1027 | |
1028 | === modified file 'src/app/webbrowser/file-operations.h' |
1029 | --- src/app/webbrowser/file-operations.h 2015-02-18 20:31:37 +0000 |
1030 | +++ src/app/webbrowser/file-operations.h 2015-10-13 11:21:13 +0000 |
1031 | @@ -20,6 +20,7 @@ |
1032 | #define __FILE_OPERATIONS_H__ |
1033 | |
1034 | #include <QtCore/QObject> |
1035 | +#include <QtCore/QStringList> |
1036 | |
1037 | class QUrl; |
1038 | |
1039 | @@ -33,6 +34,8 @@ |
1040 | Q_INVOKABLE bool exists(const QUrl& path) const; |
1041 | Q_INVOKABLE bool remove(const QUrl& file) const; |
1042 | Q_INVOKABLE bool mkpath(const QUrl& path) const; |
1043 | + Q_INVOKABLE QStringList filesInDirectory(const QUrl& directory, |
1044 | + const QStringList& filters) const; |
1045 | }; |
1046 | |
1047 | #endif // __FILE_OPERATIONS_H__ |
1048 | |
1049 | === added file 'src/app/webbrowser/qmldir' |
1050 | --- src/app/webbrowser/qmldir 1970-01-01 00:00:00 +0000 |
1051 | +++ src/app/webbrowser/qmldir 2015-10-13 11:21:13 +0000 |
1052 | @@ -0,0 +1,1 @@ |
1053 | +singleton PreviewManager 1.0 PreviewManager.qml |
1054 | |
1055 | === modified file 'src/app/webbrowser/webbrowser-app.cpp' |
1056 | --- src/app/webbrowser/webbrowser-app.cpp 2015-08-19 13:16:06 +0000 |
1057 | +++ src/app/webbrowser/webbrowser-app.cpp 2015-10-13 11:21:13 +0000 |
1058 | @@ -64,10 +64,17 @@ |
1059 | return new CacheDeleter(); |
1060 | } |
1061 | |
1062 | +static QObject* HistoryModel_singleton_factory(QQmlEngine* engine, QJSEngine* scriptEngine) |
1063 | +{ |
1064 | + Q_UNUSED(engine); |
1065 | + Q_UNUSED(scriptEngine); |
1066 | + return new HistoryModel(); |
1067 | +} |
1068 | + |
1069 | bool WebbrowserApp::initialize() |
1070 | { |
1071 | const char* uri = "webbrowserapp.private"; |
1072 | - qmlRegisterType<HistoryModel>(uri, 0, 1, "HistoryModel"); |
1073 | + qmlRegisterSingletonType<HistoryModel>(uri, 0, 1, "HistoryModel", HistoryModel_singleton_factory); |
1074 | qmlRegisterType<HistoryTimeframeModel>(uri, 0, 1, "HistoryTimeframeModel"); |
1075 | qmlRegisterType<TopSitesModel>(uri, 0 , 1, "TopSitesModel"); |
1076 | qmlRegisterType<HistoryDomainListModel>(uri, 0, 1, "HistoryDomainListModel"); |
1077 | |
1078 | === modified file 'tests/autopilot/webbrowser_app/emulators/browser.py' |
1079 | --- tests/autopilot/webbrowser_app/emulators/browser.py 2015-09-29 10:59:50 +0000 |
1080 | +++ tests/autopilot/webbrowser_app/emulators/browser.py 2015-10-13 11:21:13 +0000 |
1081 | @@ -185,6 +185,18 @@ |
1082 | else: |
1083 | return self.wait_select_single(ContextMenuMobile) |
1084 | |
1085 | + def open_item_context_menu_on_item(self, item, menuClass): |
1086 | + cx = item.globalRect.x + item.globalRect.width // 2 |
1087 | + cy = item.globalRect.y + item.globalRect.height // 2 |
1088 | + self.pointing_device.move(cx, cy) |
1089 | + if model() == 'Desktop': |
1090 | + self.pointing_device.click(button=3) |
1091 | + else: |
1092 | + self.pointing_device.press() |
1093 | + time.sleep(1.5) |
1094 | + self.pointing_device.release() |
1095 | + return self.wait_select_single(menuClass) |
1096 | + |
1097 | def open_context_menu(self): |
1098 | webview = self.get_current_webview() |
1099 | chrome = self.chrome |
1100 | @@ -519,7 +531,7 @@ |
1101 | return self.select_single(UrlsList, objectName="bookmarksList") |
1102 | |
1103 | def get_top_sites_list(self): |
1104 | - return self.select_single(UrlsList, objectName="topSitesList") |
1105 | + return self.select_single(UrlPreviewGrid, objectName="topSitesList") |
1106 | |
1107 | def get_notopsites_label(self): |
1108 | return self.select_single(objectName="notopsites") |
1109 | @@ -558,11 +570,7 @@ |
1110 | |
1111 | def get_top_sites_list(self): |
1112 | self.go_to_section(0) |
1113 | - list = self.select_single(uitk.QQuickListView, |
1114 | - objectName="topSitesList") |
1115 | - return sorted(list.select_many("UrlDelegateWide", |
1116 | - objectName="topSiteItem"), |
1117 | - key=lambda delegate: delegate.globalRect.y) |
1118 | + return self.select_single(UrlPreviewGrid, objectName="topSitesList") |
1119 | |
1120 | def get_folders_list(self): |
1121 | self.go_to_section(1) |
1122 | @@ -572,8 +580,7 @@ |
1123 | key=lambda delegate: delegate.globalRect.y) |
1124 | |
1125 | def get_top_site_items(self): |
1126 | - self.go_to_section(0) |
1127 | - return self.get_top_sites_list() |
1128 | + return self.get_top_sites_list().get_delegates() |
1129 | |
1130 | def get_bookmarks(self, folder_name): |
1131 | folders = self.get_folders_list() |
1132 | @@ -597,6 +604,16 @@ |
1133 | return [delegate.url for delegate in self.get_delegates()] |
1134 | |
1135 | |
1136 | +class UrlPreviewGrid(uitk.UbuntuUIToolkitCustomProxyObjectBase): |
1137 | + |
1138 | + def get_delegates(self): |
1139 | + return sorted(self.select_many("UrlPreviewDelegate"), |
1140 | + key=lambda delegate: delegate.globalRect.y) |
1141 | + |
1142 | + def get_urls(self): |
1143 | + return [delegate.url for delegate in self.get_delegates()] |
1144 | + |
1145 | + |
1146 | class UrlDelegate(uitk.UCListItem): |
1147 | |
1148 | pass |
1149 | @@ -607,6 +624,22 @@ |
1150 | pass |
1151 | |
1152 | |
1153 | +class UrlPreviewDelegate(uitk.UbuntuUIToolkitCustomProxyObjectBase): |
1154 | + |
1155 | + def hide_from_history(self, root): |
1156 | + menu = root.open_item_context_menu_on_item(self, |
1157 | + "ActionSelectionPopover") |
1158 | + |
1159 | + # Note: we can't still use the click_action_button method of |
1160 | + # ActionSelectionPopover's CPO, because it will crash if we delete the |
1161 | + # menu as a reaction to the click (which is the case here). |
1162 | + # However at least we can select the action button by objectName now. |
1163 | + # See bug http://pad.lv/1504189 |
1164 | + delete_item = menu.wait_select_single(objectName="delete_button") |
1165 | + self.pointing_device.click_object(delete_item) |
1166 | + menu.wait_until_destroyed() |
1167 | + |
1168 | + |
1169 | class DraggableUrlDelegateWide(UrlDelegateWide): |
1170 | |
1171 | def get_grip(self): |
1172 | |
1173 | === modified file 'tests/autopilot/webbrowser_app/tests/__init__.py' |
1174 | --- tests/autopilot/webbrowser_app/tests/__init__.py 2015-09-03 09:46:29 +0000 |
1175 | +++ tests/autopilot/webbrowser_app/tests/__init__.py 2015-10-13 11:21:13 +0000 |
1176 | @@ -82,17 +82,18 @@ |
1177 | if not os.path.exists(self.cache_location): |
1178 | os.makedirs(self.cache_location) |
1179 | |
1180 | - def setUp(self): |
1181 | + def setUp(self, launch=True): |
1182 | self.create_temporary_profile() |
1183 | self.pointing_device = uitk.get_pointing_device() |
1184 | super(BrowserTestCaseBase, self).setUp() |
1185 | - self.app = self.launch_app() |
1186 | + if (launch): |
1187 | + self.launch_app() |
1188 | |
1189 | def launch_app(self): |
1190 | if os.path.exists(self.local_location): |
1191 | - return self.launch_test_local() |
1192 | + self.app = self.launch_test_local() |
1193 | else: |
1194 | - return self.launch_test_installed() |
1195 | + self.app = self.launch_test_installed() |
1196 | self.main_window.visible.wait_for(True) |
1197 | |
1198 | def launch_test_local(self): |
1199 | @@ -238,7 +239,7 @@ |
1200 | are executed, thus making them more robust. |
1201 | """ |
1202 | |
1203 | - def setUp(self, path="/test1"): |
1204 | + def setUp(self, path="/test1", launch=True): |
1205 | self.http_server = http_server.HTTPServerInAThread() |
1206 | self.ping_server(self.http_server) |
1207 | self.addCleanup(self.http_server.cleanup) |
1208 | @@ -249,7 +250,12 @@ |
1209 | self.base_url = "http://" + self.base_domain |
1210 | self.url = self.base_url + path |
1211 | self.ARGS = self.ARGS + [self.url] |
1212 | - super(StartOpenRemotePageTestCaseBase, self).setUp() |
1213 | + super(StartOpenRemotePageTestCaseBase, self).setUp(launch) |
1214 | + if (launch): |
1215 | + self.assert_home_page_eventually_loaded() |
1216 | + |
1217 | + def launch_and_wait_for_page_loaded(self): |
1218 | + self.launch_app() |
1219 | self.assert_home_page_eventually_loaded() |
1220 | |
1221 | def assert_home_page_eventually_loaded(self): |
1222 | |
1223 | === modified file 'tests/autopilot/webbrowser_app/tests/test_new_tab_view.py' |
1224 | --- tests/autopilot/webbrowser_app/tests/test_new_tab_view.py 2015-09-24 16:01:29 +0000 |
1225 | +++ tests/autopilot/webbrowser_app/tests/test_new_tab_view.py 2015-10-13 11:21:13 +0000 |
1226 | @@ -406,9 +406,9 @@ |
1227 | Eventually(Equals(1))) |
1228 | notopsites_label = self.new_tab_view.get_notopsites_label() |
1229 | self.assertThat(notopsites_label.visible, Eventually(Equals(False))) |
1230 | + |
1231 | delegate = top_sites.get_delegates()[0] |
1232 | - delegate.trigger_leading_action("leadingAction.delete", |
1233 | - delegate.wait_until_destroyed) |
1234 | + delegate.hide_from_history(self.main_window) |
1235 | self.assertThat(lambda: len(top_sites.get_delegates()), |
1236 | Eventually(Equals(0))) |
1237 | self.assertThat(notopsites_label.visible, Eventually(Equals(True))) |
1238 | @@ -443,11 +443,10 @@ |
1239 | |
1240 | def test_remove_top_sites(self): |
1241 | view = self.new_tab_view |
1242 | - topsites = view.get_top_sites_list() |
1243 | + topsites = view.get_top_site_items() |
1244 | previous_count = len(topsites) |
1245 | - topsites[0].trigger_leading_action("leadingAction.delete", |
1246 | - topsites[0].wait_until_destroyed) |
1247 | - self.assertThat(len(view.get_top_sites_list()), |
1248 | + topsites[0].hide_from_history(self.main_window) |
1249 | + self.assertThat(len(view.get_top_site_items()), |
1250 | Equals(previous_count - 1)) |
1251 | |
1252 | def test_drag_bookmarks(self): |
1253 | |
1254 | === added file 'tests/autopilot/webbrowser_app/tests/test_site_previews.py' |
1255 | --- tests/autopilot/webbrowser_app/tests/test_site_previews.py 1970-01-01 00:00:00 +0000 |
1256 | +++ tests/autopilot/webbrowser_app/tests/test_site_previews.py 2015-10-13 11:21:13 +0000 |
1257 | @@ -0,0 +1,168 @@ |
1258 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
1259 | +# |
1260 | +# Copyright 2015 Canonical |
1261 | +# |
1262 | +# This program is free software: you can redistribute it and/or modify it |
1263 | +# under the terms of the GNU General Public License version 3, as published |
1264 | +# by the Free Software Foundation. |
1265 | +# |
1266 | +# This program is distributed in the hope that it will be useful, |
1267 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1268 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1269 | +# GNU General Public License for more details. |
1270 | +# |
1271 | +# You should have received a copy of the GNU General Public License |
1272 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1273 | + |
1274 | +import hashlib |
1275 | +import os |
1276 | +from os import path as path |
1277 | +import sqlite3 |
1278 | +import time |
1279 | + |
1280 | +from autopilot.matchers import Eventually |
1281 | +from testtools.matchers import Not, Equals, DirExists, DirContains |
1282 | + |
1283 | +from webbrowser_app.tests import StartOpenRemotePageTestCaseBase |
1284 | + |
1285 | + |
1286 | +class TestSitePreviewsBase(StartOpenRemotePageTestCaseBase): |
1287 | + |
1288 | + def setUp(self): |
1289 | + super(TestSitePreviewsBase, self).setUp(launch=False) |
1290 | + self.captures_dir = path.join(self.cache_location, "captures") |
1291 | + |
1292 | + def file_in_dir(self, file, dir): |
1293 | + return path.exists(path.join(dir, file)) |
1294 | + |
1295 | + def capture_file(self, url): |
1296 | + return hashlib.md5(url.encode()).hexdigest() + ".jpg" |
1297 | + |
1298 | + |
1299 | +class TestSitePreviewsNoLaunch(TestSitePreviewsBase): |
1300 | + |
1301 | + def populate_captures_dir(self, capture_names): |
1302 | + # captures dir should not exist on fresh run |
1303 | + self.assertThat(self.captures_dir, Not(DirExists())) |
1304 | + |
1305 | + # create some random files and ensure they get cleaned up |
1306 | + os.mkdir(self.captures_dir) |
1307 | + for capture_name in capture_names: |
1308 | + open(path.join(self.captures_dir, capture_name), 'w').close() |
1309 | + self.assertThat(self.captures_dir, |
1310 | + DirContains(capture_names)) |
1311 | + |
1312 | + def populate_history(self): |
1313 | + self.countries = [ |
1314 | + "Japan", "Russia", "France", "Italy", "Argentina", |
1315 | + "Canada", "Mexico", "Peru", "Congo", "Brazil", |
1316 | + "China", "Mali", "Morocco" |
1317 | + ] |
1318 | + db_path = os.path.join(self.data_location, "history.sqlite") |
1319 | + connection = sqlite3.connect(db_path) |
1320 | + connection.execute("""CREATE TABLE IF NOT EXISTS history |
1321 | + (url VARCHAR, domain VARCHAR, title VARCHAR, |
1322 | + icon VARCHAR, visits INTEGER, |
1323 | + lastVisit DATETIME);""") |
1324 | + visits = 50 |
1325 | + for country in self.countries: |
1326 | + timestamp = int(time.time()) |
1327 | + query = "INSERT INTO history \ |
1328 | + VALUES ('{}', '{}', '{}', '', {}, {});" |
1329 | + query = query.format("http://en.wikipedia.org/wiki/" + country, |
1330 | + "wikipedia.org", country, visits, timestamp) |
1331 | + connection.execute(query) |
1332 | + visits -= 1 |
1333 | + connection.commit() |
1334 | + connection.close() |
1335 | + |
1336 | + def test_cleanup_previews_on_startup(self): |
1337 | + self.populate_history() |
1338 | + |
1339 | + # populate the captures dir with correct thumbnail names for all |
1340 | + # the sites in history... |
1341 | + history = ["http://en.wikipedia.org/wiki/" + c for c in self.countries] |
1342 | + history = [self.capture_file(url) for url in history] |
1343 | + |
1344 | + # ...plus some other files to verify possible corner cases |
1345 | + other_url = self.capture_file("http://google.com/") |
1346 | + not_hash = "not_a_preview.jpg" |
1347 | + not_image = "not_an_image.xxx" |
1348 | + self.populate_captures_dir(history + [other_url, not_hash, not_image]) |
1349 | + |
1350 | + self.launch_app() |
1351 | + time.sleep(0.5) # wait for file system to settle |
1352 | + |
1353 | + # verify that non-image files and top 10 sites are left alone, |
1354 | + # everything else is cleaned up |
1355 | + topsites = history[0:10] |
1356 | + self.assertThat(self.captures_dir, DirContains(topsites + [not_image])) |
1357 | + |
1358 | + |
1359 | +class TestSitePreviews(TestSitePreviewsBase): |
1360 | + |
1361 | + def setUp(self): |
1362 | + super(TestSitePreviews, self).setUp() |
1363 | + self.launch_and_wait_for_page_loaded() |
1364 | + |
1365 | + def close_tab(self, index): |
1366 | + if self.main_window.wide: |
1367 | + self.main_window.chrome.get_tabs_bar().close_tab(index) |
1368 | + else: |
1369 | + tabs_view = self.open_tabs_view() |
1370 | + tabs_view.get_previews()[index].close() |
1371 | + toolbar = self.main_window.get_recent_view_toolbar() |
1372 | + toolbar.click_button("doneButton") |
1373 | + |
1374 | + def get_captures(self): |
1375 | + if not path.exists(self.captures_dir): |
1376 | + return [] |
1377 | + |
1378 | + all = os.listdir(self.captures_dir) |
1379 | + cap = [f for f in all if path.isfile(path.join(self.captures_dir, f))] |
1380 | + return cap |
1381 | + |
1382 | + def remove_top_site(self, new_tab_view, url): |
1383 | + top_sites = new_tab_view.get_top_site_items() |
1384 | + top_sites = [d for d in top_sites if d.url == url] |
1385 | + self.assertThat(len(top_sites), Equals(1)) |
1386 | + delegate = top_sites[0] |
1387 | + delegate.hide_from_history(self.main_window) |
1388 | + |
1389 | + def test_save_on_switch_tab_and_not_delete_if_topsite(self): |
1390 | + previous = self.main_window.get_current_webview().url |
1391 | + |
1392 | + # switching away from tab should save a capture |
1393 | + self.open_new_tab(open_tabs_view=True) |
1394 | + self.assertThat(self.captures_dir, |
1395 | + DirContains([self.capture_file(previous)])) |
1396 | + |
1397 | + # closing the captured tab should not delete the capture since it is |
1398 | + # now part of the top sites (being the only one we opened so far) |
1399 | + self.close_tab(0) |
1400 | + time.sleep(0.5) # wait for file system to settle |
1401 | + self.assertThat(self.captures_dir, |
1402 | + DirContains([self.capture_file(previous)])) |
1403 | + |
1404 | + def test_save_on_switch_tab_and_delete_if_not_topsite(self): |
1405 | + previous = self.main_window.get_current_webview().url |
1406 | + new_tab_view = self.open_new_tab(open_tabs_view=True) |
1407 | + self.remove_top_site(new_tab_view, previous) |
1408 | + self.close_tab(0) |
1409 | + self.assertThat(lambda: self.file_in_dir(self.capture_file(previous), |
1410 | + self.captures_dir), |
1411 | + Eventually(Equals(False))) |
1412 | + |
1413 | + def test_delete_when_tab_closed_and_removed_from_topsites(self): |
1414 | + previous = self.main_window.get_current_webview().url |
1415 | + capture = self.capture_file(previous) |
1416 | + new_tab_view = self.open_new_tab(open_tabs_view=True) |
1417 | + self.close_tab(0) |
1418 | + time.sleep(0.5) # wait for file system to settle |
1419 | + self.assertThat(self.captures_dir, DirContains([capture])) |
1420 | + |
1421 | + if not self.main_window.wide: |
1422 | + new_tab_view = self.open_new_tab(open_tabs_view=True) |
1423 | + self.remove_top_site(new_tab_view, previous) |
1424 | + self.assertThat(lambda: self.file_in_dir(capture, self.captures_dir), |
1425 | + Eventually(Equals(False))) |
1426 | |
1427 | === modified file 'tests/unittests/qml/tst_BrowserTab.qml' |
1428 | --- tests/unittests/qml/tst_BrowserTab.qml 2015-09-23 15:53:01 +0000 |
1429 | +++ tests/unittests/qml/tst_BrowserTab.qml 2015-10-13 11:21:13 +0000 |
1430 | @@ -19,6 +19,7 @@ |
1431 | import QtQuick 2.4 |
1432 | import QtTest 1.0 |
1433 | import "../../../src/app/webbrowser" |
1434 | +import webbrowserapp.private 0.1 |
1435 | |
1436 | Item { |
1437 | id: root |
1438 | @@ -30,13 +31,16 @@ |
1439 | id: tabComponent |
1440 | |
1441 | BrowserTab { |
1442 | + id: tab |
1443 | + anchors.fill: parent |
1444 | webviewComponent: Item { |
1445 | + anchors.fill: parent |
1446 | property url url |
1447 | property string title |
1448 | property url icon |
1449 | property var request |
1450 | property string currentState |
1451 | - |
1452 | + property bool incognito: tab.incognito |
1453 | property int reloaded: 0 |
1454 | function reload() { reloaded++ } |
1455 | } |
1456 | @@ -44,10 +48,20 @@ |
1457 | } |
1458 | } |
1459 | |
1460 | + SignalSpy { |
1461 | + id: previewSavedSpy |
1462 | + target: PreviewManager |
1463 | + signalName: "previewSaved" |
1464 | + } |
1465 | + |
1466 | TestCase { |
1467 | name: "BrowserTab" |
1468 | when: windowShown |
1469 | |
1470 | + function init() { |
1471 | + previewSavedSpy.clear() |
1472 | + } |
1473 | + |
1474 | function test_unique_ids() { |
1475 | var tab = tabComponent.createObject(root) |
1476 | var tab2 = tabComponent.createObject(root) |
1477 | @@ -100,5 +114,54 @@ |
1478 | compare(tab.webview.request, "foobar") |
1479 | tab.destroy() |
1480 | } |
1481 | + |
1482 | + function test_save_preview() { |
1483 | + var tab = tabComponent.createObject(root) |
1484 | + tab.initialUrl = "http://example.org" |
1485 | + tab.load() |
1486 | + tryCompare(tab, 'webviewPresent', true) |
1487 | + |
1488 | + tab.current = true |
1489 | + tab.current = false |
1490 | + tryCompare(previewSavedSpy, "count", 1) |
1491 | + verify(!tab.visible) |
1492 | + compare(previewSavedSpy.signalArguments[0][0], tab.initialUrl) |
1493 | + compare(previewSavedSpy.signalArguments[0][1], Qt.resolvedUrl(PreviewManager.previewPathFromUrl(tab.initialUrl))) |
1494 | + compare(tab.preview, Qt.resolvedUrl(PreviewManager.previewPathFromUrl(tab.initialUrl))) |
1495 | + tab.destroy() |
1496 | + } |
1497 | + |
1498 | + function test_no_save_preview_when_incognito() { |
1499 | + var tab = tabComponent.createObject(root) |
1500 | + tab.incognito = true |
1501 | + tab.initialUrl = "http://example.org" |
1502 | + tab.load() |
1503 | + tryCompare(tab, 'webviewPresent', true) |
1504 | + |
1505 | + tab.current = true |
1506 | + tab.current = false |
1507 | + // this does not fully guarantee the event won't be emitted later, |
1508 | + // but it is a reasonable delay and certainly better than nothing |
1509 | + wait(250) |
1510 | + compare(previewSavedSpy.count, 0) |
1511 | + compare(tab.preview, "") |
1512 | + tab.destroy() |
1513 | + } |
1514 | + |
1515 | + function test_delete_preview_on_close() { |
1516 | + var url = "http://example.org" |
1517 | + var path = Qt.resolvedUrl(PreviewManager.previewPathFromUrl(url)) |
1518 | + var tab = tabComponent.createObject(root) |
1519 | + tab.initialUrl = url |
1520 | + tab.load() |
1521 | + tryCompare(tab, 'webviewPresent', true) |
1522 | + |
1523 | + tab.current = true |
1524 | + tab.current = false |
1525 | + tryCompare(previewSavedSpy, "count", 1) |
1526 | + verify(FileOperations.exists(path)) |
1527 | + tab.close() |
1528 | + verify(!FileOperations.exists(path)) |
1529 | + } |
1530 | } |
1531 | } |
1532 | |
1533 | === modified file 'tests/unittests/qml/tst_HistoryViewWide.qml' |
1534 | --- tests/unittests/qml/tst_HistoryViewWide.qml 2015-09-18 15:20:09 +0000 |
1535 | +++ tests/unittests/qml/tst_HistoryViewWide.qml 2015-10-13 11:21:13 +0000 |
1536 | @@ -21,6 +21,7 @@ |
1537 | import Ubuntu.Components 1.3 |
1538 | import Ubuntu.Components.ListItems 1.3 as ListItems |
1539 | import Ubuntu.Test 1.0 |
1540 | +import webbrowserapp.private 0.1 |
1541 | import webbrowsertest.private 0.1 |
1542 | import "../../../src/app/webbrowser" |
1543 | |
1544 | @@ -45,9 +46,6 @@ |
1545 | focus: true |
1546 | sourceComponent: HistoryViewWide { |
1547 | focus: true |
1548 | - historyModel: HistoryModelMock { |
1549 | - databasePath: ":memory:" |
1550 | - } |
1551 | } |
1552 | } |
1553 | |
1554 | @@ -90,19 +88,25 @@ |
1555 | mouseRelease(item, center.x + 100, center.y, Qt.LeftButton, Qt.NoModifier, 2000) |
1556 | } |
1557 | |
1558 | + function initTestCase() { |
1559 | + HistoryModel.databasePath = ":memory:" |
1560 | + } |
1561 | + |
1562 | function init() { |
1563 | historyViewWideLoader.active = true |
1564 | waitForRendering(historyViewWideLoader.item) |
1565 | |
1566 | for (var i = 0; i < 3; ++i) { |
1567 | - historyViewWide.historyModel.add("http://example.org/" + i, "Example Domain " + i, "") |
1568 | + HistoryModel.add("http://example.org/" + i, "Example Domain " + i, "") |
1569 | } |
1570 | + historyViewWide.loadModel() |
1571 | var urlsList = findChild(historyViewWide, "urlsListView") |
1572 | waitForRendering(urlsList) |
1573 | tryCompare(urlsList, "count", 3) |
1574 | } |
1575 | |
1576 | function cleanup() { |
1577 | + HistoryModel.clearAll() |
1578 | historyViewWideLoader.active = false |
1579 | ctrlFCaptured = 0 |
1580 | } |
1581 | @@ -205,7 +209,9 @@ |
1582 | function test_keyboard_navigation_between_lists() { |
1583 | var lastVisitDateList = findChild(historyViewWide, "lastVisitDateListView") |
1584 | var urlsList = findChild(historyViewWide, "urlsListView") |
1585 | + verify(!lastVisitDateList.activeFocus) |
1586 | verify(urlsList.activeFocus) |
1587 | + |
1588 | keyClick(Qt.Key_Left) |
1589 | verify(lastVisitDateList.activeFocus) |
1590 | verify(!urlsList.activeFocus) |
1591 | @@ -309,7 +315,7 @@ |
1592 | keyClick(Qt.Key_Enter) |
1593 | compare(historyEntryClickedSpy.count, 1) |
1594 | var args = historyEntryClickedSpy.signalArguments[0] |
1595 | - var entry = urlsList.model.get(2) |
1596 | + var entry = urlsList.model.get(0) |
1597 | compare(String(args[0]), String(entry.url)) |
1598 | |
1599 | // now try the same during a search |
1600 | @@ -372,7 +378,7 @@ |
1601 | var today = new Date() |
1602 | today = new Date(today.getFullYear(), today.getMonth(), today.getDate()) |
1603 | var youngest = new Date(1912, 6, 23) |
1604 | - var model = historyViewWide.historyModel |
1605 | + var model = HistoryModel |
1606 | model.addByDate("https://en.wikipedia.org/wiki/Alan_Turing", "Alan Turing", youngest) |
1607 | model.addByDate("https://en.wikipedia.org/wiki/Alonzo_Church", "Alonzo Church", new Date(1903, 6, 14)) |
1608 | |
1609 | |
1610 | === modified file 'tests/unittests/qml/tst_NewTabViewWide.qml' |
1611 | --- tests/unittests/qml/tst_NewTabViewWide.qml 2015-08-11 14:28:29 +0000 |
1612 | +++ tests/unittests/qml/tst_NewTabViewWide.qml 2015-10-13 11:21:13 +0000 |
1613 | @@ -29,13 +29,6 @@ |
1614 | height: 600 |
1615 | |
1616 | Component { |
1617 | - id: historyModel |
1618 | - HistoryModel { |
1619 | - databasePath: ":memory:" |
1620 | - } |
1621 | - } |
1622 | - |
1623 | - Component { |
1624 | id: bookmarksModel |
1625 | BookmarksModel { |
1626 | databasePath: ":memory:" |
1627 | @@ -44,7 +37,6 @@ |
1628 | |
1629 | property NewTabViewWide view |
1630 | property var bookmarks |
1631 | - property var history |
1632 | property string homepage: "http://example.com/homepage" |
1633 | |
1634 | Component { |
1635 | @@ -56,7 +48,6 @@ |
1636 | property int newTabDefaultSection: 0 |
1637 | } |
1638 | bookmarksModel: bookmarks |
1639 | - historyModel: history |
1640 | } |
1641 | } |
1642 | |
1643 | @@ -84,9 +75,12 @@ |
1644 | name: "NewTabViewWide" |
1645 | when: windowShown |
1646 | |
1647 | + function initTestCase() { |
1648 | + HistoryModel.databasePath = ":memory:" |
1649 | + } |
1650 | + |
1651 | function init() { |
1652 | bookmarks = bookmarksModel.createObject() |
1653 | - history = historyModel.createObject() |
1654 | view = viewComponent.createObject(root) |
1655 | populate() |
1656 | |
1657 | @@ -103,9 +97,9 @@ |
1658 | } |
1659 | |
1660 | function populate() { |
1661 | - history.add("http://example.com", "Example Com", "") |
1662 | - history.add("http://example.org", "Example Org", "") |
1663 | - history.add("http://example.net", "Example Net", "") |
1664 | + HistoryModel.add("http://example.com", "Example Com", "") |
1665 | + HistoryModel.add("http://example.org", "Example Org", "") |
1666 | + HistoryModel.add("http://example.net", "Example Net", "") |
1667 | bookmarks.add("http://example.com", "Example Com", "", "") |
1668 | bookmarks.add("http://example.org/bar", "Example Org Bar", "", "Folder B") |
1669 | bookmarks.add("http://example.org/foo", "Example Org Foo", "", "Folder B") |
1670 | @@ -114,8 +108,7 @@ |
1671 | } |
1672 | |
1673 | function cleanup() { |
1674 | - history.destroy() |
1675 | - history = null |
1676 | + HistoryModel.clearAll() |
1677 | bookmarks.destroy() |
1678 | bookmarks = null |
1679 | |
1680 | @@ -155,7 +148,7 @@ |
1681 | function test_topsites_list() { |
1682 | // add 8 more top sites so that we are beyond the limit of 10 |
1683 | for (var i = 0; i < 8; i++) { |
1684 | - history.add("http://example.com/" + i, "Example Com " + i, "") |
1685 | + HistoryModel.add("http://example.com/" + i, "Example Com " + i, "") |
1686 | } |
1687 | |
1688 | var items = getListItems("topSitesList", "topSiteItem") |
1689 | @@ -208,22 +201,24 @@ |
1690 | |
1691 | function test_navigate_topsites_by_keyboard() { |
1692 | var items = getListItems("topSitesList", "topSiteItem") |
1693 | - findChild(view, "topSitesList").currentIndex = 0 |
1694 | - verify(items[0].highlighted) |
1695 | - keyClick(Qt.Key_Down) |
1696 | - verify(!items[0].highlighted) |
1697 | - verify(items[1].highlighted) |
1698 | - keyClick(Qt.Key_Down) |
1699 | - verify(items[2].highlighted) |
1700 | - keyClick(Qt.Key_Down) // ensure no scrolling past bottom boundary |
1701 | - verify(items[2].highlighted) |
1702 | - keyClick(Qt.Key_Up) |
1703 | - verify(items[1].highlighted) |
1704 | - keyClick(Qt.Key_Up) |
1705 | - verify(items[0].highlighted) |
1706 | - keyClick(Qt.Key_Up) |
1707 | - verify(items[0].highlighted) |
1708 | + var list = findChild(view, "topSitesList") |
1709 | + list.currentIndex = 0 |
1710 | + keyClick(Qt.Key_Right) |
1711 | + compare(list.currentIndex, 1) |
1712 | + keyClick(Qt.Key_Right) |
1713 | + compare(list.currentIndex, 2) |
1714 | + keyClick(Qt.Key_Right) // ensure list does not wrap around |
1715 | + compare(list.currentIndex, 2) |
1716 | + keyClick(Qt.Key_Left) |
1717 | + compare(list.currentIndex, 1) |
1718 | + keyClick(Qt.Key_Left) |
1719 | + compare(list.currentIndex, 0) |
1720 | + keyClick(Qt.Key_Up) |
1721 | + compare(list.currentIndex, 0) |
1722 | compare(releasingKeyboardFocusSpy.count, 1) |
1723 | + keyClick(Qt.Key_Left) |
1724 | + compare(list.currentIndex, 0) |
1725 | + compare(releasingKeyboardFocusSpy.count, 2) |
1726 | } |
1727 | |
1728 | function test_activate_topsites_by_keyboard() { |
1729 | @@ -231,7 +226,7 @@ |
1730 | keyClick(Qt.Key_Return) |
1731 | compare(historyEntryClickedSpy.count, 1) |
1732 | compare(historyEntryClickedSpy.signalArguments[0][0], "http://example.com") |
1733 | - keyClick(Qt.Key_Down) |
1734 | + keyClick(Qt.Key_Right) |
1735 | keyClick(Qt.Key_Return) |
1736 | compare(historyEntryClickedSpy.count, 2) |
1737 | compare(historyEntryClickedSpy.signalArguments[1][0], "http://example.org") |
1738 | |
1739 | === added file 'tests/unittests/qml/tst_PreviewManager.qml' |
1740 | --- tests/unittests/qml/tst_PreviewManager.qml 1970-01-01 00:00:00 +0000 |
1741 | +++ tests/unittests/qml/tst_PreviewManager.qml 2015-10-13 11:21:13 +0000 |
1742 | @@ -0,0 +1,117 @@ |
1743 | +/* |
1744 | + * Copyright 2015 Canonical Ltd. |
1745 | + * |
1746 | + * This file is part of webbrowser-app. |
1747 | + * |
1748 | + * webbrowser-app is free software; you can redistribute it and/or modify |
1749 | + * it under the terms of the GNU General Public License as published by |
1750 | + * the Free Software Foundation; version 3. |
1751 | + * |
1752 | + * webbrowser-app is distributed in the hope that it will be useful, |
1753 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
1754 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1755 | + * GNU General Public License for more details. |
1756 | + * |
1757 | + * You should have received a copy of the GNU General Public License |
1758 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
1759 | + */ |
1760 | + |
1761 | +import QtQuick 2.4 |
1762 | +import QtTest 1.0 |
1763 | +import Ubuntu.Test 1.0 |
1764 | +import "../../../src/app/webbrowser" |
1765 | +import webbrowserapp.private 0.1 |
1766 | +import webbrowsertest.private 0.1 |
1767 | + |
1768 | +Item { |
1769 | + id: root |
1770 | + |
1771 | + width: 800 |
1772 | + height: 600 |
1773 | + |
1774 | + SignalSpy { |
1775 | + id: previewSavedSpy |
1776 | + target: PreviewManager |
1777 | + signalName: "previewSaved" |
1778 | + } |
1779 | + |
1780 | + QtObject { |
1781 | + id: grabResultMock |
1782 | + function saveToFile(path) { |
1783 | + TestContext.createFile(path); |
1784 | + return true |
1785 | + } |
1786 | + } |
1787 | + |
1788 | + QtObject { |
1789 | + id: grabResultFailMock |
1790 | + function saveToFile(path) { return false } |
1791 | + } |
1792 | + |
1793 | + UbuntuTestCase { |
1794 | + name: "PreviewManager" |
1795 | + when: windowShown |
1796 | + |
1797 | + property string baseUrl: "http://example.com/" |
1798 | + |
1799 | + function initTestCase() { |
1800 | + HistoryModel.databasePath = ":memory:" |
1801 | + } |
1802 | + |
1803 | + function init() { |
1804 | + previewSavedSpy.clear() |
1805 | + verify(TestContext.removeDirectory(PreviewManager.capturesDir)) |
1806 | + } |
1807 | + |
1808 | + function populate(count, createPreviewFiles) { |
1809 | + for (var i = 0; i < 11; i++) { |
1810 | + var url = baseUrl + i |
1811 | + HistoryModel.add(url, "Example Com" + i, "") |
1812 | + if (createPreviewFiles) { |
1813 | + var path = PreviewManager.previewPathFromUrl(url) |
1814 | + TestContext.createFile(path) |
1815 | + } |
1816 | + } |
1817 | + } |
1818 | + |
1819 | + function cleanup() { |
1820 | + HistoryModel.clearAll() |
1821 | + } |
1822 | + |
1823 | + function test_topsites_not_deleted() { |
1824 | + populate(11, true) |
1825 | + for (var i = 0; i < 11; i++) { |
1826 | + var url = baseUrl + i |
1827 | + PreviewManager.checkDelete(url) |
1828 | + var path = Qt.resolvedUrl(PreviewManager.previewPathFromUrl(url)) |
1829 | + |
1830 | + // verify that only the item that is outside of the top 10 list |
1831 | + // gets deleted |
1832 | + if (i < 10) verify(FileOperations.exists(path)) |
1833 | + else verify(!FileOperations.exists(path)) |
1834 | + } |
1835 | + } |
1836 | + |
1837 | + function test_save_preview() { |
1838 | + var file = Qt.resolvedUrl(PreviewManager.previewPathFromUrl(baseUrl)) |
1839 | + |
1840 | + PreviewManager.saveToDisk(grabResultMock, baseUrl) |
1841 | + verify(FileOperations.exists(file)) |
1842 | + compare(previewSavedSpy.count, 1) |
1843 | + compare(previewSavedSpy.signalArguments[0][0], baseUrl) |
1844 | + compare(previewSavedSpy.signalArguments[0][1], file) |
1845 | + } |
1846 | + |
1847 | + function test_save_preview_fail() { |
1848 | + var path = PreviewManager.previewPathFromUrl(baseUrl) |
1849 | + var file = Qt.resolvedUrl(path) |
1850 | + |
1851 | + ignoreWarning("Failed to save preview to disk for %1 (path is %2)".arg(baseUrl).arg(path)) |
1852 | + PreviewManager.saveToDisk(grabResultFailMock, baseUrl) |
1853 | + verify(!FileOperations.exists(file)) |
1854 | + compare(previewSavedSpy.count, 1) |
1855 | + compare(previewSavedSpy.signalArguments[0][0], baseUrl) |
1856 | + compare(previewSavedSpy.signalArguments[0][1], "") |
1857 | + } |
1858 | + } |
1859 | +} |
1860 | |
1861 | === modified file 'tests/unittests/qml/tst_QmlTests.cpp' |
1862 | --- tests/unittests/qml/tst_QmlTests.cpp 2015-09-16 17:19:17 +0000 |
1863 | +++ tests/unittests/qml/tst_QmlTests.cpp 2015-10-13 11:21:13 +0000 |
1864 | @@ -105,6 +105,22 @@ |
1865 | return QFile(QDir(path).absoluteFilePath(QString("%1.xml").arg(filename))).remove(); |
1866 | } |
1867 | |
1868 | + Q_INVOKABLE bool createFile(const QString& filePath) { |
1869 | + // create all the directories necessary for the file to be created |
1870 | + QFileInfo fileInfo(filePath); |
1871 | + if (!QFileInfo::exists(fileInfo.path())) { |
1872 | + QDir::root().mkpath(fileInfo.path()); |
1873 | + } |
1874 | + |
1875 | + QFile file(fileInfo.absoluteFilePath()); |
1876 | + return file.open(QIODevice::WriteOnly | QIODevice::Text); |
1877 | + } |
1878 | + |
1879 | + Q_INVOKABLE bool removeDirectory(const QString& path) { |
1880 | + QDir dir(path); |
1881 | + return dir.removeRecursively(); |
1882 | + } |
1883 | + |
1884 | private: |
1885 | QTemporaryDir m_testDir1; |
1886 | QTemporaryDir m_testDir2; |
1887 | @@ -152,6 +168,13 @@ |
1888 | return new TestContext(); |
1889 | } |
1890 | |
1891 | +static QObject* HistoryModel_singleton_factory(QQmlEngine* engine, QJSEngine* scriptEngine) |
1892 | +{ |
1893 | + Q_UNUSED(engine); |
1894 | + Q_UNUSED(scriptEngine); |
1895 | + return new HistoryModelMock(); |
1896 | +} |
1897 | + |
1898 | int main(int argc, char** argv) |
1899 | { |
1900 | const char* commonUri = "webbrowsercommon.private"; |
1901 | @@ -162,7 +185,7 @@ |
1902 | qmlRegisterType<TabsModel>(browserUri, 0, 1, "TabsModel"); |
1903 | qmlRegisterType<BookmarksModel>(browserUri, 0, 1, "BookmarksModel"); |
1904 | qmlRegisterType<BookmarksFolderListModel>(browserUri, 0, 1, "BookmarksFolderListModel"); |
1905 | - qmlRegisterType<HistoryModel>(browserUri, 0, 1, "HistoryModel"); |
1906 | + qmlRegisterSingletonType<HistoryModel>(browserUri, 0, 1, "HistoryModel", HistoryModel_singleton_factory); |
1907 | qmlRegisterType<HistoryTimeframeModel>(browserUri, 0, 1, "HistoryTimeframeModel"); |
1908 | qmlRegisterType<HistoryLastVisitDateListModel>(browserUri, 0, 1, "HistoryLastVisitDateListModel"); |
1909 | qmlRegisterType<HistoryLastVisitDateModel>(browserUri, 0, 1, "HistoryLastVisitDateModel"); |
1910 | @@ -173,7 +196,6 @@ |
1911 | |
1912 | const char* testUri = "webbrowsertest.private"; |
1913 | qmlRegisterSingletonType<TestContext>(testUri, 0, 1, "TestContext", TestContext_singleton_factory); |
1914 | - qmlRegisterType<HistoryModelMock>(testUri, 0, 1, "HistoryModelMock"); |
1915 | |
1916 | return quick_test_main(argc, argv, "QmlTests", nullptr); |
1917 | } |
FAILED: Continuous integration, rev:1159 jenkins. qa.ubuntu. com/job/ webbrowser- app-ci/ 2150/ jenkins. qa.ubuntu. com/job/ generic- deb-autopilot- vivid-touch/ 3929 jenkins. qa.ubuntu. com/job/ webbrowser- app-vivid- amd64-ci/ 904 jenkins. qa.ubuntu. com/job/ webbrowser- app-vivid- armhf-ci/ 904 jenkins. qa.ubuntu. com/job/ webbrowser- app-vivid- armhf-ci/ 904/artifact/ work/output/ *zip*/output. zip jenkins. qa.ubuntu. com/job/ webbrowser- app-vivid- i386-ci/ 904 jenkins. qa.ubuntu. com/job/ generic- deb-autopilot- runner- vivid-mako/ 3215 jenkins. qa.ubuntu. com/job/ generic- mediumtests- builder- vivid-armhf/ 3926 jenkins. qa.ubuntu. com/job/ generic- mediumtests- builder- vivid-armhf/ 3926/artifact/ work/output/ *zip*/output. zip s-jenkins. ubuntu- ci:8080/ job/touch- flash-device/ 22955
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild: s-jenkins. ubuntu- ci:8080/ job/webbrowser- app-ci/ 2150/rebuild
http://