• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import os
2import re
3import shlex
4import subprocess
5import sys
6import unittest
7import webbrowser
8from test import support
9from test.support import import_helper
10from test.support import is_apple_mobile
11from test.support import os_helper
12from test.support import requires_subprocess
13from test.support import threading_helper
14from unittest import mock
15
16# The webbrowser module uses threading locks
17threading_helper.requires_working_threading(module=True)
18
19URL = 'https://www.example.com'
20CMD_NAME = 'test'
21
22
23class PopenMock(mock.MagicMock):
24
25    def poll(self):
26        return 0
27
28    def wait(self, seconds=None):
29        return 0
30
31
32@requires_subprocess()
33class CommandTestMixin:
34
35    def _test(self, meth, *, args=[URL], kw={}, options, arguments):
36        """Given a web browser instance method name along with arguments and
37        keywords for same (which defaults to the single argument URL), creates
38        a browser instance from the class pointed to by self.browser, calls the
39        indicated instance method with the indicated arguments, and compares
40        the resulting options and arguments passed to Popen by the browser
41        instance against the 'options' and 'args' lists.  Options are compared
42        in a position independent fashion, and the arguments are compared in
43        sequence order to whatever is left over after removing the options.
44
45        """
46        popen = PopenMock()
47        support.patch(self, subprocess, 'Popen', popen)
48        browser = self.browser_class(name=CMD_NAME)
49        getattr(browser, meth)(*args, **kw)
50        popen_args = subprocess.Popen.call_args[0][0]
51        self.assertEqual(popen_args[0], CMD_NAME)
52        popen_args.pop(0)
53        for option in options:
54            self.assertIn(option, popen_args)
55            popen_args.pop(popen_args.index(option))
56        self.assertEqual(popen_args, arguments)
57
58
59class GenericBrowserCommandTest(CommandTestMixin, unittest.TestCase):
60
61    browser_class = webbrowser.GenericBrowser
62
63    def test_open(self):
64        self._test('open',
65                   options=[],
66                   arguments=[URL])
67
68
69class BackgroundBrowserCommandTest(CommandTestMixin, unittest.TestCase):
70
71    browser_class = webbrowser.BackgroundBrowser
72
73    def test_open(self):
74        self._test('open',
75                   options=[],
76                   arguments=[URL])
77
78
79class ChromeCommandTest(CommandTestMixin, unittest.TestCase):
80
81    browser_class = webbrowser.Chrome
82
83    def test_open(self):
84        self._test('open',
85                   options=[],
86                   arguments=[URL])
87
88    def test_open_with_autoraise_false(self):
89        self._test('open', kw=dict(autoraise=False),
90                   options=[],
91                   arguments=[URL])
92
93    def test_open_new(self):
94        self._test('open_new',
95                   options=['--new-window'],
96                   arguments=[URL])
97
98    def test_open_new_tab(self):
99        self._test('open_new_tab',
100                   options=[],
101                   arguments=[URL])
102
103    def test_open_bad_new_parameter(self):
104        with self.assertRaisesRegex(webbrowser.Error,
105                                    re.escape("Bad 'new' parameter to open(); "
106                                              "expected 0, 1, or 2, got 999")):
107            self._test('open',
108                       options=[],
109                       arguments=[URL],
110                       kw=dict(new=999))
111
112
113class EdgeCommandTest(CommandTestMixin, unittest.TestCase):
114
115    browser_class = webbrowser.Edge
116
117    def test_open(self):
118        self._test('open',
119                   options=[],
120                   arguments=[URL])
121
122    def test_open_with_autoraise_false(self):
123        self._test('open', kw=dict(autoraise=False),
124                   options=[],
125                   arguments=[URL])
126
127    def test_open_new(self):
128        self._test('open_new',
129                   options=['--new-window'],
130                   arguments=[URL])
131
132    def test_open_new_tab(self):
133        self._test('open_new_tab',
134                   options=[],
135                   arguments=[URL])
136
137
138class MozillaCommandTest(CommandTestMixin, unittest.TestCase):
139
140    browser_class = webbrowser.Mozilla
141
142    def test_open(self):
143        self._test('open',
144                   options=[],
145                   arguments=[URL])
146
147    def test_open_with_autoraise_false(self):
148        self._test('open', kw=dict(autoraise=False),
149                   options=[],
150                   arguments=[URL])
151
152    def test_open_new(self):
153        self._test('open_new',
154                   options=[],
155                   arguments=['-new-window', URL])
156
157    def test_open_new_tab(self):
158        self._test('open_new_tab',
159                   options=[],
160                   arguments=['-new-tab', URL])
161
162
163class EpiphanyCommandTest(CommandTestMixin, unittest.TestCase):
164
165    browser_class = webbrowser.Epiphany
166
167    def test_open(self):
168        self._test('open',
169                   options=['-n'],
170                   arguments=[URL])
171
172    def test_open_with_autoraise_false(self):
173        self._test('open', kw=dict(autoraise=False),
174                   options=['-noraise', '-n'],
175                   arguments=[URL])
176
177    def test_open_new(self):
178        self._test('open_new',
179                   options=['-w'],
180                   arguments=[URL])
181
182    def test_open_new_tab(self):
183        self._test('open_new_tab',
184                   options=['-w'],
185                   arguments=[URL])
186
187
188class OperaCommandTest(CommandTestMixin, unittest.TestCase):
189
190    browser_class = webbrowser.Opera
191
192    def test_open(self):
193        self._test('open',
194                   options=[],
195                   arguments=[URL])
196
197    def test_open_with_autoraise_false(self):
198        self._test('open', kw=dict(autoraise=False),
199                   options=[],
200                   arguments=[URL])
201
202    def test_open_new(self):
203        self._test('open_new',
204                   options=['--new-window'],
205                   arguments=[URL])
206
207    def test_open_new_tab(self):
208        self._test('open_new_tab',
209                   options=[],
210                   arguments=[URL])
211
212
213class ELinksCommandTest(CommandTestMixin, unittest.TestCase):
214
215    browser_class = webbrowser.Elinks
216
217    def test_open(self):
218        self._test('open', options=['-remote'],
219                   arguments=[f'openURL({URL})'])
220
221    def test_open_with_autoraise_false(self):
222        self._test('open',
223                   options=['-remote'],
224                   arguments=[f'openURL({URL})'])
225
226    def test_open_new(self):
227        self._test('open_new',
228                   options=['-remote'],
229                   arguments=[f'openURL({URL},new-window)'])
230
231    def test_open_new_tab(self):
232        self._test('open_new_tab',
233                   options=['-remote'],
234                   arguments=[f'openURL({URL},new-tab)'])
235
236
237@unittest.skipUnless(sys.platform == "ios", "Test only applicable to iOS")
238class IOSBrowserTest(unittest.TestCase):
239    def _obj_ref(self, *args):
240        # Construct a string representation of the arguments that can be used
241        # as a proxy for object instance references
242        return "|".join(str(a) for a in args)
243
244    @unittest.skipIf(getattr(webbrowser, "objc", None) is None,
245                     "iOS Webbrowser tests require ctypes")
246    def setUp(self):
247        # Intercept the objc library. Wrap the calls to get the
248        # references to classes and selectors to return strings, and
249        # wrap msgSend to return stringified object references
250        self.orig_objc = webbrowser.objc
251
252        webbrowser.objc = mock.Mock()
253        webbrowser.objc.objc_getClass = lambda cls: f"C#{cls.decode()}"
254        webbrowser.objc.sel_registerName = lambda sel: f"S#{sel.decode()}"
255        webbrowser.objc.objc_msgSend.side_effect = self._obj_ref
256
257    def tearDown(self):
258        webbrowser.objc = self.orig_objc
259
260    def _test(self, meth, **kwargs):
261        # The browser always gets focus, there's no concept of separate browser
262        # windows, and there's no API-level control over creating a new tab.
263        # Therefore, all calls to webbrowser are effectively the same.
264        getattr(webbrowser, meth)(URL, **kwargs)
265
266        # The ObjC String version of the URL is created with UTF-8 encoding
267        url_string_args = [
268            "C#NSString",
269            "S#stringWithCString:encoding:",
270            b'https://www.example.com',
271            4,
272        ]
273        # The NSURL version of the URL is created from that string
274        url_obj_args = [
275            "C#NSURL",
276            "S#URLWithString:",
277            self._obj_ref(*url_string_args),
278        ]
279        # The openURL call is invoked on the shared application
280        shared_app_args = ["C#UIApplication", "S#sharedApplication"]
281
282        # Verify that the last call is the one that opens the URL.
283        webbrowser.objc.objc_msgSend.assert_called_with(
284            self._obj_ref(*shared_app_args),
285            "S#openURL:options:completionHandler:",
286            self._obj_ref(*url_obj_args),
287            None,
288            None
289        )
290
291    def test_open(self):
292        self._test('open')
293
294    def test_open_with_autoraise_false(self):
295        self._test('open', autoraise=False)
296
297    def test_open_new(self):
298        self._test('open_new')
299
300    def test_open_new_tab(self):
301        self._test('open_new_tab')
302
303
304class BrowserRegistrationTest(unittest.TestCase):
305
306    def setUp(self):
307        # Ensure we don't alter the real registered browser details
308        self._saved_tryorder = webbrowser._tryorder
309        webbrowser._tryorder = []
310        self._saved_browsers = webbrowser._browsers
311        webbrowser._browsers = {}
312
313    def tearDown(self):
314        webbrowser._tryorder = self._saved_tryorder
315        webbrowser._browsers = self._saved_browsers
316
317    def _check_registration(self, preferred):
318        class ExampleBrowser:
319            pass
320
321        expected_tryorder = []
322        expected_browsers = {}
323
324        self.assertEqual(webbrowser._tryorder, expected_tryorder)
325        self.assertEqual(webbrowser._browsers, expected_browsers)
326
327        webbrowser.register('Example1', ExampleBrowser)
328        expected_tryorder = ['Example1']
329        expected_browsers['example1'] = [ExampleBrowser, None]
330        self.assertEqual(webbrowser._tryorder, expected_tryorder)
331        self.assertEqual(webbrowser._browsers, expected_browsers)
332
333        instance = ExampleBrowser()
334        if preferred is not None:
335            webbrowser.register('example2', ExampleBrowser, instance,
336                                preferred=preferred)
337        else:
338            webbrowser.register('example2', ExampleBrowser, instance)
339        if preferred:
340            expected_tryorder = ['example2', 'Example1']
341        else:
342            expected_tryorder = ['Example1', 'example2']
343        expected_browsers['example2'] = [ExampleBrowser, instance]
344        self.assertEqual(webbrowser._tryorder, expected_tryorder)
345        self.assertEqual(webbrowser._browsers, expected_browsers)
346
347    def test_register(self):
348        self._check_registration(preferred=False)
349
350    def test_register_default(self):
351        self._check_registration(preferred=None)
352
353    def test_register_preferred(self):
354        self._check_registration(preferred=True)
355
356    @unittest.skipUnless(sys.platform == "darwin", "macOS specific test")
357    def test_no_xdg_settings_on_macOS(self):
358        # On macOS webbrowser should not use xdg-settings to
359        # look for X11 based browsers (for those users with
360        # XQuartz installed)
361        with mock.patch("subprocess.check_output") as ck_o:
362            webbrowser.register_standard_browsers()
363
364        ck_o.assert_not_called()
365
366
367class ImportTest(unittest.TestCase):
368    def test_register(self):
369        webbrowser = import_helper.import_fresh_module('webbrowser')
370        self.assertIsNone(webbrowser._tryorder)
371        self.assertFalse(webbrowser._browsers)
372
373        class ExampleBrowser:
374            pass
375        webbrowser.register('Example1', ExampleBrowser)
376        self.assertTrue(webbrowser._tryorder)
377        self.assertEqual(webbrowser._tryorder[-1], 'Example1')
378        self.assertTrue(webbrowser._browsers)
379        self.assertIn('example1', webbrowser._browsers)
380        self.assertEqual(webbrowser._browsers['example1'], [ExampleBrowser, None])
381
382    def test_get(self):
383        webbrowser = import_helper.import_fresh_module('webbrowser')
384        self.assertIsNone(webbrowser._tryorder)
385        self.assertFalse(webbrowser._browsers)
386
387        with self.assertRaises(webbrowser.Error):
388            webbrowser.get('fakebrowser')
389        self.assertIsNotNone(webbrowser._tryorder)
390
391    @unittest.skipIf(" " in sys.executable, "test assumes no space in path (GH-114452)")
392    def test_synthesize(self):
393        webbrowser = import_helper.import_fresh_module('webbrowser')
394        name = os.path.basename(sys.executable).lower()
395        webbrowser.register(name, None, webbrowser.GenericBrowser(name))
396        webbrowser.get(sys.executable)
397
398    @unittest.skipIf(
399        is_apple_mobile,
400        "Apple mobile doesn't allow modifying browser with environment"
401    )
402    def test_environment(self):
403        webbrowser = import_helper.import_fresh_module('webbrowser')
404        try:
405            browser = webbrowser.get().name
406        except webbrowser.Error as err:
407            self.skipTest(str(err))
408        with os_helper.EnvironmentVarGuard() as env:
409            env["BROWSER"] = browser
410            webbrowser = import_helper.import_fresh_module('webbrowser')
411            webbrowser.get()
412
413    @unittest.skipIf(
414        is_apple_mobile,
415        "Apple mobile doesn't allow modifying browser with environment"
416    )
417    def test_environment_preferred(self):
418        webbrowser = import_helper.import_fresh_module('webbrowser')
419        try:
420            webbrowser.get()
421            least_preferred_browser = webbrowser.get(webbrowser._tryorder[-1]).name
422        except (webbrowser.Error, IndexError) as err:
423            self.skipTest(str(err))
424
425        with os_helper.EnvironmentVarGuard() as env:
426            env["BROWSER"] = least_preferred_browser
427            webbrowser = import_helper.import_fresh_module('webbrowser')
428            self.assertEqual(webbrowser.get().name, least_preferred_browser)
429
430        with os_helper.EnvironmentVarGuard() as env:
431            env["BROWSER"] = sys.executable
432            webbrowser = import_helper.import_fresh_module('webbrowser')
433            self.assertEqual(webbrowser.get().name, sys.executable)
434
435
436class CliTest(unittest.TestCase):
437    def test_parse_args(self):
438        for command, url, new_win in [
439            # No optional arguments
440            ("https://example.com", "https://example.com", 0),
441            # Each optional argument
442            ("https://example.com -n", "https://example.com", 1),
443            ("-n https://example.com", "https://example.com", 1),
444            ("https://example.com -t", "https://example.com", 2),
445            ("-t https://example.com", "https://example.com", 2),
446            # Long form
447            ("https://example.com --new-window", "https://example.com", 1),
448            ("--new-window https://example.com", "https://example.com", 1),
449            ("https://example.com --new-tab", "https://example.com", 2),
450            ("--new-tab https://example.com", "https://example.com", 2),
451        ]:
452            args = webbrowser.parse_args(shlex.split(command))
453
454            self.assertEqual(args.url, url)
455            self.assertEqual(args.new_win, new_win)
456
457    def test_parse_args_error(self):
458        for command in [
459            # Arguments must not both be given
460            "https://example.com -n -t",
461            "https://example.com --new-window --new-tab",
462            "https://example.com -n --new-tab",
463            "https://example.com --new-window -t",
464        ]:
465            with support.captured_stderr() as stderr:
466                with self.assertRaises(SystemExit):
467                    webbrowser.parse_args(shlex.split(command))
468                self.assertIn(
469                    'error: argument -t/--new-tab: not allowed with argument -n/--new-window',
470                    stderr.getvalue(),
471                )
472
473        # Ensure ambiguous shortening fails
474        with support.captured_stderr() as stderr:
475            with self.assertRaises(SystemExit):
476                webbrowser.parse_args(shlex.split("https://example.com --new"))
477            self.assertIn(
478                'error: ambiguous option: --new could match --new-window, --new-tab',
479                stderr.getvalue()
480            )
481
482    def test_main(self):
483        for command, expected_url, expected_new_win in [
484            # No optional arguments
485            ("https://example.com", "https://example.com", 0),
486            # Each optional argument
487            ("https://example.com -n", "https://example.com", 1),
488            ("-n https://example.com", "https://example.com", 1),
489            ("https://example.com -t", "https://example.com", 2),
490            ("-t https://example.com", "https://example.com", 2),
491            # Long form
492            ("https://example.com --new-window", "https://example.com", 1),
493            ("--new-window https://example.com", "https://example.com", 1),
494            ("https://example.com --new-tab", "https://example.com", 2),
495            ("--new-tab https://example.com", "https://example.com", 2),
496        ]:
497            with (
498                mock.patch("webbrowser.open", return_value=None) as mock_open,
499                mock.patch("builtins.print", return_value=None),
500            ):
501                webbrowser.main(shlex.split(command))
502                mock_open.assert_called_once_with(expected_url, expected_new_win)
503
504
505if __name__ == '__main__':
506    unittest.main()
507