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