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