Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
SeleniumHQ
GitHub Repository: SeleniumHQ/Selenium
Path: blob/trunk/py/conftest.py
1856 views
1
# Licensed to the Software Freedom Conservancy (SFC) under one
2
# or more contributor license agreements. See the NOTICE file
3
# distributed with this work for additional information
4
# regarding copyright ownership. The SFC licenses this file
5
# to you under the Apache License, Version 2.0 (the
6
# "License"); you may not use this file except in compliance
7
# with the License. You may obtain a copy of the License at
8
#
9
# http://www.apache.org/licenses/LICENSE-2.0
10
#
11
# Unless required by applicable law or agreed to in writing,
12
# software distributed under the License is distributed on an
13
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
# KIND, either express or implied. See the License for the
15
# specific language governing permissions and limitations
16
# under the License.
17
18
import os
19
import sys
20
from dataclasses import dataclass
21
from pathlib import Path
22
23
import pytest
24
25
from selenium import webdriver
26
from selenium.webdriver.remote.server import Server
27
from test.selenium.webdriver.common.network import get_lan_ip
28
from test.selenium.webdriver.common.webserver import SimpleWebServer
29
30
drivers = (
31
"chrome",
32
"edge",
33
"firefox",
34
"ie",
35
"remote",
36
"safari",
37
"webkitgtk",
38
"wpewebkit",
39
)
40
41
42
def pytest_addoption(parser):
43
parser.addoption(
44
"--driver",
45
action="append",
46
choices=drivers,
47
dest="drivers",
48
metavar="DRIVER",
49
help="Driver to run tests against ({})".format(", ".join(drivers)),
50
)
51
parser.addoption(
52
"--browser-binary",
53
action="store",
54
dest="binary",
55
help="Location of the browser binary",
56
)
57
parser.addoption(
58
"--driver-binary",
59
action="store",
60
dest="executable",
61
help="Location of the service executable binary",
62
)
63
parser.addoption(
64
"--browser-args",
65
action="store",
66
dest="args",
67
help="Arguments to start the browser with",
68
)
69
parser.addoption(
70
"--headless",
71
action="store_true",
72
dest="headless",
73
help="Run tests in headless mode",
74
)
75
parser.addoption(
76
"--use-lan-ip",
77
action="store_true",
78
dest="use_lan_ip",
79
help="Start test server with lan ip instead of localhost",
80
)
81
parser.addoption(
82
"--bidi",
83
action="store_true",
84
dest="bidi",
85
help="Enable BiDi support",
86
)
87
88
89
def pytest_ignore_collect(collection_path, config):
90
drivers_opt = config.getoption("drivers")
91
_drivers = set(drivers).difference(drivers_opt or drivers)
92
if drivers_opt:
93
_drivers.add("unit")
94
if len([d for d in _drivers if d.lower() in collection_path.parts]) > 0:
95
return True
96
return None
97
98
99
def pytest_generate_tests(metafunc):
100
if "driver" in metafunc.fixturenames and metafunc.config.option.drivers:
101
metafunc.parametrize("driver", metafunc.config.option.drivers, indirect=True)
102
103
104
driver_instance = None
105
selenium_driver = None
106
107
108
class ContainerProtocol:
109
def __contains__(self, name):
110
if name.lower() in self.__dict__:
111
return True
112
return False
113
114
115
@dataclass
116
class SupportedDrivers(ContainerProtocol):
117
chrome: str = "Chrome"
118
firefox: str = "Firefox"
119
safari: str = "Safari"
120
edge: str = "Edge"
121
ie: str = "Ie"
122
webkitgtk: str = "WebKitGTK"
123
wpewebkit: str = "WPEWebKit"
124
remote: str = "Remote"
125
126
127
@dataclass
128
class SupportedOptions(ContainerProtocol):
129
chrome: str = "ChromeOptions"
130
firefox: str = "FirefoxOptions"
131
edge: str = "EdgeOptions"
132
safari: str = "SafariOptions"
133
ie: str = "IeOptions"
134
remote: str = "FirefoxOptions"
135
webkitgtk: str = "WebKitGTKOptions"
136
wpewebkit: str = "WPEWebKitOptions"
137
138
139
@dataclass
140
class SupportedBidiDrivers(ContainerProtocol):
141
chrome: str = "Chrome"
142
firefox: str = "Firefox"
143
edge: str = "Edge"
144
remote: str = "Remote"
145
146
147
class Driver:
148
def __init__(self, driver_class, request):
149
self.driver_class = driver_class
150
self._request = request
151
self._driver = None
152
self._service = None
153
self.options = driver_class
154
self.headless = driver_class
155
self.bidi = driver_class
156
157
@classmethod
158
def clean_options(cls, driver_class, request):
159
return cls(driver_class, request).options
160
161
@property
162
def supported_drivers(self):
163
return SupportedDrivers()
164
165
@property
166
def supported_options(self):
167
return SupportedOptions()
168
169
@property
170
def supported_bidi_drivers(self):
171
return SupportedBidiDrivers()
172
173
@property
174
def driver_class(self):
175
return self._driver_class
176
177
@driver_class.setter
178
def driver_class(self, cls_name):
179
if cls_name.lower() not in self.supported_drivers:
180
raise AttributeError(f"Invalid driver class {cls_name.lower()}")
181
self._driver_class = getattr(self.supported_drivers, cls_name.lower())
182
183
@property
184
def exe_platform(self):
185
if sys.platform == "win32":
186
return "Windows"
187
elif sys.platform == "darwin":
188
return "Darwin"
189
elif sys.platform == "linux":
190
return "Linux"
191
else:
192
return sys.platform.title()
193
194
@property
195
def browser_path(self):
196
if self._request.config.option.binary:
197
return self._request.config.option.binary
198
return None
199
200
@property
201
def browser_args(self):
202
if self._request.config.option.args:
203
return self._request.config.option.args
204
return None
205
206
@property
207
def driver_path(self):
208
if self._request.config.option.executable:
209
return self._request.config.option.executable
210
return None
211
212
@property
213
def headless(self):
214
return self._headless
215
216
@headless.setter
217
def headless(self, cls_name):
218
self._headless = self._request.config.option.headless
219
if self._headless:
220
if cls_name.lower() == "chrome" or cls_name.lower() == "edge":
221
self._options.add_argument("--headless")
222
if cls_name.lower() == "firefox":
223
self._options.add_argument("-headless")
224
225
@property
226
def bidi(self):
227
return self._bidi
228
229
@bidi.setter
230
def bidi(self, cls_name):
231
self._bidi = self._request.config.option.bidi
232
if self._bidi:
233
self._options.web_socket_url = True
234
self._options.unhandled_prompt_behavior = "ignore"
235
236
@property
237
def options(self):
238
return self._options
239
240
@options.setter
241
def options(self, cls_name):
242
if cls_name.lower() not in self.supported_options:
243
raise AttributeError(f"Invalid Options class {cls_name.lower()}")
244
245
if self.driver_class == self.supported_drivers.firefox:
246
self._options = getattr(webdriver, self.supported_options.firefox)()
247
if self.exe_platform == "Linux":
248
# There are issues with window size/position when running Firefox
249
# under Wayland, so we use XWayland instead.
250
os.environ["MOZ_ENABLE_WAYLAND"] = "0"
251
elif self.driver_class == self.supported_drivers.remote:
252
self._options = getattr(webdriver, self.supported_options.firefox)()
253
self._options.set_capability("moz:firefoxOptions", {})
254
self._options.enable_downloads = True
255
else:
256
opts_cls = getattr(self.supported_options, cls_name.lower())
257
self._options = getattr(webdriver, opts_cls)()
258
259
if self.browser_path or self.browser_args:
260
if self.driver_class == self.supported_drivers.webkitgtk:
261
self._options.overlay_scrollbars_enabled = False
262
if self.browser_path is not None:
263
self._options.binary_location = self.browser_path.strip("'")
264
if self.browser_args is not None:
265
for arg in self.browser_args.split():
266
self._options.add_argument(arg)
267
268
@property
269
def service(self):
270
executable = self.driver_path
271
if executable:
272
module = getattr(webdriver, self.driver_class.lower())
273
self._service = module.service.Service(executable_path=executable)
274
return self._service
275
return None
276
277
@property
278
def driver(self):
279
self._driver = self._initialize_driver()
280
return self._driver
281
282
@property
283
def is_platform_valid(self):
284
if self.driver_class.lower() == "safari" and self.exe_platform != "Darwin":
285
return False
286
if self.driver_class.lower() == "ie" and self.exe_platform != "Windows":
287
return False
288
if "webkit" in self.driver_class.lower() and self.exe_platform == "Windows":
289
return False
290
return True
291
292
def _initialize_driver(self):
293
kwargs = {}
294
if self.options is not None:
295
kwargs["options"] = self.options
296
if self.driver_path is not None:
297
kwargs["service"] = self.service
298
return getattr(webdriver, self.driver_class)(**kwargs)
299
300
@property
301
def stop_driver(self):
302
def fin():
303
global driver_instance
304
if self._driver is not None:
305
self._driver.quit()
306
self._driver = None
307
driver_instance = None
308
309
return fin
310
311
312
@pytest.fixture(scope="function")
313
def driver(request):
314
global driver_instance
315
global selenium_driver
316
driver_class = getattr(request, "param", "Chrome").lower()
317
318
if selenium_driver is None:
319
selenium_driver = Driver(driver_class, request)
320
321
# skip tests if not available on the platform
322
if not selenium_driver.is_platform_valid:
323
pytest.skip(f"{driver_class} tests can only run on {selenium_driver.exe_platform}")
324
325
# skip tests in the 'remote' directory if run with a local driver
326
if request.node.path.parts[-2] == "remote" and selenium_driver.driver_class != "Remote":
327
pytest.skip(f"Remote tests can't be run with driver '{selenium_driver.driver_class}'")
328
329
# skip tests for drivers that don't support BiDi when --bidi is enabled
330
if selenium_driver.bidi:
331
if driver_class.lower() not in selenium_driver.supported_bidi_drivers:
332
pytest.skip(f"{driver_class} does not support BiDi")
333
334
# conditionally mark tests as expected to fail based on driver
335
marker = request.node.get_closest_marker(f"xfail_{driver_class.lower()}")
336
if marker is not None:
337
if "run" in marker.kwargs:
338
if marker.kwargs["run"] is False:
339
pytest.skip()
340
yield
341
return
342
if "raises" in marker.kwargs:
343
marker.kwargs.pop("raises")
344
pytest.xfail(**marker.kwargs)
345
346
request.addfinalizer(selenium_driver.stop_driver)
347
348
if driver_instance is None:
349
driver_instance = selenium_driver.driver
350
351
yield driver_instance
352
# Close the browser after BiDi tests. Those make event subscriptions
353
# and doesn't seems to be stable enough, causing the flakiness of the
354
# subsequent tests.
355
# Remove this when BiDi implementation and API is stable.
356
if selenium_driver.bidi:
357
request.addfinalizer(selenium_driver.stop_driver)
358
359
if request.node.get_closest_marker("no_driver_after_test"):
360
driver_instance = None
361
362
363
@pytest.fixture(scope="session", autouse=True)
364
def stop_driver(request):
365
def fin():
366
global driver_instance
367
if driver_instance is not None:
368
driver_instance.quit()
369
driver_instance = None
370
371
request.addfinalizer(fin)
372
373
374
def pytest_exception_interact(node, call, report):
375
if report.failed:
376
global driver_instance
377
if driver_instance is not None:
378
driver_instance.quit()
379
driver_instance = None
380
381
382
@pytest.fixture
383
def pages(driver, webserver):
384
class Pages:
385
def url(self, name, localhost=False):
386
return webserver.where_is(name, localhost)
387
388
def load(self, name):
389
driver.get(self.url(name))
390
391
return Pages()
392
393
394
@pytest.fixture(autouse=True, scope="session")
395
def server(request):
396
drivers = request.config.getoption("drivers")
397
if drivers is None or "remote" not in drivers:
398
yield None
399
return
400
401
jar_path = os.path.join(
402
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
403
"java/src/org/openqa/selenium/grid/selenium_server_deploy.jar",
404
)
405
406
remote_env = os.environ.copy()
407
if sys.platform == "linux":
408
# There are issues with window size/position when running Firefox
409
# under Wayland, so we use XWayland instead.
410
remote_env["MOZ_ENABLE_WAYLAND"] = "0"
411
412
if Path(jar_path).exists():
413
# use the grid server built by bazel
414
server = Server(path=jar_path, env=remote_env)
415
else:
416
# use the local grid server (downloads a new one if needed)
417
server = Server(env=remote_env)
418
server.start()
419
yield server
420
server.stop()
421
422
423
@pytest.fixture(autouse=True, scope="session")
424
def webserver(request):
425
host = get_lan_ip() if request.config.getoption("use_lan_ip") else None
426
427
webserver = SimpleWebServer(host=host)
428
webserver.start()
429
yield webserver
430
webserver.stop()
431
432
433
@pytest.fixture
434
def edge_service():
435
from selenium.webdriver.edge.service import Service as EdgeService
436
437
return EdgeService
438
439
440
@pytest.fixture(scope="function")
441
def driver_executable(request):
442
return request.config.option.executable
443
444
445
@pytest.fixture(scope="function")
446
def clean_driver(request):
447
_supported_drivers = SupportedDrivers()
448
try:
449
driver_class = getattr(_supported_drivers, request.config.option.drivers[0].lower())
450
except (AttributeError, TypeError):
451
raise Exception("This test requires a --driver to be specified.")
452
driver_reference = getattr(webdriver, driver_class)
453
454
# conditionally mark tests as expected to fail based on driver
455
marker = request.node.get_closest_marker(f"xfail_{driver_class.lower()}")
456
if marker is not None:
457
if "run" in marker.kwargs:
458
if marker.kwargs["run"] is False:
459
pytest.skip()
460
yield
461
return
462
if "raises" in marker.kwargs:
463
marker.kwargs.pop("raises")
464
pytest.xfail(**marker.kwargs)
465
466
yield driver_reference
467
if request.node.get_closest_marker("no_driver_after_test"):
468
driver_reference = None
469
470
471
@pytest.fixture(scope="function")
472
def clean_service(request):
473
driver_class = request.config.option.drivers[0].lower()
474
selenium_driver = Driver(driver_class, request)
475
yield selenium_driver.service
476
477
478
@pytest.fixture(scope="function")
479
def clean_options(request):
480
driver_class = request.config.option.drivers[0].lower()
481
yield Driver.clean_options(driver_class, request)
482
483
484
@pytest.fixture
485
def firefox_options(request):
486
_supported_drivers = SupportedDrivers()
487
try:
488
driver_class = request.config.option.drivers[0].lower()
489
except (AttributeError, TypeError):
490
raise Exception("This test requires a --driver to be specified")
491
492
# skip tests in the 'remote' directory if run with a local driver
493
if request.node.path.parts[-2] == "remote" and getattr(_supported_drivers, driver_class) != "Remote":
494
pytest.skip(f"Remote tests can't be run with driver '{driver_class}'")
495
496
options = Driver.clean_options("firefox", request)
497
498
return options
499
500
501
@pytest.fixture
502
def chromium_options(request):
503
_supported_drivers = SupportedDrivers()
504
try:
505
driver_class = request.config.option.drivers[0].lower()
506
except (AttributeError, TypeError):
507
raise Exception("This test requires a --driver to be specified")
508
509
# Skip if not Chrome or Edge
510
if driver_class not in ("chrome", "edge"):
511
pytest.skip(f"This test requires Chrome or Edge, got {driver_class}")
512
513
# skip tests in the 'remote' directory if run with a local driver
514
if request.node.path.parts[-2] == "remote" and getattr(_supported_drivers, driver_class) != "Remote":
515
pytest.skip(f"Remote tests can't be run with driver '{driver_class}'")
516
517
if driver_class in ("chrome", "edge"):
518
options = Driver.clean_options(driver_class, request)
519
520
return options
521
522