Merge lp:~dobey/ubuntuone-control-panel/update-4-2 into lp:ubuntuone-control-panel/stable-4-2
- update-4-2
- Merge into stable-4-2
Proposed by
dobey
Status: | Merged |
---|---|
Approved by: | dobey |
Approved revision: | 383 |
Merged at revision: | 383 |
Proposed branch: | lp:~dobey/ubuntuone-control-panel/update-4-2 |
Merge into: | lp:ubuntuone-control-panel/stable-4-2 |
Diff against target: |
429 lines (+133/-141) 6 files modified
ubuntuone/controlpanel/gui/qt/controlpanel.py (+5/-0) ubuntuone/controlpanel/gui/qt/share_links.py (+6/-1) ubuntuone/controlpanel/gui/qt/share_links_search.py (+9/-16) ubuntuone/controlpanel/gui/qt/tests/test_controlpanel.py (+42/-2) ubuntuone/controlpanel/gui/qt/tests/test_share_links.py (+8/-0) ubuntuone/controlpanel/gui/qt/tests/test_share_links_search.py (+63/-122) |
To merge this branch: | bzr merge lp:~dobey/ubuntuone-control-panel/update-4-2 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Roberto Alsina (community) | Approve | ||
Review via email: mp+155611@code.launchpad.net |
Description of the change
To post a comment you must log in.
Revision history for this message
Roberto Alsina (ralsina) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'ubuntuone/controlpanel/gui/qt/controlpanel.py' |
2 | --- ubuntuone/controlpanel/gui/qt/controlpanel.py 2012-10-18 21:45:05 +0000 |
3 | +++ ubuntuone/controlpanel/gui/qt/controlpanel.py 2013-03-26 20:55:24 +0000 |
4 | @@ -79,6 +79,11 @@ |
5 | self.ui.tab_widget.setTabText( |
6 | self.ui.tab_widget.indexOf(self.ui.share_links_tab), |
7 | MAIN_SHARE_LINKS_TAB) |
8 | + |
9 | + # Workaround for bug LP: 1152388 |
10 | + handler = self.ui.share_links_tab.handle_current_tab_changed |
11 | + self.ui.tab_widget.currentChanged.connect(handler) |
12 | + |
13 | self.ui.tab_widget.setTabText( |
14 | self.ui.tab_widget.indexOf(self.ui.devices_tab), MAIN_DEVICES_TAB) |
15 | self.ui.tab_widget.setTabText( |
16 | |
17 | === modified file 'ubuntuone/controlpanel/gui/qt/share_links.py' |
18 | --- ubuntuone/controlpanel/gui/qt/share_links.py 2012-11-02 17:15:15 +0000 |
19 | +++ ubuntuone/controlpanel/gui/qt/share_links.py 2013-03-26 20:55:24 +0000 |
20 | @@ -1,6 +1,6 @@ |
21 | # -*- coding: utf-8 *-* |
22 | |
23 | -# Copyright 2012 Canonical Ltd. |
24 | +# Copyright 2012-2013 Canonical Ltd. |
25 | # |
26 | # This program is free software: you can redistribute it and/or modify it |
27 | # under the terms of the GNU General Public License version 3, as published |
28 | @@ -107,6 +107,11 @@ |
29 | self.get_public_files() |
30 | self._enhanced_line.btn_operation.hide() |
31 | |
32 | + def handle_current_tab_changed(self, index): |
33 | + """Workaround for bug LP: 1152388""" |
34 | + self.ui.line_search.clearFocus() |
35 | + self.ui.line_search.popup.hide() |
36 | + |
37 | @inlineCallbacks |
38 | def share_file(self, file_path): |
39 | """Clean the previous file share details and publish file_path.""" |
40 | |
41 | === modified file 'ubuntuone/controlpanel/gui/qt/share_links_search.py' |
42 | --- ubuntuone/controlpanel/gui/qt/share_links_search.py 2013-01-24 21:35:59 +0000 |
43 | +++ ubuntuone/controlpanel/gui/qt/share_links_search.py 2013-03-26 20:55:24 +0000 |
44 | @@ -36,6 +36,7 @@ |
45 | HOME_DIR = '' |
46 | AVOID_SECOND_ITEM = 2 |
47 | NORMAL_INCREMENT = 1 |
48 | +SEARCH_TYPING_DELAY = 100 # msec |
49 | |
50 | |
51 | def get_system_icon_for_filename(file_path): |
52 | @@ -63,7 +64,6 @@ |
53 | self.current_results = [] |
54 | self.page_index = 0 |
55 | self.page_size = 20 |
56 | - self.pending_call = None |
57 | self._post_key_event = { |
58 | QtCore.Qt.Key_Escape: lambda *args: self.popup.hide(), |
59 | QtCore.Qt.Key_Down: self._key_down_pressed, |
60 | @@ -76,6 +76,10 @@ |
61 | self.popup.list_widget.verticalScrollBar().valueChanged.connect( |
62 | self._scroll_fetch_more) |
63 | self.textChanged.connect(self.handle_text_changed) |
64 | + self._do_search_timer = QtCore.QTimer() |
65 | + self._do_search_timer.timeout.connect(self._do_search) |
66 | + self._do_search_timer.setInterval(SEARCH_TYPING_DELAY) |
67 | + self._do_search_timer.setSingleShot(True) |
68 | |
69 | self._get_home_path() |
70 | |
71 | @@ -94,30 +98,19 @@ |
72 | def handle_text_changed(self, text): |
73 | """Use delayed IPC to search for filenames after user stops typing.""" |
74 | |
75 | - # Import here to avoid getting the wrong reactor due to import |
76 | - # order. Save in class var to ease patching for tests: |
77 | - if not self.qtreactor: |
78 | - self.qtreactor = __import__('twisted').internet.reactor |
79 | text = unicode(text) |
80 | self.page_index = 0 |
81 | |
82 | if text == '': |
83 | self.popup.hide() |
84 | - if self.pending_call and self.pending_call.active(): |
85 | - self.pending_call.cancel() |
86 | - return |
87 | - |
88 | - if self.pending_call and self.pending_call.active(): |
89 | - self.pending_call.reset(0.1) |
90 | - return |
91 | - |
92 | - self.pending_call = self.qtreactor.callLater(0.1, self._do_search) |
93 | + self._do_search_timer.stop() |
94 | + return |
95 | + |
96 | + self._do_search_timer.start(SEARCH_TYPING_DELAY) |
97 | |
98 | @inlineCallbacks |
99 | def _do_search(self): |
100 | |
101 | - self.pending_call = None |
102 | - |
103 | search_text = unicode(self.text()) |
104 | results = yield self.backend.search_files(search_text) |
105 | |
106 | |
107 | === modified file 'ubuntuone/controlpanel/gui/qt/tests/test_controlpanel.py' |
108 | --- ubuntuone/controlpanel/gui/qt/tests/test_controlpanel.py 2012-10-17 07:21:28 +0000 |
109 | +++ ubuntuone/controlpanel/gui/qt/tests/test_controlpanel.py 2013-03-26 20:55:24 +0000 |
110 | @@ -1,6 +1,6 @@ |
111 | # -*- coding: utf-8 -*- |
112 | # |
113 | -# Copyright 2011-2012 Canonical Ltd. |
114 | +# Copyright 2011-2013 Canonical Ltd. |
115 | # |
116 | # This program is free software: you can redistribute it and/or modify it |
117 | # under the terms of the GNU General Public License version 3, as published |
118 | @@ -19,10 +19,17 @@ |
119 | from __future__ import division |
120 | |
121 | from twisted.internet import defer |
122 | +from PyQt4 import QtCore |
123 | + |
124 | +from ubuntuone.controlpanel import backend, cache |
125 | +from ubuntuone.controlpanel.tests import TestCase |
126 | +from mock import call, Mock |
127 | + |
128 | +from ubuntuone.controlpanel.gui.qt import share_links |
129 | |
130 | from ubuntuone.controlpanel.gui.qt import controlpanel as gui |
131 | from ubuntuone.controlpanel.gui.qt.tests import ( |
132 | - SAMPLE_ACCOUNT_INFO, SAMPLE_NAME, |
133 | + FakedControlPanelBackend, SAMPLE_ACCOUNT_INFO, SAMPLE_NAME, |
134 | ) |
135 | from ubuntuone.controlpanel.gui.qt.tests.test_ubuntuonebin import ( |
136 | UbuntuOneBinTestCase, |
137 | @@ -204,6 +211,39 @@ |
138 | self.ui.ui.wizard.pages[self.ui.ui.wizard.license_page]) |
139 | |
140 | |
141 | +class ControlPanelConnectionTestCase(TestCase): |
142 | + """Test qt signal connections from controlpanel.""" |
143 | + |
144 | + @defer.inlineCallbacks |
145 | + def setUp(self): |
146 | + cache.Cache._shared_objects = {} |
147 | + yield super(ControlPanelConnectionTestCase, self).setUp() |
148 | + self.patch(backend, 'ControlBackend', FakedControlPanelBackend) |
149 | + |
150 | + self.mock_handler = Mock(name='handle_current_tab_changed') |
151 | + self.patch(share_links.ShareLinksPanel, 'handle_current_tab_changed', |
152 | + self.mock_handler) |
153 | + |
154 | + self.ui = gui.ControlPanel() |
155 | + self.ui.show() |
156 | + self.addCleanup(self.ui.hide) |
157 | + #self.addCleanup(self.ui.deleteLater) |
158 | + self.addCleanup(QtCore.QCoreApplication.instance().processEvents) |
159 | + |
160 | + if getattr(self.ui, 'backend', None) is not None: |
161 | + self.addCleanup(self.ui.backend._called.clear) |
162 | + |
163 | + def test_popup_hides_when_switching_tab(self): |
164 | + """Test that the share_links_tab gets the signal for changed tabs""" |
165 | + folders_index = self.ui.ui.tab_widget.indexOf(self.ui.ui.folders_tab) |
166 | + share_index = self.ui.ui.tab_widget.indexOf(self.ui.ui.share_links_tab) |
167 | + self.ui.ui.tab_widget.setCurrentIndex(share_index) |
168 | + self.ui.ui.tab_widget.setCurrentIndex(folders_index) |
169 | + |
170 | + self.assertEqual(self.mock_handler.mock_calls, |
171 | + [call(share_index), call(folders_index)]) |
172 | + |
173 | + |
174 | class ExternalLinkButtonsTestCase(ControlPanelTestCase): |
175 | """The link in the go-to-web buttons are correct.""" |
176 | |
177 | |
178 | === modified file 'ubuntuone/controlpanel/gui/qt/tests/test_share_links.py' |
179 | --- ubuntuone/controlpanel/gui/qt/tests/test_share_links.py 2012-11-02 17:15:15 +0000 |
180 | +++ ubuntuone/controlpanel/gui/qt/tests/test_share_links.py 2013-03-26 20:55:24 +0000 |
181 | @@ -254,6 +254,14 @@ |
182 | os.path.basename(file_path)) |
183 | self.assertEqual(widget.ui.lbl_path.text(), file_path) |
184 | |
185 | + def test_hide_popup_on_tab_changed(self): |
186 | + """Test that the popup is hidden by the tab changed signal.""" |
187 | + |
188 | + self.ui.ui.line_search.popup.show() |
189 | + self.ui.handle_current_tab_changed(0) |
190 | + self.assertFalse(self.ui.ui.line_search.popup.isVisible()) |
191 | + self.assertFalse(self.ui.ui.line_search.hasFocus()) |
192 | + |
193 | |
194 | class ActionsButtonsTestCase(BaseTestCase): |
195 | """Test the Actions Buttons.""" |
196 | |
197 | === modified file 'ubuntuone/controlpanel/gui/qt/tests/test_share_links_search.py' |
198 | --- ubuntuone/controlpanel/gui/qt/tests/test_share_links_search.py 2013-01-24 21:35:59 +0000 |
199 | +++ ubuntuone/controlpanel/gui/qt/tests/test_share_links_search.py 2013-03-26 20:55:24 +0000 |
200 | @@ -24,7 +24,7 @@ |
201 | from ubuntuone.controlpanel.gui.qt import share_links_search as gui |
202 | from ubuntuone.controlpanel.gui.qt.tests import BaseTestCase |
203 | |
204 | -from mock import call, patch |
205 | +from mock import patch |
206 | |
207 | # pylint: disable=W0212 |
208 | |
209 | @@ -175,7 +175,7 @@ |
210 | |
211 | |
212 | class SearchingTestCase(BaseTestCase): |
213 | - """test _do_search by itself and with multiple calls to handle_text.""" |
214 | + """Set up patches used by subclasses.""" |
215 | class_ui = gui.SearchBox |
216 | |
217 | @defer.inlineCallbacks |
218 | @@ -208,6 +208,9 @@ |
219 | self.mock_load_items = self.load_items_patch.start() |
220 | self.addCleanup(self.load_items_patch.stop) |
221 | |
222 | + |
223 | +class DoSearchTestCase(SearchingTestCase): |
224 | + """A subclass so that MultipleSearchingTestCase doesn't also call these.""" |
225 | @defer.inlineCallbacks |
226 | def test_do_search_text_same(self): |
227 | """If searchbox text same after search_files call, call load_items.""" |
228 | @@ -231,11 +234,37 @@ |
229 | yield d |
230 | self.assertFalse(self.mock_load_items.called) |
231 | |
232 | + |
233 | +class MultipleSearchingTestCase(SearchingTestCase): |
234 | + """Test multiple fast calls to handle_text.""" |
235 | + |
236 | + @defer.inlineCallbacks |
237 | + def setUp(self): |
238 | + # do all this before calling super, because ui._do_search is |
239 | + # connected to a Qt signal in the __init__ of the ui class, |
240 | + # and we need to connect our patched version: |
241 | + self.do_search_ended = defer.DeferredQueue() |
242 | + # save unbound function because self.ui won't exist yet: |
243 | + self.orig_do_search = gui.SearchBox._do_search |
244 | + |
245 | + @defer.inlineCallbacks |
246 | + def do_search_later(): |
247 | + # call unbound function with now-existing self.ui: |
248 | + yield self.orig_do_search(self.ui) |
249 | + self.do_search_ended.put("done") |
250 | + |
251 | + self.do_search_patch = patch.object(gui.SearchBox, '_do_search') |
252 | + self.do_search_mock = self.do_search_patch.start() |
253 | + self.do_search_mock.side_effect = do_search_later |
254 | + self.addCleanup(self.do_search_patch.stop) |
255 | + |
256 | + yield super(MultipleSearchingTestCase, self).setUp() |
257 | + |
258 | @defer.inlineCallbacks |
259 | def test_multiple_searches_while_waiting(self): |
260 | """Only call load_items once despite multiple quick text changes.""" |
261 | |
262 | - # This test checks the case not covered in other tests, where |
263 | + # This test checks a case not covered earlier, where |
264 | # text changes once, then the text changes again, after |
265 | # _do_search is called but before search_files has returned. |
266 | |
267 | @@ -243,48 +272,37 @@ |
268 | # be called twice, but _load_items should only be called once, |
269 | # by the last call to _do_search. |
270 | |
271 | - self.do_search_ended = defer.DeferredQueue() |
272 | - self.orig_do_search = self.ui._do_search |
273 | - |
274 | - @defer.inlineCallbacks |
275 | - def do_search_later(): |
276 | - yield self.orig_do_search() |
277 | - self.do_search_ended.put("done") |
278 | - |
279 | - with patch.object(self.ui, '_do_search') as mock_do_search: |
280 | - mock_do_search.side_effect = do_search_later |
281 | - |
282 | - # call once with original query |
283 | - self.mock_text.return_value = 'query' |
284 | - self.ui.handle_text_changed('query') |
285 | - |
286 | - # wait for the first delayed do_search call to call |
287 | - # search_files and check its arg for good measure |
288 | - search_files_query = yield self.search_files_started_q.get() |
289 | - self.assertEqual('query', search_files_query) |
290 | - |
291 | - # first call to search_files is paused waiting for |
292 | - # search_files_done_q, simulating a long IPC call. |
293 | - # the delayed call to do_search is no longer active. |
294 | - |
295 | - # while we wait for search_files, the user changes the |
296 | - # query, scheduling a new delayed call to do_search: |
297 | - self.mock_text.return_value = 'query2' |
298 | - self.ui.handle_text_changed('query2') |
299 | - |
300 | - # release both calls to search_files: |
301 | - self.search_files_done_q.put(['result1']) |
302 | - self.search_files_done_q.put(['result2']) |
303 | - |
304 | - # wait for first delayed call to do_search to finish: |
305 | - yield self.do_search_ended.get() |
306 | - |
307 | - # check that the second call to search_files got the right |
308 | - # text: |
309 | - search_files_query = yield self.search_files_started_q.get() |
310 | - self.assertEqual('query2', search_files_query) |
311 | - # wait for second do_search to end |
312 | - yield self.do_search_ended.get() |
313 | + # call once with original query |
314 | + self.mock_text.return_value = 'query' |
315 | + self.ui.handle_text_changed('query') |
316 | + |
317 | + # wait for the first delayed do_search call to call |
318 | + # search_files and check its arg for good measure |
319 | + search_files_query = yield self.search_files_started_q.get() |
320 | + self.assertEqual('query', search_files_query) |
321 | + |
322 | + # first call to search_files is paused waiting for |
323 | + # search_files_done_q, simulating a long IPC call. |
324 | + # the delayed call to do_search is no longer active. |
325 | + |
326 | + # while we wait for search_files, the user changes the |
327 | + # query, scheduling a new delayed call to do_search: |
328 | + self.mock_text.return_value = 'query2' |
329 | + self.ui.handle_text_changed('query2') |
330 | + |
331 | + # release both calls to search_files: |
332 | + self.search_files_done_q.put(['result1']) |
333 | + self.search_files_done_q.put(['result2']) |
334 | + |
335 | + # wait for first delayed call to do_search to finish: |
336 | + yield self.do_search_ended.get() |
337 | + |
338 | + # check that the second call to search_files got the right |
339 | + # text: |
340 | + search_files_query = yield self.search_files_started_q.get() |
341 | + self.assertEqual('query2', search_files_query) |
342 | + # wait for second do_search to end |
343 | + yield self.do_search_ended.get() |
344 | |
345 | # check that _load_items is only called once, and that it's |
346 | # using only the results from the last call to search_files: |
347 | @@ -292,83 +310,6 @@ |
348 | self.assertEqual(['result2'], self.ui.current_results) |
349 | |
350 | |
351 | -class TextChangedTestCase(BaseTestCase): |
352 | - """Test handle_text_changed scheduling pending calls.""" |
353 | - |
354 | - class_ui = gui.SearchBox |
355 | - |
356 | - @defer.inlineCallbacks |
357 | - def setUp(self): |
358 | - yield super(TextChangedTestCase, self).setUp() |
359 | - |
360 | - # patch this way instead of using decorators because we need |
361 | - # to patch self.ui.popup, which isn't defined yet when the |
362 | - # decorators run. |
363 | - self.qtreactor_patch = patch.object(gui.SearchBox, 'qtreactor') |
364 | - self.mock_qtreactor = self.qtreactor_patch.start() |
365 | - self.addCleanup(self.qtreactor_patch.stop) |
366 | - |
367 | - self.popup_patch = patch.object(self.ui, 'popup') |
368 | - self.mock_popup = self.popup_patch.start() |
369 | - self.addCleanup(self.popup_patch.stop) |
370 | - |
371 | - def test_empty_text_no_pending(self): |
372 | - """arg='' with no pending call only hides the popup.""" |
373 | - self.ui.handle_text_changed('') |
374 | - self.mock_popup.hide.assert_called_once_with() |
375 | - self.assertFalse(self.mock_qtreactor.callLater.called) |
376 | - |
377 | - def test_empty_text_with_pending(self): |
378 | - """arg='' with a pending call hides the popup and cancels pending.""" |
379 | - with patch.object(self.ui, 'pending_call') as mock_pending_call: |
380 | - mock_pending_call.active.return_value = True |
381 | - self.ui.handle_text_changed('') |
382 | - mock_pending_call.active.assert_called_once_with() |
383 | - mock_pending_call.cancel.assert_called_once_with() |
384 | - self.mock_popup.hide.assert_called_once_with() |
385 | - self.assertFalse(self.mock_qtreactor.callLater.called) |
386 | - |
387 | - def test_empty_text_with_inactive_pending(self): |
388 | - """arg='' with a pending call hides the popup and cancels pending.""" |
389 | - with patch.object(self.ui, 'pending_call') as mock_pending_call: |
390 | - mock_pending_call.active.return_value = False |
391 | - self.ui.handle_text_changed('') |
392 | - mock_pending_call.active.assert_called_once_with() |
393 | - self.assertFalse(mock_pending_call.cancel.called) |
394 | - self.mock_popup.hide.assert_called_once_with() |
395 | - self.assertFalse(self.mock_qtreactor.callLater.called) |
396 | - |
397 | - def test_nonempty_text_with_no_pending(self): |
398 | - """arg='b' with no pending call schedules a call.""" |
399 | - self.ui.handle_text_changed('b') |
400 | - self.assertEqual([call(0.1, self.ui._do_search)], |
401 | - self.mock_qtreactor.callLater.mock_calls) |
402 | - |
403 | - def test_nonempty_text_with_inactive_pending(self): |
404 | - """call an inactive (called already) call schedules another call.""" |
405 | - with patch.object(self.ui, 'pending_call') as mock_pending_call: |
406 | - mock_pending_call.active.return_value = False |
407 | - self.ui.handle_text_changed('b') |
408 | - mock_pending_call.active.assert_called_once_with() |
409 | - self.assertFalse(mock_pending_call.reset.called) |
410 | - |
411 | - self.assertFalse(self.mock_popup.hide.called) |
412 | - |
413 | - self.assertEqual([call(0.1, self.ui._do_search)], |
414 | - self.mock_qtreactor.callLater.mock_calls) |
415 | - |
416 | - def test_nonempty_text_with_pending(self): |
417 | - """arg='' with a pending call hides the popup and resets pending.""" |
418 | - with patch.object(self.ui, 'pending_call') as mock_pending_call: |
419 | - mock_pending_call.active.return_value = True |
420 | - self.ui.handle_text_changed('b') |
421 | - mock_pending_call.active.assert_called_once_with() |
422 | - mock_pending_call.reset.assert_called_once_with(0.1) |
423 | - |
424 | - self.assertFalse(self.mock_popup.hide.called) |
425 | - self.assertFalse(self.mock_qtreactor.callLater.called) |
426 | - |
427 | - |
428 | class FileItemTestCase(BaseTestCase): |
429 | """Test the File Item.""" |
430 |