1# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
2
3# Copyright 2018-2021 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
4#
5# This file is part of qutebrowser.
6#
7# qutebrowser is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# qutebrowser is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with qutebrowser.  If not, see <https://www.gnu.org/licenses/>.
19
20"""Tests for caret browsing mode."""
21
22import textwrap
23
24import pytest
25from PyQt5.QtCore import QUrl
26
27from qutebrowser.utils import usertypes
28from qutebrowser.browser import browsertab
29
30
31@pytest.fixture
32def caret(web_tab, qtbot, mode_manager):
33    web_tab.container.expose()
34
35    with qtbot.wait_signal(web_tab.load_finished, timeout=10000):
36        web_tab.load_url(QUrl('qute://testdata/data/caret.html'))
37
38    with qtbot.wait_signal(web_tab.caret.selection_toggled):
39        mode_manager.enter(usertypes.KeyMode.caret)
40
41    return web_tab.caret
42
43
44class Selection:
45
46    """Helper to interact with the caret selection."""
47
48    def __init__(self, qtbot, caret):
49        self._qtbot = qtbot
50        self._caret = caret
51
52    def check(self, expected, *, strip=False):
53        """Check whether we got the expected selection.
54
55        Since (especially on Windows) the selection is empty if we're checking
56        too quickly, we try to read it multiple times.
57        """
58        for _ in range(10):
59            with self._qtbot.wait_callback() as callback:
60                self._caret.selection(callback)
61
62            selection = callback.args[0]
63            if selection:
64                if strip:
65                    selection = selection.strip()
66                assert selection == expected
67                return
68            elif not selection and not expected:
69                return
70
71            self._qtbot.wait(50)
72
73        assert False, 'Failed to get selection!'
74
75    def check_multiline(self, expected, *, strip=False):
76        self.check(textwrap.dedent(expected).strip(), strip=strip)
77
78    def toggle(self, *, line=False):
79        """Toggle the selection and return the new selection state."""
80        with self._qtbot.wait_signal(self._caret.selection_toggled) as blocker:
81            self._caret.toggle_selection(line=line)
82        return blocker.args[0]
83
84
85@pytest.fixture
86def selection(qtbot, caret):
87    return Selection(qtbot, caret)
88
89
90def test_toggle(caret, selection, qtbot):
91    """Make sure calling toggleSelection produces the correct callback values.
92
93    This also makes sure that the SelectionState enum in JS lines up with the
94    Python browsertab.SelectionState enum.
95    """
96    assert selection.toggle() == browsertab.SelectionState.normal
97    assert selection.toggle(line=True) == browsertab.SelectionState.line
98    assert selection.toggle() == browsertab.SelectionState.normal
99    assert selection.toggle() == browsertab.SelectionState.none
100
101
102def test_selection_callback_wrong_mode(qtbot, caplog,
103                                       webengine_tab, mode_manager):
104    """Test what calling the selection callback outside of caret mode.
105
106    It should be ignored, as something could have left caret mode while the
107    async callback was happening, so we don't want to mess with the status bar.
108    """
109    assert mode_manager.mode == usertypes.KeyMode.normal
110    with qtbot.assert_not_emitted(webengine_tab.caret.selection_toggled):
111        webengine_tab.caret._toggle_sel_translate('normal')
112
113    msg = 'Ignoring caret selection callback in KeyMode.normal'
114    assert caplog.messages == [msg]
115
116
117class TestDocument:
118
119    def test_selecting_entire_document(self, caret, selection):
120        selection.toggle()
121        caret.move_to_end_of_document()
122        selection.check_multiline("""
123            one two three
124            eins zwei drei
125
126            four five six
127            vier fünf sechs
128        """, strip=True)
129
130    def test_moving_to_end_and_start(self, caret, selection):
131        caret.move_to_end_of_document()
132        caret.move_to_start_of_document()
133        selection.toggle()
134        caret.move_to_end_of_word()
135        selection.check("one")
136
137    def test_moving_to_end_and_start_with_selection(self, caret, selection):
138        caret.move_to_end_of_document()
139        selection.toggle()
140        caret.move_to_start_of_document()
141        selection.check_multiline("""
142            one two three
143            eins zwei drei
144
145            four five six
146            vier fünf sechs
147        """, strip=True)
148
149
150class TestBlock:
151
152    def test_selecting_block(self, caret, selection):
153        selection.toggle()
154        caret.move_to_end_of_next_block()
155        selection.check_multiline("""
156            one two three
157            eins zwei drei
158        """)
159
160    def test_moving_back_to_the_end_of_prev_block_with_sel(self, caret, selection):
161        caret.move_to_end_of_next_block(2)
162        selection.toggle()
163        caret.move_to_end_of_prev_block()
164        caret.move_to_prev_word()
165        selection.check_multiline("""
166            drei
167
168            four five six
169        """)
170
171    def test_moving_back_to_the_end_of_prev_block(self, caret, selection):
172        caret.move_to_end_of_next_block(2)
173        caret.move_to_end_of_prev_block()
174        selection.toggle()
175        caret.move_to_prev_word()
176        selection.check("drei")
177
178    def test_moving_back_to_the_start_of_prev_block_with_sel(self, caret, selection):
179        caret.move_to_end_of_next_block(2)
180        selection.toggle()
181        caret.move_to_start_of_prev_block()
182        selection.check_multiline("""
183            eins zwei drei
184
185            four five six
186        """)
187
188    def test_moving_back_to_the_start_of_prev_block(self, caret, selection):
189        caret.move_to_end_of_next_block(2)
190        caret.move_to_start_of_prev_block()
191        selection.toggle()
192        caret.move_to_next_word()
193        selection.check("eins ")
194
195    def test_moving_to_the_start_of_next_block_with_sel(self, caret, selection):
196        selection.toggle()
197        caret.move_to_start_of_next_block()
198        selection.check("one two three\n")
199
200    def test_moving_to_the_start_of_next_block(self, caret, selection):
201        caret.move_to_start_of_next_block()
202        selection.toggle()
203        caret.move_to_end_of_word()
204        selection.check("eins")
205
206
207class TestLine:
208
209    def test_selecting_a_line(self, caret, selection):
210        selection.toggle()
211        caret.move_to_end_of_line()
212        selection.check("one two three")
213
214    def test_moving_and_selecting_a_line(self, caret, selection):
215        caret.move_to_next_line()
216        selection.toggle()
217        caret.move_to_end_of_line()
218        selection.check("eins zwei drei")
219
220    def test_selecting_next_line(self, caret, selection):
221        selection.toggle()
222        caret.move_to_next_line()
223        selection.check("one two three\n")
224
225    def test_moving_to_end_and_to_start_of_line(self, caret, selection):
226        caret.move_to_end_of_line()
227        caret.move_to_start_of_line()
228        selection.toggle()
229        caret.move_to_end_of_word()
230        selection.check("one")
231
232    def test_selecting_a_line_backwards(self, caret, selection):
233        caret.move_to_end_of_line()
234        selection.toggle()
235        caret.move_to_start_of_line()
236        selection.check("one two three")
237
238    def test_selecting_previous_line(self, caret, selection):
239        caret.move_to_next_line()
240        selection.toggle()
241        caret.move_to_prev_line()
242        selection.check("one two three\n")
243
244    def test_moving_to_previous_line(self, caret, selection):
245        caret.move_to_next_line()
246        caret.move_to_prev_line()
247        selection.toggle()
248        caret.move_to_next_line()
249        selection.check("one two three\n")
250
251
252class TestWord:
253
254    def test_selecting_a_word(self, caret, selection):
255        selection.toggle()
256        caret.move_to_end_of_word()
257        selection.check("one")
258
259    def test_moving_to_end_and_selecting_a_word(self, caret, selection):
260        caret.move_to_end_of_word()
261        selection.toggle()
262        caret.move_to_end_of_word()
263        selection.check(" two")
264
265    def test_moving_to_next_word_and_selecting_a_word(self, caret, selection):
266        caret.move_to_next_word()
267        selection.toggle()
268        caret.move_to_end_of_word()
269        selection.check("two")
270
271    def test_moving_to_next_word_and_selecting_until_next_word(self, caret, selection):
272        caret.move_to_next_word()
273        selection.toggle()
274        caret.move_to_next_word()
275        selection.check("two ")
276
277    def test_moving_to_previous_word_and_selecting_a_word(self, caret, selection):
278        caret.move_to_end_of_word()
279        selection.toggle()
280        caret.move_to_prev_word()
281        selection.check("one")
282
283    def test_moving_to_previous_word(self, caret, selection):
284        caret.move_to_end_of_word()
285        caret.move_to_prev_word()
286        selection.toggle()
287        caret.move_to_end_of_word()
288        selection.check("one")
289
290
291class TestChar:
292
293    def test_selecting_a_char(self, caret, selection):
294        selection.toggle()
295        caret.move_to_next_char()
296        selection.check("o")
297
298    def test_moving_and_selecting_a_char(self, caret, selection):
299        caret.move_to_next_char()
300        selection.toggle()
301        caret.move_to_next_char()
302        selection.check("n")
303
304    def test_selecting_previous_char(self, caret, selection):
305        caret.move_to_end_of_word()
306        selection.toggle()
307        caret.move_to_prev_char()
308        selection.check("e")
309
310    def test_moving_to_previous_char(self, caret, selection):
311        caret.move_to_end_of_word()
312        caret.move_to_prev_char()
313        selection.toggle()
314        caret.move_to_end_of_word()
315        selection.check("e")
316
317
318def test_drop_selection(caret, selection):
319    selection.toggle()
320    caret.move_to_end_of_word()
321    caret.drop_selection()
322    selection.check("")
323
324
325class TestSearch:
326
327    @pytest.mark.no_xvfb
328    def test_yanking_a_searched_line(self, caret, selection, mode_manager, web_tab, qtbot):
329        mode_manager.leave(usertypes.KeyMode.caret)
330
331        with qtbot.wait_callback() as callback:
332            web_tab.search.search('fiv', result_cb=callback)
333        callback.assert_called_with(True)
334
335        mode_manager.enter(usertypes.KeyMode.caret)
336        caret.move_to_end_of_line()
337        selection.check('five six')
338
339    @pytest.mark.no_xvfb
340    def test_yanking_a_searched_line_with_multiple_matches(self, caret, selection, mode_manager, web_tab, qtbot):
341        mode_manager.leave(usertypes.KeyMode.caret)
342
343        with qtbot.wait_callback() as callback:
344            web_tab.search.search('w', result_cb=callback)
345        callback.assert_called_with(True)
346
347        with qtbot.wait_callback() as callback:
348            web_tab.search.next_result(result_cb=callback)
349        callback.assert_called_with(True)
350
351        mode_manager.enter(usertypes.KeyMode.caret)
352
353        caret.move_to_end_of_line()
354        selection.check('wei drei')
355
356
357class TestFollowSelected:
358
359    LOAD_STARTED_DELAY = 50
360
361    @pytest.fixture(params=[True, False], autouse=True)
362    def toggle_js(self, request, config_stub):
363        config_stub.val.content.javascript.enabled = request.param
364
365    def test_follow_selected_without_a_selection(self, qtbot, caret, selection, web_tab,
366                                                 mode_manager):
367        caret.move_to_next_word()  # Move cursor away from the link
368        mode_manager.leave(usertypes.KeyMode.caret)
369        with qtbot.wait_signal(caret.follow_selected_done):
370            with qtbot.assert_not_emitted(web_tab.load_started,
371                                          wait=self.LOAD_STARTED_DELAY):
372                caret.follow_selected()
373
374    def test_follow_selected_with_text(self, qtbot, caret, selection, web_tab):
375        caret.move_to_next_word()
376        selection.toggle()
377        caret.move_to_end_of_word()
378        with qtbot.wait_signal(caret.follow_selected_done):
379            with qtbot.assert_not_emitted(web_tab.load_started,
380                                          wait=self.LOAD_STARTED_DELAY):
381                caret.follow_selected()
382
383    def test_follow_selected_with_link(self, caret, selection, config_stub,
384                                       qtbot, web_tab):
385        selection.toggle()
386        caret.move_to_end_of_word()
387        with qtbot.wait_signal(web_tab.load_finished):
388            with qtbot.wait_signal(caret.follow_selected_done):
389                caret.follow_selected()
390        assert web_tab.url().path() == '/data/hello.txt'
391
392
393class TestReverse:
394
395    def test_does_not_change_selection(self, caret, selection):
396        selection.toggle()
397        caret.reverse_selection()
398        selection.check("")
399
400    def test_repetition_of_movement_results_in_empty_selection(self, caret, selection):
401        selection.toggle()
402        caret.move_to_end_of_word()
403        caret.reverse_selection()
404        caret.move_to_end_of_word()
405        selection.check("")
406
407    def test_reverse(self, caret, selection):
408        selection.toggle()
409        caret.move_to_end_of_word()
410        caret.reverse_selection()
411        caret.move_to_next_char()
412        selection.check("ne")
413        caret.reverse_selection()
414        caret.move_to_next_char()
415        selection.check("ne ")
416        caret.move_to_end_of_line()
417        selection.check("ne two three")
418        caret.reverse_selection()
419        caret.move_to_start_of_line()
420        selection.check("one two three")
421
422
423class TestLineSelection:
424
425    def test_toggle(self, caret, selection):
426        selection.toggle(line=True)
427        selection.check("one two three")
428
429    def test_toggle_untoggle(self, caret, selection):
430        selection.toggle()
431        selection.check("")
432        selection.toggle(line=True)
433        selection.check("one two three")
434        selection.toggle()
435        selection.check("one two three")
436
437    def test_from_center(self, caret, selection):
438        caret.move_to_next_char(4)
439        selection.toggle(line=True)
440        selection.check("one two three")
441
442    def test_more_lines(self, caret, selection):
443        selection.toggle(line=True)
444        caret.move_to_next_line(2)
445        selection.check_multiline("""
446            one two three
447            eins zwei drei
448
449            four five six
450        """, strip=True)
451
452    def test_not_selecting_char(self, caret, selection):
453        selection.toggle(line=True)
454        caret.move_to_next_char()
455        selection.check("one two three")
456        caret.move_to_prev_char()
457        selection.check("one two three")
458
459    def test_selecting_prev_next_word(self, caret, selection):
460        selection.toggle(line=True)
461        caret.move_to_next_word()
462        selection.check("one two three")
463        caret.move_to_prev_word()
464        selection.check("one two three")
465
466    def test_selecting_end_word(self, caret, selection):
467        selection.toggle(line=True)
468        caret.move_to_end_of_word()
469        selection.check("one two three")
470
471    def test_selecting_prev_next_line(self, caret, selection):
472        selection.toggle(line=True)
473        caret.move_to_next_line()
474        selection.check_multiline("""
475            one two three
476            eins zwei drei
477        """, strip=True)
478        caret.move_to_prev_line()
479        selection.check("one two three")
480
481    def test_not_selecting_start_end_line(self, caret, selection):
482        selection.toggle(line=True)
483        caret.move_to_end_of_line()
484        selection.check("one two three")
485        caret.move_to_start_of_line()
486        selection.check("one two three")
487
488    def test_selecting_block(self, caret, selection):
489        selection.toggle(line=True)
490        caret.move_to_end_of_next_block()
491        selection.check_multiline("""
492            one two three
493            eins zwei drei
494        """, strip=True)
495
496    @pytest.mark.not_mac(
497        reason='https://github.com/qutebrowser/qutebrowser/issues/5459')
498    def test_selecting_start_end_document(self, caret, selection):
499        selection.toggle(line=True)
500        caret.move_to_end_of_document()
501        selection.check_multiline("""
502            one two three
503            eins zwei drei
504
505            four five six
506            vier fünf sechs
507        """, strip=True)
508
509        caret.move_to_start_of_document()
510        selection.check("one two three")
511