Skip to content

Commit

Permalink
Upgrade to Selenium 4 and use CDP for Performance metrics (#233)
Browse files Browse the repository at this point in the history
Co-authored-by: Carlos Kidman <carlos@qap.dev>
  • Loading branch information
ElSnoMan and Carlos Kidman authored Jan 27, 2022
1 parent 79373c9 commit b1a5822
Show file tree
Hide file tree
Showing 9 changed files with 115 additions and 42 deletions.
58 changes: 58 additions & 0 deletions pylenium/cdp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
""" Chrome DevTools Protocol (CDP) introduced in Selenium 4.
Resources:
- https://www.selenium.dev/documentation/webdriver/bidirectional/chrome_devtools/
- https://chromedevtools.github.io/devtools-protocol/
* Currently only supports the Chrome Browser, although some chromium browsers may work as well.
"""

from typing import Dict


class CDP:
"""Chrome DevTools Protocol."""

def __init__(self, webdriver):
self._webdriver = webdriver

def execute_command(self, cmd: str, cmd_args: Dict) -> Dict:
"""Execute Chrome Devtools Protocol command and get returned result.
The command and command args should follow chrome devtools protocol domains/commands, refer to link
https://chromedevtools.github.io/devtools-protocol/
Args:
cmd: The command name
cmd_args: The command args. Pass an empty dict {} if there is no command args
Examples:
py.cdp.execute_command('Network.getResponseBody', {'requestId': requestId})
Returns:
A dict of results or an empty dict {} if there is no result to return.
For example, to getResponseBody:
{'base64Encoded': False, 'body': 'response body string'}
"""
return self._webdriver.execute_cdp_cmd(cmd, cmd_args)

def get_performance_metrics(self) -> Dict:
"""Get performance metrics from Chrome DevTools - similar to the Performance tab in Chrome.
Examples:
metrics = py.cdp.get_performance_metrics()
Returns:
A dict of performance metrics including 'ScriptDuration', 'ThreadTime', 'ProcessTime', and 'DomContentLoaded'.
{'metrics': [
{'name': 'Timestamp', 'value': 425608.80694},
{'name': 'AudioHandlers', 'value': 0},
{'name': 'ThreadTime', 'value': 0.002074},
...
]
}
"""
# The commented out code below should have been executed prior to this function call.
# self._webdriver.execute_cdp_cmd("Performance.enable", {})
return self._webdriver.execute_cdp_cmd("Performance.getMetrics", {})
6 changes: 6 additions & 0 deletions pylenium/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from selenium.webdriver.support import expected_conditions as ec

from pylenium import webdriver_factory
from pylenium.cdp import CDP
from pylenium.config import PyleniumConfig
from pylenium.element import Element, Elements
from pylenium.performance import Performance
Expand Down Expand Up @@ -236,6 +237,11 @@ def performance(self) -> Performance:
"""The Pylenium Performance API."""
return Performance(self.webdriver)

@property
def cdp(self) -> CDP:
"""The Chrome DevTools Protocol API."""
return CDP(self.webdriver)

def title(self) -> str:
"""The current page's title."""
self.log.info("[STEP] py.title() - Get the current page title")
Expand Down
21 changes: 14 additions & 7 deletions pylenium/webdriver_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import List, Optional

from selenium import webdriver
from selenium.webdriver import Chrome
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.chrome.service import Service as ChromeService
from selenium.webdriver.edge.service import Service as EdgeService
Expand Down Expand Up @@ -178,7 +179,7 @@ def build_chrome(
extension_paths: Optional[List[str]],
local_path: Optional[str],
webdriver_kwargs: Optional[dict],
) -> WebDriver:
) -> Chrome:
"""Build a ChromeDriver.
Args:
Expand All @@ -194,12 +195,18 @@ def build_chrome(
"""
options = build_options(Browser.CHROME, browser_options, experimental_options, extension_paths)
if local_path:
return webdriver.Chrome(service=ChromeService(local_path), options=options, **(webdriver_kwargs or {}))
return webdriver.Chrome(
service=ChromeService(ChromeDriverManager(version=version).install()),
options=options,
**(webdriver_kwargs or {}),
)
driver = webdriver.Chrome(service=ChromeService(local_path), options=options, **(webdriver_kwargs or {}))
else:
driver = webdriver.Chrome(
service=ChromeService(ChromeDriverManager(version=version).install()),
options=options,
**(webdriver_kwargs or {}),
)

# enable Performance Metrics from Chrome Dev Tools
driver.execute_cdp_cmd("Performance.enable", {})

return driver


def build_edge(
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pytest-xdist = "^2.2.0"
pytest-parallel = "^0.1.0"
axe-selenium-python = "^2.1.6"
Faker = "^8.2.1"
selenium = "4.0.0"
selenium = "4.1.0"

[tool.poetry.dev-dependencies]
black = "^21.5b1"
Expand Down
10 changes: 10 additions & 0 deletions tests/performance/test_cdp_performance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
""" Chrome DevTools Protocol - Performance Tab """
from pylenium.driver import Pylenium


def test_capture_performance_metrics(py: Pylenium):
py.visit("https://qap.dev")
metrics = py.cdp.get_performance_metrics()
assert metrics["metrics"]
assert metrics["metrics"][0]["name"] == "Timestamp"
assert metrics["metrics"][0]["value"] > 0
File renamed without changes.
16 changes: 0 additions & 16 deletions tests/test_cli.py

This file was deleted.

37 changes: 22 additions & 15 deletions tests/test_flows.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
import pytest
from pylenium.driver import Pylenium

def test_add_to_cart(py):
py.visit('https://jane.com')
py.get('[data-testid="deal-image"]').click()

for dropdown in py.find('select'):
dropdown.select(1)
@pytest.fixture
def sauce(py: Pylenium) -> Pylenium:
"""Login to saucedemo.com as standard user."""
py.visit("https://www.saucedemo.com/")
py.get("#user-name").type("standard_user")
py.get("#password").type("secret_sauce")
py.get("#login-button").click()
yield py
py.get("#react-burger-menu-btn").click()
py.get("#logout_sidebar_link").should().be_visible().click()

py.get('[data-testid="add-to-bag"]', timeout=1).click()
assert py.contains("$00.11")

def test_add_to_cart_css(sauce: Pylenium):
"""Add an item to the cart. The number badge on the cart icon should increment as expected."""
sauce.get("[id*='add-to-cart']").click()
assert sauce.get("a.shopping_cart_link").should().have_text("1")

def test_add_to_cart_xpath(py):
py.visit('https://jane.com')
py.getx('//*[@data-testid="deal-image"]').click()

for dropdown in py.findx('//select'):
dropdown.select(1)

py.getx('//*[@data-testid="add-to-bag"]', timeout=1).click()
assert py.contains("$00.11")
def test_add_to_cart_xpath(sauce: Pylenium):
"""Add 6 different items to the cart. There should be 6 items in the cart."""
for button in sauce.findx("//*[contains(@id, 'add-to-cart')]"):
button.click()
sauce.getx("//a[@class='shopping_cart_link']").click()
assert sauce.findx("//*[@class='cart_item']").should().have_length(6)
7 changes: 4 additions & 3 deletions tests/ui/test_element.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,10 @@ def test_element_property(py: Pylenium):


def test_element_should_disappear(py: Pylenium):
spinner = "#serverSideDataTable_processing"
py.visit("https://www.copart.com/lotSearchResults/?free=true&query=nissan")
assert py.get(spinner).should().disappear()
py.visit(f"{THE_INTERNET}/dynamic_loading/1")
py.get("#start > button").click()
assert py.get("#loading").should().disappear()
assert py.get("#finish").should().have_text("Hello World!")


def test_element_has_attribute(py: Pylenium):
Expand Down

0 comments on commit b1a5822

Please sign in to comment.