1# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: 2 3# Copyright 2016-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"""Test starting qutebrowser with special arguments/environments.""" 21 22import configparser 23import subprocess 24import sys 25import logging 26import importlib 27import re 28import json 29import platform 30 31import pytest 32from PyQt5.QtCore import QProcess, QPoint 33 34from helpers import testutils 35from qutebrowser.utils import qtutils, utils 36 37 38ascii_locale = pytest.mark.skipif(sys.hexversion >= 0x03070000, 39 reason="Python >= 3.7 doesn't force ASCII " 40 "locale with LC_ALL=C") 41 42 43# For some reason (some floating point rounding differences?), color values are 44# slightly different (and wrong!) on ARM machines. We adjust our expected values 45# accordingly, since we don't really care about the exact value, we just want to 46# know that the underlying Chromium is respecting our preferences. 47# FIXME what to do about 32-bit ARM? 48IS_ARM = platform.machine() == 'aarch64' 49 50 51def _base_args(config): 52 """Get the arguments to pass with every invocation.""" 53 args = ['--debug', '--json-logging', '--no-err-windows'] 54 if config.webengine: 55 args += ['--backend', 'webengine'] 56 else: 57 args += ['--backend', 'webkit'] 58 59 if config.webengine: 60 args += testutils.seccomp_args(qt_flag=True) 61 62 args.append('about:blank') 63 return args 64 65 66@pytest.fixture 67def runtime_tmpdir(short_tmpdir): 68 """A directory suitable for XDG_RUNTIME_DIR.""" 69 runtime_dir = short_tmpdir / 'rt' 70 runtime_dir.ensure(dir=True) 71 runtime_dir.chmod(0o700) 72 return runtime_dir 73 74 75@pytest.fixture 76def temp_basedir_env(tmp_path, runtime_tmpdir): 77 """Return a dict of environment variables that fakes --temp-basedir. 78 79 We can't run --basedir or --temp-basedir for some tests, so we mess with 80 XDG_*_DIR to get things relocated. 81 """ 82 data_dir = tmp_path / 'data' 83 config_dir = tmp_path / 'config' 84 cache_dir = tmp_path / 'cache' 85 86 lines = [ 87 '[general]', 88 'quickstart-done = 1', 89 'backend-warning-shown = 1', 90 'webkit-warning-shown = 1', 91 ] 92 93 state_file = data_dir / 'qutebrowser' / 'state' 94 state_file.parent.mkdir(parents=True) 95 state_file.write_text('\n'.join(lines), encoding='utf-8') 96 97 env = { 98 'XDG_DATA_HOME': str(data_dir), 99 'XDG_CONFIG_HOME': str(config_dir), 100 'XDG_RUNTIME_DIR': str(runtime_tmpdir), 101 'XDG_CACHE_HOME': str(cache_dir), 102 } 103 return env 104 105 106@pytest.mark.linux 107@ascii_locale 108def test_downloads_with_ascii_locale(request, server, tmp_path, quteproc_new): 109 """Test downloads with LC_ALL=C set. 110 111 https://github.com/qutebrowser/qutebrowser/issues/908 112 https://github.com/qutebrowser/qutebrowser/issues/1726 113 """ 114 args = ['--temp-basedir'] + _base_args(request.config) 115 quteproc_new.start(args, env={'LC_ALL': 'C'}) 116 quteproc_new.set_setting('downloads.location.directory', str(tmp_path)) 117 118 # Test a normal download 119 quteproc_new.set_setting('downloads.location.prompt', 'false') 120 url = 'http://localhost:{port}/data/downloads/ä-issue908.bin'.format( 121 port=server.port) 122 quteproc_new.send_cmd(':download {}'.format(url)) 123 quteproc_new.wait_for(category='downloads', 124 message='Download ?-issue908.bin finished') 125 126 # Test :prompt-open-download 127 quteproc_new.set_setting('downloads.location.prompt', 'true') 128 quteproc_new.send_cmd(':download {}'.format(url)) 129 quteproc_new.send_cmd(':prompt-open-download "{}" -c pass' 130 .format(sys.executable)) 131 quteproc_new.wait_for(category='downloads', 132 message='Download ä-issue908.bin finished') 133 quteproc_new.wait_for(category='misc', 134 message='Opening * with [*python*]') 135 136 assert len(list(tmp_path.iterdir())) == 1 137 assert (tmp_path / '?-issue908.bin').exists() 138 139 140@pytest.mark.linux 141@pytest.mark.parametrize('url', ['/föö.html', 'file:///föö.html']) 142@ascii_locale 143def test_open_with_ascii_locale(request, server, tmp_path, quteproc_new, url): 144 """Test opening non-ascii URL with LC_ALL=C set. 145 146 https://github.com/qutebrowser/qutebrowser/issues/1450 147 """ 148 args = ['--temp-basedir'] + _base_args(request.config) 149 quteproc_new.start(args, env={'LC_ALL': 'C'}) 150 quteproc_new.set_setting('url.auto_search', 'never') 151 152 # Test opening a file whose name contains non-ascii characters. 153 # No exception thrown means test success. 154 quteproc_new.send_cmd(':open {}'.format(url)) 155 156 if not request.config.webengine: 157 line = quteproc_new.wait_for(message="Error while loading *: Error " 158 "opening /*: No such file or directory") 159 line.expected = True 160 161 quteproc_new.wait_for(message="load status for <* tab_id=* " 162 "url='*/f%C3%B6%C3%B6.html'>: LoadStatus.error") 163 164 if request.config.webengine: 165 line = quteproc_new.wait_for(message='Load error: ERR_FILE_NOT_FOUND') 166 line.expected = True 167 168 169@pytest.mark.linux 170@ascii_locale 171def test_open_command_line_with_ascii_locale(request, server, tmp_path, 172 quteproc_new): 173 """Test opening file via command line with a non-ascii name with LC_ALL=C. 174 175 https://github.com/qutebrowser/qutebrowser/issues/1450 176 """ 177 # The file does not actually have to exist because the relevant checks will 178 # all be called. No exception thrown means test success. 179 args = (['--temp-basedir'] + _base_args(request.config) + 180 ['/home/user/föö.html']) 181 quteproc_new.start(args, env={'LC_ALL': 'C'}) 182 183 if not request.config.webengine: 184 line = quteproc_new.wait_for(message="Error while loading *: Error " 185 "opening /*: No such file or directory") 186 line.expected = True 187 188 quteproc_new.wait_for(message="load status for <* tab_id=* " 189 "url='*/f*.html'>: LoadStatus.error") 190 191 if request.config.webengine: 192 line = quteproc_new.wait_for(message="Load error: ERR_FILE_NOT_FOUND") 193 line.expected = True 194 195 196@pytest.mark.linux 197def test_misconfigured_user_dirs(request, server, temp_basedir_env, 198 tmp_path, quteproc_new): 199 """Test downloads with a misconfigured XDG_DOWNLOAD_DIR. 200 201 https://github.com/qutebrowser/qutebrowser/issues/866 202 https://github.com/qutebrowser/qutebrowser/issues/1269 203 """ 204 home = tmp_path / 'home' 205 home.mkdir() 206 temp_basedir_env['HOME'] = str(home) 207 config_userdir_dir = (tmp_path / 'config') 208 config_userdir_dir.mkdir(parents=True) 209 config_userdir_file = (tmp_path / 'config' / 'user-dirs.dirs') 210 config_userdir_file.touch() 211 212 assert temp_basedir_env['XDG_CONFIG_HOME'] == str(tmp_path / 'config') 213 config_userdir_file.write_text('XDG_DOWNLOAD_DIR="relative"') 214 215 quteproc_new.start(_base_args(request.config), env=temp_basedir_env) 216 217 quteproc_new.set_setting('downloads.location.prompt', 'false') 218 url = 'http://localhost:{port}/data/downloads/download.bin'.format( 219 port=server.port) 220 quteproc_new.send_cmd(':download {}'.format(url)) 221 line = quteproc_new.wait_for( 222 loglevel=logging.ERROR, category='message', 223 message='XDG_DOWNLOAD_DIR points to a relative path - please check ' 224 'your ~/.config/user-dirs.dirs. The download is saved in your ' 225 'home directory.') 226 line.expected = True 227 quteproc_new.wait_for(category='downloads', 228 message='Download download.bin finished') 229 230 assert (home / 'download.bin').exists() 231 232 233def test_no_loglines(request, quteproc_new): 234 """Test qute://log with --loglines=0.""" 235 quteproc_new.start(args=['--temp-basedir', '--loglines=0'] + 236 _base_args(request.config)) 237 quteproc_new.open_path('qute://log') 238 assert quteproc_new.get_content() == 'Log output was disabled.' 239 240 241@pytest.mark.not_frozen 242@pytest.mark.parametrize('level', ['1', '2']) 243def test_optimize(request, quteproc_new, capfd, level): 244 quteproc_new.start(args=['--temp-basedir'] + _base_args(request.config), 245 env={'PYTHONOPTIMIZE': level}) 246 if level == '2': 247 msg = ("Running on optimize level higher than 1, unexpected behavior " 248 "may occur.") 249 line = quteproc_new.wait_for(message=msg) 250 line.expected = True 251 252 # Waiting for quit to make sure no other warning is emitted 253 quteproc_new.send_cmd(':quit') 254 quteproc_new.wait_for_quit() 255 256 257@pytest.mark.not_frozen 258@pytest.mark.flaky # Fails sometimes with empty output... 259def test_version(request): 260 """Test invocation with --version argument.""" 261 args = ['-m', 'qutebrowser', '--version'] + _base_args(request.config) 262 # can't use quteproc_new here because it's confused by 263 # early process termination 264 proc = QProcess() 265 proc.setProcessChannelMode(QProcess.SeparateChannels) 266 267 proc.start(sys.executable, args) 268 ok = proc.waitForStarted(2000) 269 assert ok 270 ok = proc.waitForFinished(10000) 271 272 stdout = bytes(proc.readAllStandardOutput()).decode('utf-8') 273 print(stdout) 274 stderr = bytes(proc.readAllStandardError()).decode('utf-8') 275 print(stderr) 276 277 assert ok 278 assert proc.exitStatus() == QProcess.NormalExit 279 280 match = re.search(r'^qutebrowser\s+v\d+(\.\d+)', stdout, re.MULTILINE) 281 assert match is not None 282 283 284def test_qt_arg(request, quteproc_new, tmp_path): 285 """Test --qt-arg.""" 286 args = (['--temp-basedir', '--qt-arg', 'stylesheet', 287 str(tmp_path / 'does-not-exist')] + _base_args(request.config)) 288 quteproc_new.start(args) 289 290 msg = 'QCss::Parser - Failed to load file "*does-not-exist"' 291 line = quteproc_new.wait_for(message=msg) 292 line.expected = True 293 294 quteproc_new.send_cmd(':quit') 295 quteproc_new.wait_for_quit() 296 297 298@pytest.mark.linux 299def test_webengine_download_suffix(request, quteproc_new, tmp_path): 300 """Make sure QtWebEngine does not add a suffix to downloads.""" 301 if not request.config.webengine: 302 pytest.skip() 303 304 download_dir = tmp_path / 'downloads' 305 download_dir.mkdir() 306 307 (tmp_path / 'user-dirs.dirs').write_text( 308 'XDG_DOWNLOAD_DIR={}'.format(download_dir)) 309 env = {'XDG_CONFIG_HOME': str(tmp_path)} 310 args = (['--temp-basedir'] + _base_args(request.config)) 311 quteproc_new.start(args, env=env) 312 313 quteproc_new.set_setting('downloads.location.prompt', 'false') 314 quteproc_new.set_setting('downloads.location.directory', str(download_dir)) 315 quteproc_new.open_path('data/downloads/download.bin', wait=False) 316 quteproc_new.wait_for(category='downloads', message='Download * finished') 317 quteproc_new.open_path('data/downloads/download.bin', wait=False) 318 quteproc_new.wait_for(message='Entering mode KeyMode.yesno *') 319 quteproc_new.send_cmd(':prompt-accept yes') 320 quteproc_new.wait_for(category='downloads', message='Download * finished') 321 322 files = list(download_dir.iterdir()) 323 assert len(files) == 1 324 assert files[0].name == 'download.bin' 325 326 327def test_command_on_start(request, quteproc_new): 328 """Make sure passing a command on start works. 329 330 See https://github.com/qutebrowser/qutebrowser/issues/2408 331 """ 332 args = (['--temp-basedir'] + _base_args(request.config) + 333 [':quickmark-add https://www.example.com/ example']) 334 quteproc_new.start(args) 335 quteproc_new.send_cmd(':quit') 336 quteproc_new.wait_for_quit() 337 338 339@pytest.mark.parametrize('python', ['python2', 'python3.5']) 340def test_launching_with_old_python(python): 341 try: 342 proc = subprocess.run( 343 [python, '-m', 'qutebrowser', '--no-err-windows'], 344 stderr=subprocess.PIPE, 345 check=False) 346 except FileNotFoundError: 347 pytest.skip(f"{python} not found") 348 assert proc.returncode == 1 349 error = "At least Python 3.6.1 is required to run qutebrowser" 350 assert proc.stderr.decode('ascii').startswith(error) 351 352 353def test_initial_private_browsing(request, quteproc_new): 354 """Make sure the initial window is private when the setting is set.""" 355 args = (_base_args(request.config) + 356 ['--temp-basedir', '-s', 'content.private_browsing', 'true']) 357 quteproc_new.start(args) 358 359 quteproc_new.compare_session(""" 360 windows: 361 - private: True 362 tabs: 363 - history: 364 - url: about:blank 365 """) 366 367 quteproc_new.send_cmd(':quit') 368 quteproc_new.wait_for_quit() 369 370 371def test_loading_empty_session(tmp_path, request, quteproc_new): 372 """Make sure loading an empty session opens a window.""" 373 session = tmp_path / 'session.yml' 374 session.write_text('windows: []') 375 376 args = _base_args(request.config) + ['--temp-basedir', '-r', str(session)] 377 quteproc_new.start(args) 378 379 quteproc_new.compare_session(""" 380 windows: 381 - tabs: 382 - history: 383 - url: about:blank 384 """) 385 386 quteproc_new.send_cmd(':quit') 387 quteproc_new.wait_for_quit() 388 389 390def test_qute_settings_persistence(short_tmpdir, request, quteproc_new): 391 """Make sure settings from qute://settings are persistent.""" 392 args = _base_args(request.config) + ['--basedir', str(short_tmpdir)] 393 quteproc_new.start(args) 394 quteproc_new.open_path('qute://settings/') 395 quteproc_new.send_cmd(':jseval --world main ' 396 'cset("search.ignore_case", "always")') 397 quteproc_new.wait_for(message='No output or error') 398 quteproc_new.wait_for(category='config', 399 message='Config option changed: ' 400 'search.ignore_case = always') 401 402 assert quteproc_new.get_setting('search.ignore_case') == 'always' 403 404 quteproc_new.send_cmd(':quit') 405 quteproc_new.wait_for_quit() 406 407 quteproc_new.start(args) 408 assert quteproc_new.get_setting('search.ignore_case') == 'always' 409 410 quteproc_new.send_cmd(':quit') 411 quteproc_new.wait_for_quit() 412 413 414@pytest.mark.parametrize('value, expected', [ 415 ('always', 'http://localhost:(port2)/headers-link/(port)'), 416 ('never', None), 417 ('same-domain', 'http://localhost:(port2)/'), # None with QtWebKit 418]) 419def test_referrer(quteproc_new, server, server2, request, value, expected): 420 """Check referrer settings.""" 421 args = _base_args(request.config) + [ 422 '--temp-basedir', 423 '-s', 'content.headers.referer', value, 424 ] 425 quteproc_new.start(args) 426 427 quteproc_new.open_path(f'headers-link/{server.port}', port=server2.port) 428 quteproc_new.send_cmd(':click-element id link') 429 quteproc_new.wait_for_load_finished('headers') 430 431 content = quteproc_new.get_content() 432 data = json.loads(content) 433 print(data) 434 headers = data['headers'] 435 436 if not request.config.webengine and value == 'same-domain': 437 # With QtWebKit and same-domain, we don't send a referer at all. 438 expected = None 439 440 if expected is not None: 441 for key, val in [('(port)', server.port), ('(port2)', server2.port)]: 442 expected = expected.replace(key, str(val)) 443 444 assert headers.get('Referer') == expected 445 446 447def test_preferred_colorscheme_unsupported(request, quteproc_new): 448 """Test versions without preferred-color-scheme support.""" 449 if request.config.webengine and qtutils.version_check('5.14'): 450 pytest.skip("preferred-color-scheme is supported") 451 452 args = _base_args(request.config) + ['--temp-basedir'] 453 quteproc_new.start(args) 454 quteproc_new.open_path('data/darkmode/prefers-color-scheme.html') 455 content = quteproc_new.get_content() 456 assert content == "Preference support missing." 457 458 459@pytest.mark.qtwebkit_skip 460@testutils.qt514 461@pytest.mark.parametrize('value', ["dark", "light", "auto", None]) 462def test_preferred_colorscheme(request, quteproc_new, value): 463 """Make sure the the preferred colorscheme is set.""" 464 if not request.config.webengine: 465 pytest.skip("Skipped with QtWebKit") 466 467 args = _base_args(request.config) + ['--temp-basedir'] 468 if value is not None: 469 args += ['-s', 'colors.webpage.preferred_color_scheme', value] 470 quteproc_new.start(args) 471 472 dark_text = "Dark preference detected." 473 light_text = "Light preference detected." 474 475 expected_values = { 476 "dark": [dark_text], 477 "light": [light_text], 478 479 # Depends on the environment the test is running in. 480 "auto": [dark_text, light_text], 481 None: [dark_text, light_text], 482 } 483 xfail = False 484 if not qtutils.version_check('5.15.2', compiled=False): 485 # On older versions, "light" is not supported, so the result will depend on the 486 # environment. 487 expected_values["light"].append(dark_text) 488 elif qtutils.version_check('5.15.2', exact=True, compiled=False): 489 # Test the WORKAROUND https://bugreports.qt.io/browse/QTBUG-89753 490 # With that workaround, we should always get the light preference. 491 for key in ["auto", None]: 492 expected_values[key].remove(dark_text) 493 xfail = value in ["auto", None] 494 495 quteproc_new.open_path('data/darkmode/prefers-color-scheme.html') 496 content = quteproc_new.get_content() 497 assert content in expected_values[value] 498 499 if xfail: 500 # Unsatisfactory result, but expected based on a Qt bug. 501 pytest.xfail("QTBUG-89753") 502 503 504@testutils.qt514 505def test_preferred_colorscheme_with_dark_mode( 506 request, quteproc_new, webengine_versions): 507 """Test interaction between preferred-color-scheme and dark mode.""" 508 if not request.config.webengine: 509 pytest.skip("Skipped with QtWebKit") 510 511 args = _base_args(request.config) + [ 512 '--temp-basedir', 513 '-s', 'colors.webpage.preferred_color_scheme', 'dark', 514 '-s', 'colors.webpage.darkmode.enabled', 'true', 515 '-s', 'colors.webpage.darkmode.algorithm', 'brightness-rgb', 516 ] 517 quteproc_new.start(args) 518 519 quteproc_new.open_path('data/darkmode/prefers-color-scheme.html') 520 content = quteproc_new.get_content() 521 522 qtwe_version = webengine_versions.webengine 523 xfail = None 524 if utils.VersionNumber(5, 15, 3) <= qtwe_version <= utils.VersionNumber(6): 525 # https://bugs.chromium.org/p/chromium/issues/detail?id=1177973 526 # No workaround known. 527 expected_text = 'Light preference detected.' 528 # light website color, inverted by darkmode 529 expected_color = (testutils.Color(123, 125, 123) if IS_ARM 530 else testutils.Color(127, 127, 127)) 531 xfail = "Chromium bug 1177973" 532 elif qtwe_version == utils.VersionNumber(5, 15, 2): 533 # Our workaround breaks when dark mode is enabled... 534 # Also, for some reason, dark mode doesn't work on that page either! 535 expected_text = 'No preference detected.' 536 expected_color = testutils.Color(0, 170, 0) # green 537 xfail = "QTBUG-89753" 538 else: 539 # Qt 5.14 and 5.15.0/.1 work correctly. 540 # Hopefully, so does Qt 6.x in the future? 541 expected_text = 'Dark preference detected.' 542 expected_color = (testutils.Color(33, 32, 33) if IS_ARM 543 else testutils.Color(34, 34, 34)) # dark website color 544 xfail = False 545 546 pos = QPoint(0, 0) 547 img = quteproc_new.get_screenshot(probe_pos=pos, probe_color=expected_color) 548 color = testutils.Color(img.pixelColor(pos)) 549 550 assert content == expected_text 551 assert color == expected_color 552 if xfail: 553 # We still do some checks, but we want to mark the test outcome as xfail. 554 pytest.xfail(xfail) 555 556 557@pytest.mark.qtwebkit_skip 558@pytest.mark.parametrize('reason', [ 559 'Explicitly enabled', 560 pytest.param('Qt 5.14', marks=testutils.qt514), 561 'Qt version changed', 562 None, 563]) 564def test_service_worker_workaround( 565 request, server, quteproc_new, short_tmpdir, reason): 566 """Make sure we remove the QtWebEngine Service Worker directory if configured.""" 567 args = _base_args(request.config) + ['--basedir', str(short_tmpdir)] 568 if reason == 'Explicitly enabled': 569 settings_args = ['-s', 'qt.workarounds.remove_service_workers', 'true'] 570 else: 571 settings_args = [] 572 573 service_worker_dir = short_tmpdir / 'data' / 'webengine' / 'Service Worker' 574 575 # First invocation: Create directory 576 quteproc_new.start(args) 577 quteproc_new.open_path('data/service-worker/index.html') 578 server.wait_for(verb='GET', path='/data/service-worker/data.json') 579 quteproc_new.send_cmd(':quit') 580 quteproc_new.wait_for_quit() 581 assert service_worker_dir.exists() 582 583 # Edit state file if needed 584 state_file = short_tmpdir / 'data' / 'state' 585 if reason == 'Qt 5.14': 586 state_file.remove() 587 elif reason == 'Qt version changed': 588 parser = configparser.ConfigParser() 589 parser.read(state_file) 590 del parser['general']['qt_version'] 591 with state_file.open('w', encoding='utf-8') as f: 592 parser.write(f) 593 594 # Second invocation: Directory gets removed (if workaround enabled) 595 quteproc_new.start(args + settings_args) 596 if reason is not None: 597 quteproc_new.wait_for( 598 message=(f'Removing service workers at {service_worker_dir} ' 599 f'(reason: {reason})')) 600 601 quteproc_new.send_cmd(':quit') 602 quteproc_new.wait_for_quit() 603 604 if reason is None: 605 assert service_worker_dir.exists() 606 quteproc_new.ensure_not_logged(message='Removing service workers at *') 607 else: 608 assert not service_worker_dir.exists() 609 610 611@testutils.qt513 # Qt 5.12 doesn't store cookies immediately 612@pytest.mark.parametrize('store', [True, False]) 613def test_cookies_store(quteproc_new, request, short_tmpdir, store): 614 # Start test process 615 args = _base_args(request.config) + [ 616 '--basedir', str(short_tmpdir), 617 '-s', 'content.cookies.store', str(store), 618 ] 619 quteproc_new.start(args) 620 621 # Set cookie and ensure it's set 622 quteproc_new.open_path('cookies/set-custom?max_age=30', wait=False) 623 quteproc_new.wait_for_load_finished('cookies') 624 content = quteproc_new.get_content() 625 data = json.loads(content) 626 assert data == {'cookies': {'cookie': 'value'}} 627 628 # Restart 629 quteproc_new.send_cmd(':quit') 630 quteproc_new.wait_for_quit() 631 quteproc_new.start(args) 632 633 # Check cookies 634 quteproc_new.open_path('cookies') 635 content = quteproc_new.get_content() 636 data = json.loads(content) 637 expected_cookies = {'cookie': 'value'} if store else {} 638 assert data == {'cookies': expected_cookies} 639 640 quteproc_new.send_cmd(':quit') 641 quteproc_new.wait_for_quit() 642 643 644# The 'colors' dictionaries in the parametrize decorator below have (QtWebEngine 645# version, CPU architecture) as keys. Either of those (or both) can be None to 646# say "on all other Qt versions" or "on all other CPU architectures". 647@pytest.mark.parametrize('filename, algorithm, colors', [ 648 ( 649 'blank', 650 'lightness-cielab', 651 { 652 ('5.15', None): testutils.Color(18, 18, 18), 653 ('5.15', 'aarch64'): testutils.Color(16, 16, 16), 654 ('5.14', None): testutils.Color(27, 27, 27), 655 ('5.14', 'aarch64'): testutils.Color(24, 24, 24), 656 (None, None): testutils.Color(0, 0, 0), 657 } 658 ), 659 ('blank', 'lightness-hsl', {(None, None): testutils.Color(0, 0, 0)}), 660 ('blank', 'brightness-rgb', {(None, None): testutils.Color(0, 0, 0)}), 661 662 ( 663 'yellow', 664 'lightness-cielab', 665 { 666 ('5.15', None): testutils.Color(35, 34, 0), 667 ('5.15', 'aarch64'): testutils.Color(33, 32, 0), 668 ('5.14', None): testutils.Color(35, 34, 0), 669 ('5.14', 'aarch64'): testutils.Color(33, 32, 0), 670 (None, None): testutils.Color(204, 204, 0), 671 } 672 ), 673 ( 674 'yellow', 675 'lightness-hsl', 676 { 677 (None, None): testutils.Color(204, 204, 0), 678 (None, 'aarch64'): testutils.Color(206, 207, 0), 679 }, 680 ), 681 ( 682 'yellow', 683 'brightness-rgb', 684 { 685 (None, None): testutils.Color(0, 0, 204), 686 (None, 'aarch64'): testutils.Color(0, 0, 206), 687 } 688 ), 689]) 690def test_dark_mode(webengine_versions, quteproc_new, request, 691 filename, algorithm, colors): 692 if not request.config.webengine: 693 pytest.skip("Skipped with QtWebKit") 694 695 args = _base_args(request.config) + [ 696 '--temp-basedir', 697 '-s', 'colors.webpage.darkmode.enabled', 'true', 698 '-s', 'colors.webpage.darkmode.algorithm', algorithm, 699 ] 700 quteproc_new.start(args) 701 702 ver = webengine_versions.webengine 703 minor_version = str(ver.strip_patch()) 704 705 arch = platform.machine() 706 for key in [ 707 (minor_version, arch), 708 (minor_version, None), 709 (None, arch), 710 (None, None), 711 ]: 712 if key in colors: 713 expected = colors[key] 714 break 715 716 quteproc_new.open_path(f'data/darkmode/{filename}.html') 717 718 # Position chosen by fair dice roll. 719 # https://xkcd.com/221/ 720 quteproc_new.get_screenshot( 721 probe_pos=QPoint(4, 4), 722 probe_color=expected, 723 ) 724 725 726def test_dark_mode_mathml(quteproc_new, request, qtbot): 727 if not request.config.webengine: 728 pytest.skip("Skipped with QtWebKit") 729 730 args = _base_args(request.config) + [ 731 '--temp-basedir', 732 '-s', 'colors.webpage.darkmode.enabled', 'true', 733 '-s', 'colors.webpage.darkmode.algorithm', 'brightness-rgb', 734 ] 735 quteproc_new.start(args) 736 737 quteproc_new.open_path('data/darkmode/mathml.html') 738 quteproc_new.wait_for_js('Image loaded') 739 740 # First make sure loading finished by looking outside of the image 741 expected = testutils.Color(0, 0, 206) if IS_ARM else testutils.Color(0, 0, 204) 742 743 quteproc_new.get_screenshot( 744 probe_pos=QPoint(105, 0), 745 probe_color=expected, 746 ) 747 748 # Then get the actual formula color, probing again in case it's not displayed yet... 749 quteproc_new.get_screenshot( 750 probe_pos=QPoint(4, 4), 751 probe_color=testutils.Color(255, 255, 255), 752 ) 753 754 755@testutils.qt514 756@pytest.mark.parametrize('value, preference', [ 757 ('true', 'Reduced motion'), 758 ('false', 'No'), 759]) 760@pytest.mark.skipif( 761 utils.is_windows, 762 reason="Outcome on Windows depends on system settings", 763) 764def test_prefers_reduced_motion(quteproc_new, request, value, preference): 765 if not request.config.webengine: 766 pytest.skip("Skipped with QtWebKit") 767 768 args = _base_args(request.config) + [ 769 '--temp-basedir', 770 '-s', 'content.prefers_reduced_motion', value, 771 ] 772 quteproc_new.start(args) 773 774 quteproc_new.open_path('data/prefers_reduced_motion.html') 775 content = quteproc_new.get_content() 776 assert content == f"{preference} preference detected." 777 778 779def test_unavailable_backend(request, quteproc_new): 780 """Test starting with a backend which isn't available. 781 782 If we use --qute-bdd-webengine, we test with QtWebKit here; otherwise we test with 783 QtWebEngine. If both are available, the test is skipped. 784 785 This ensures that we don't accidentally use backend-specific code before checking 786 that the chosen backend is actually available - i.e., that the error message is 787 properly printed, rather than an unhandled exception. 788 """ 789 qtwe_module = "PyQt5.QtWebEngineWidgets" 790 qtwk_module = "PyQt5.QtWebKitWidgets" 791 # Note we want to try the *opposite* backend here. 792 if request.config.webengine: 793 pytest.importorskip(qtwe_module) 794 module = qtwk_module 795 backend = 'webkit' 796 else: 797 pytest.importorskip(qtwk_module) 798 module = qtwe_module 799 backend = 'webengine' 800 801 try: 802 importlib.import_module(module) 803 except ImportError: 804 pass 805 else: 806 pytest.skip(f"{module} is available") 807 808 args = [ 809 '--debug', '--json-logging', '--no-err-windows', 810 '--backend', backend, 811 '--temp-basedir' 812 ] 813 quteproc_new.exit_expected = True 814 quteproc_new.start(args) 815 line = quteproc_new.wait_for( 816 message=('*qutebrowser tried to start with the Qt* backend but failed ' 817 'because * could not be imported.*')) 818 line.expected = True 819 820 821def test_json_logging_without_debug(request, quteproc_new, runtime_tmpdir): 822 args = _base_args(request.config) + ['--temp-basedir', ':quit'] 823 args.remove('--debug') 824 args.remove('about:blank') # interfers with :quit at the end 825 826 quteproc_new.exit_expected = True 827 quteproc_new.start(args, env={'XDG_RUNTIME_DIR': str(runtime_tmpdir)}) 828 assert not quteproc_new.is_running() 829