Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/py/conftest.py
2864 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 platform
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
return platform.system()
186
187
@property
188
def browser_path(self):
189
if self._request.config.option.binary:
190
return self._request.config.option.binary
191
return None
192
193
@property
194
def browser_args(self):
195
if self._request.config.option.args:
196
return self._request.config.option.args
197
return None
198
199
@property
200
def driver_path(self):
201
if self._request.config.option.executable:
202
return self._request.config.option.executable
203
return None
204
205
@property
206
def headless(self):
207
return self._headless
208
209
@headless.setter
210
def headless(self, cls_name):
211
self._headless = self._request.config.option.headless
212
if self._headless:
213
if cls_name.lower() == "chrome" or cls_name.lower() == "edge":
214
self._options.add_argument("--headless")
215
if cls_name.lower() == "firefox":
216
self._options.add_argument("-headless")
217
218
@property
219
def bidi(self):
220
return self._bidi
221
222
@bidi.setter
223
def bidi(self, cls_name):
224
self._bidi = self._request.config.option.bidi
225
if self._bidi:
226
self._options.web_socket_url = True
227
self._options.unhandled_prompt_behavior = "ignore"
228
229
@property
230
def options(self):
231
return self._options
232
233
@options.setter
234
def options(self, cls_name):
235
if cls_name.lower() not in self.supported_options:
236
raise AttributeError(f"Invalid Options class {cls_name.lower()}")
237
238
if self.driver_class == self.supported_drivers.firefox:
239
self._options = getattr(webdriver, self.supported_options.firefox)()
240
if self.exe_platform == "Linux":
241
# There are issues with window size/position when running Firefox
242
# under Wayland, so we use XWayland instead.
243
os.environ["MOZ_ENABLE_WAYLAND"] = "0"
244
elif self.driver_class == self.supported_drivers.remote:
245
self._options = getattr(webdriver, self.supported_options.firefox)()
246
self._options.set_capability("moz:firefoxOptions", {})
247
self._options.enable_downloads = True
248
else:
249
opts_cls = getattr(self.supported_options, cls_name.lower())
250
self._options = getattr(webdriver, opts_cls)()
251
252
if self.browser_path or self.browser_args:
253
if self.driver_class == self.supported_drivers.webkitgtk:
254
self._options.overlay_scrollbars_enabled = False
255
if self.browser_path is not None:
256
self._options.binary_location = self.browser_path.strip("'")
257
if self.browser_args is not None:
258
for arg in self.browser_args.split():
259
self._options.add_argument(arg)
260
261
@property
262
def service(self):
263
executable = self.driver_path
264
if executable:
265
module = getattr(webdriver, self.driver_class.lower())
266
self._service = module.service.Service(executable_path=executable)
267
return self._service
268
return None
269
270
@property
271
def driver(self):
272
self._driver = self._initialize_driver()
273
return self._driver
274
275
@property
276
def is_platform_valid(self):
277
if self.driver_class.lower() == "safari" and self.exe_platform != "Darwin":
278
return False
279
if self.driver_class.lower() == "ie" and self.exe_platform != "Windows":
280
return False
281
if "webkit" in self.driver_class.lower() and self.exe_platform == "Windows":
282
return False
283
return True
284
285
def _initialize_driver(self):
286
kwargs = {}
287
if self.options is not None:
288
kwargs["options"] = self.options
289
if self.driver_path is not None:
290
kwargs["service"] = self.service
291
return getattr(webdriver, self.driver_class)(**kwargs)
292
293
@property
294
def stop_driver(self):
295
def fin():
296
global driver_instance
297
if self._driver is not None:
298
self._driver.quit()
299
self._driver = None
300
driver_instance = None
301
302
return fin
303
304
305
@pytest.fixture(scope="function")
306
def driver(request):
307
global driver_instance
308
global selenium_driver
309
driver_class = getattr(request, "param", "Chrome").lower()
310
311
if selenium_driver is None:
312
selenium_driver = Driver(driver_class, request)
313
314
# skip tests if not available on the platform
315
if not selenium_driver.is_platform_valid:
316
pytest.skip(f"{driver_class} tests can only run on {selenium_driver.exe_platform}")
317
318
# skip tests in the 'remote' directory if run with a local driver
319
if request.node.path.parts[-2] == "remote" and selenium_driver.driver_class != "Remote":
320
pytest.skip(f"Remote tests can't be run with driver '{selenium_driver.driver_class}'")
321
322
# skip tests for drivers that don't support BiDi when --bidi is enabled
323
if selenium_driver.bidi:
324
if driver_class.lower() not in selenium_driver.supported_bidi_drivers:
325
pytest.skip(f"{driver_class} does not support BiDi")
326
327
# conditionally mark tests as expected to fail based on driver
328
marker = request.node.get_closest_marker(f"xfail_{driver_class.lower()}")
329
if marker is not None:
330
if "run" in marker.kwargs:
331
if marker.kwargs["run"] is False:
332
pytest.skip()
333
yield
334
return
335
if "raises" in marker.kwargs:
336
marker.kwargs.pop("raises")
337
pytest.xfail(**marker.kwargs)
338
339
request.addfinalizer(selenium_driver.stop_driver)
340
341
if driver_instance is None:
342
driver_instance = selenium_driver.driver
343
344
yield driver_instance
345
# Close the browser after BiDi tests. Those make event subscriptions
346
# and doesn't seems to be stable enough, causing the flakiness of the
347
# subsequent tests.
348
# Remove this when BiDi implementation and API is stable.
349
if selenium_driver.bidi:
350
request.addfinalizer(selenium_driver.stop_driver)
351
352
if request.node.get_closest_marker("no_driver_after_test"):
353
driver_instance = None
354
355
356
@pytest.fixture(scope="session", autouse=True)
357
def stop_driver(request):
358
def fin():
359
global driver_instance
360
if driver_instance is not None:
361
driver_instance.quit()
362
driver_instance = None
363
364
request.addfinalizer(fin)
365
366
367
def pytest_exception_interact(node, call, report):
368
if report.failed:
369
global driver_instance
370
if driver_instance is not None:
371
driver_instance.quit()
372
driver_instance = None
373
374
375
@pytest.fixture
376
def pages(driver, webserver):
377
class Pages:
378
def url(self, name, localhost=False):
379
return webserver.where_is(name, localhost)
380
381
def load(self, name):
382
driver.get(self.url(name))
383
384
return Pages()
385
386
387
@pytest.fixture(autouse=True, scope="session")
388
def server(request):
389
drivers = request.config.getoption("drivers")
390
if drivers is None or "remote" not in drivers:
391
yield None
392
return
393
394
jar_path = os.path.join(
395
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
396
"java/src/org/openqa/selenium/grid/selenium_server_deploy.jar",
397
)
398
399
remote_env = os.environ.copy()
400
if platform.system() == "Linux":
401
# There are issues with window size/position when running Firefox
402
# under Wayland, so we use XWayland instead.
403
remote_env["MOZ_ENABLE_WAYLAND"] = "0"
404
405
if Path(jar_path).exists():
406
# use the grid server built by bazel
407
server = Server(path=jar_path, env=remote_env)
408
else:
409
# use the local grid server (downloads a new one if needed)
410
server = Server(env=remote_env)
411
server.start()
412
yield server
413
server.stop()
414
415
416
@pytest.fixture(autouse=True, scope="session")
417
def webserver(request):
418
host = get_lan_ip() if request.config.getoption("use_lan_ip") else None
419
420
webserver = SimpleWebServer(host=host)
421
webserver.start()
422
yield webserver
423
webserver.stop()
424
425
426
@pytest.fixture
427
def edge_service():
428
from selenium.webdriver.edge.service import Service as EdgeService
429
430
return EdgeService
431
432
433
@pytest.fixture(scope="function")
434
def driver_executable(request):
435
return request.config.option.executable
436
437
438
@pytest.fixture(scope="function")
439
def clean_driver(request):
440
_supported_drivers = SupportedDrivers()
441
try:
442
driver_class = getattr(_supported_drivers, request.config.option.drivers[0].lower())
443
except (AttributeError, TypeError):
444
raise Exception("This test requires a --driver to be specified.")
445
driver_reference = getattr(webdriver, driver_class)
446
447
# conditionally mark tests as expected to fail based on driver
448
marker = request.node.get_closest_marker(f"xfail_{driver_class.lower()}")
449
if marker is not None:
450
if "run" in marker.kwargs:
451
if marker.kwargs["run"] is False:
452
pytest.skip()
453
yield
454
return
455
if "raises" in marker.kwargs:
456
marker.kwargs.pop("raises")
457
pytest.xfail(**marker.kwargs)
458
459
yield driver_reference
460
if request.node.get_closest_marker("no_driver_after_test"):
461
driver_reference = None
462
463
464
@pytest.fixture(scope="function")
465
def clean_service(request):
466
driver_class = request.config.option.drivers[0].lower()
467
selenium_driver = Driver(driver_class, request)
468
yield selenium_driver.service
469
470
471
@pytest.fixture(scope="function")
472
def clean_options(request):
473
driver_class = request.config.option.drivers[0].lower()
474
yield Driver.clean_options(driver_class, request)
475
476
477
@pytest.fixture
478
def firefox_options(request):
479
_supported_drivers = SupportedDrivers()
480
try:
481
driver_class = request.config.option.drivers[0].lower()
482
except (AttributeError, TypeError):
483
raise Exception("This test requires a --driver to be specified")
484
485
# skip tests in the 'remote' directory if run with a local driver
486
if request.node.path.parts[-2] == "remote" and getattr(_supported_drivers, driver_class) != "Remote":
487
pytest.skip(f"Remote tests can't be run with driver '{driver_class}'")
488
489
options = Driver.clean_options("firefox", request)
490
491
return options
492
493
494
@pytest.fixture
495
def chromium_options(request):
496
_supported_drivers = SupportedDrivers()
497
try:
498
driver_class = request.config.option.drivers[0].lower()
499
except (AttributeError, TypeError):
500
raise Exception("This test requires a --driver to be specified")
501
502
# Skip if not Chrome or Edge
503
if driver_class not in ("chrome", "edge"):
504
pytest.skip(f"This test requires Chrome or Edge, got {driver_class}")
505
506
# skip tests in the 'remote' directory if run with a local driver
507
if request.node.path.parts[-2] == "remote" and getattr(_supported_drivers, driver_class) != "Remote":
508
pytest.skip(f"Remote tests can't be run with driver '{driver_class}'")
509
510
if driver_class in ("chrome", "edge"):
511
options = Driver.clean_options(driver_class, request)
512
513
return options
514
515