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