forked from ElSnoMan/pyleniumio
-
Notifications
You must be signed in to change notification settings - Fork 0
/
conftest.py
347 lines (283 loc) · 12.2 KB
/
conftest.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
"""
`conftest.py` and `pylenium.json` files should stay at your Workspace Root (aka Project Root).
conftest.py
Although this file is editable, you should only change its contents if you know what you are doing.
Instead, you can create your own conftest.py file in the folder where you store your tests.
pylenium.json
You can change the values, but DO NOT touch the keys or you will break the schema.
py
The main fixture you need from this is `py`. This is the instance of Pylenium for each test.
Just pass py into your test and you're ready to go!
Examples:
def test_go_to_google(py):
py.visit('https://google.com')
assert 'Google' in py.title()
"""
import copy
import json
import logging
import os
import shutil
import sys
from pathlib import Path
from typing import Dict, Optional
import pytest
import requests
from faker import Faker
from reportportal_client import RPLogger, RPLogHandler
from selenium.common.exceptions import JavascriptException
from pylenium.a11y import PyleniumAxe
from pylenium.config import PyleniumConfig, TestCase
from pylenium.driver import Pylenium
@pytest.fixture(scope="function")
def fake() -> Faker:
"""A basic instance of Faker to make test data."""
return Faker()
@pytest.fixture(scope="function")
def api():
"""A basic instance of Requests to make HTTP API calls."""
return requests
@pytest.fixture(scope="session")
def rp_logger(request):
"""Report Portal Logger"""
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
# Create handler for Report Portal if the service has been
# configured and started.
if hasattr(request.node.config, "py_test_service"):
# Import Report Portal logger and handler to the test module.
logging.setLoggerClass(RPLogger)
rp_handler = RPLogHandler(request.node.config.py_test_service)
# Add additional handlers if it is necessary
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.INFO)
logger.addHandler(console_handler)
else:
rp_handler = logging.StreamHandler(sys.stdout)
# Set INFO level for Report Portal handler.
rp_handler.setLevel(logging.INFO)
return logger
@pytest.fixture(scope="session", autouse=True)
def project_root() -> Path:
"""The Project (or Workspace) root as a filepath.
* This conftest.py file should be in the Project Root if not already.
"""
return Path(__file__).absolute().parent
@pytest.fixture(scope="session", autouse=True)
def test_results_dir(project_root: Path, request) -> Path:
"""Creates the `/test_results` directory to store the results of the Test Run.
Returns:
The `/test_results` directory as a filepath (str).
"""
session = request.node
test_results_dir = project_root.joinpath("test_results")
if test_results_dir.exists():
# delete /test_results from previous Test Run
shutil.rmtree(test_results_dir, ignore_errors=True)
try:
# race condition can occur between checking file existence and
# creating the file when using pytest with multiple workers
test_results_dir.mkdir(parents=True, exist_ok=True)
except FileExistsError:
pass
for test in session.items:
try:
# make the test_result directory for each test
test_results_dir.joinpath(test.name).mkdir(parents=True, exist_ok=True)
except FileExistsError:
pass
return test_results_dir
@pytest.fixture(scope="session")
def _load_pylenium_json(project_root, request) -> PyleniumConfig:
"""Load the default pylenium.json file or the given pylenium.json config file (if specified).
* Pylenium looks for these files from the Project Root!
I may have multiple pylenium.json files with different presets. For example:
- stage-pylenium.json
- dev-testing.json
- firefox-pylenium.json
Examples
--------
$ pytest
>>> Loads the default file: PROJECT_ROOT/pylenium.json
$ pytest pylenium_json=dev-pylenium.json
>>> Loads the config file: PROJECT_ROOT/dev-pylenium.json
$ pytest pylenium_json="configs/stage-pylenium.json"
>>> Loads the config file: PROJECT_ROOT/configs/stage-pylenium.json
"""
custom_config_filepath = request.config.getoption("pylenium_json")
config_filepath = project_root.joinpath(custom_config_filepath or "pylenium.json")
try:
with config_filepath.open() as file:
_json = json.load(file)
config = PyleniumConfig(**_json)
except FileNotFoundError:
logging.warn(
f"The config_filepath was not found, so PyleniumConfig will load with default values. File not found: {config_filepath.absolute()}"
)
config = PyleniumConfig()
return config
@pytest.fixture(scope="session")
def _override_pylenium_config_values(_load_pylenium_json, request) -> PyleniumConfig:
"""Override any PyleniumConfig values after loading the initial pylenium.json config file.
After a pylenium.json config file is loaded and converted to a PyleniumConfig object,
then any CLI arguments override their respective key/values.
"""
config = _load_pylenium_json
# Driver Settings
cli_remote_url = request.config.getoption("--remote_url")
if cli_remote_url:
config.driver.remote_url = cli_remote_url
cli_browser_options = request.config.getoption("--options")
if cli_browser_options:
config.driver.options = [option.strip() for option in cli_browser_options.split(",")]
cli_browser = request.config.getoption("--browser")
if cli_browser:
config.driver.browser = cli_browser
cli_capabilities = request.config.getoption("--caps")
if cli_capabilities:
# --caps must be in '{"name": "value", "boolean": true}' format
# with double quotes around each key. booleans are lowercase.
config.driver.capabilities = json.loads(cli_capabilities)
cli_wire_options = request.config.getoption("--wire_options")
if cli_wire_options:
# --wire_options must be in '{"name": "value", "boolean": true}' format
# with double quotes around each key. booleans are lowercase.
config.driver.seleniumwire_options = json.loads(cli_wire_options)
cli_page_wait_time = request.config.getoption("--page_load_wait_time")
if cli_page_wait_time and cli_page_wait_time.isdigit():
config.driver.page_load_wait_time = int(cli_page_wait_time)
# Logging Settings
cli_screenshots_on = request.config.getoption("--screenshots_on")
if cli_screenshots_on:
shots_on = True if cli_screenshots_on.lower() == "true" else False
config.logging.screenshots_on = shots_on
cli_extensions = request.config.getoption("--extensions")
if cli_extensions:
config.driver.extension_paths = [ext.strip() for ext in cli_extensions.split(",")]
return config
@pytest.fixture(scope="session")
def lambdatest_config() -> Optional[Dict]:
"""Read the LambdatestConfig for the test session.
I want to dynamically set these values:
* via CLI, but this currently doesn't work with LambdaTest
* via ENV variables, but this requires more setup on my (aka the user's) side
"""
capabilities = None
if os.environ.get("LT_USERNAME") and os.environ.get("LT_ACCESS_KEY"):
capabilities = {
"build": os.environ.get("LT_BUILD_NAME"),
"name": os.environ.get("LT_TESTRUN_NAME"),
"platform": "Linux",
"browserName": "Chrome",
"version": "latest",
}
return capabilities
@pytest.fixture(scope="function")
def py_config(_override_pylenium_config_values, lambdatest_config) -> PyleniumConfig:
"""Get a fresh copy of the PyleniumConfig for each test
See _load_pylenium_json and _override_pylenium_config_values for how the initial configuration is read.
"""
config = _override_pylenium_config_values
if lambdatest_config:
config.driver.capabilities = lambdatest_config
return copy.deepcopy(config)
@pytest.fixture(scope="function")
def test_case(test_results_dir: Path, py_config, request) -> TestCase:
"""Manages data pertaining to the currently running Test Function or Case.
* Creates the test-specific logger.
Args:
test_results_dir: The ./test_results directory this Test Run (aka Session) is writing to
Returns:
An instance of TestCase.
"""
test_name = request.node.name
test_result_path = test_results_dir.joinpath(test_name)
py_config.driver.capabilities.update({"name": test_name})
return TestCase(name=test_name, file_path=test_result_path)
@pytest.fixture(scope="function")
def py(test_case: TestCase, py_config, request, rp_logger):
"""Initialize a Pylenium driver for each test.
Pass in this `py` fixture into the test function.
Examples:
def test_go_to_google(py):
py.visit('https://google.com')
assert 'Google' in py.title()
"""
py = Pylenium(py_config)
yield py
try:
if request.node.report.failed:
# if the test failed, execute code in this block
if os.environ.get("LT_USERNAME"):
py.execute_script("lambda-status=failed")
if py_config.logging.screenshots_on:
screenshot = py.screenshot(str(test_case.file_path.joinpath("test_failed.png")))
with open(screenshot, "rb") as image_file:
rp_logger.info(
"Test Failed - Attaching Screenshot",
attachment={"name": "test_failed.png", "data": image_file, "mime": "image/png"},
)
elif request.node.report.passed:
# if the test passed, execute code in this block
if os.environ.get("LT_USERNAME"):
try:
py.webdriver.execute_script("lambda-status=passed")
except JavascriptException:
pass # test not executed in LambdaTest provider
else:
pass
except AttributeError:
rp_logger.error("Unable to access request.node.report.failed, unable to take screenshot.")
except TypeError:
rp_logger.info("Report Portal is not connected to this test run.")
py.quit()
@pytest.fixture(scope="function")
def axe(py) -> PyleniumAxe:
"""The aXe A11y audit tool as a fixture."""
return PyleniumAxe(py.webdriver)
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
"""Yield each test's outcome so we can handle it in other fixtures."""
outcome = yield
report = outcome.get_result()
if report.when == "call":
setattr(item, "report", report)
return report
def pytest_addoption(parser):
parser.addoption("--browser", action="store", default="", help="The lowercase browser name: chrome | firefox")
parser.addoption("--remote_url", action="store", default="", help="Grid URL to connect tests to.")
parser.addoption("--screenshots_on", action="store", default="", help="Should screenshots be saved? true | false")
parser.addoption(
"--pylenium_json",
action="store",
default="",
help="The filepath of the pylenium.json file to use (ie dev-pylenium.json)",
)
parser.addoption("--pylog_level", action="store", default="", help="Set the pylog_level: 'off' | 'info' | 'debug'")
parser.addoption(
"--options",
action="store",
default="",
help='Comma-separated list of Browser Options. Ex. "headless, incognito"',
)
parser.addoption(
"--caps",
action="store",
default="",
help='List of key-value pairs. Ex. \'{"name": "value", "boolean": true}\'',
)
parser.addoption(
"--page_load_wait_time",
action="store",
default="",
help="The amount of time to wait for a page load before raising an error. Default is 0.",
)
parser.addoption(
"--extensions", action="store", default="", help='Comma-separated list of extension paths. Ex. "*.crx, *.crx"'
)
parser.addoption(
"--wire_options",
action="store",
default="",
help='Dict of key-value pairs as a string. Ex. \'{"name": "value", "boolean": true}\'',
)