Skip to content

Commit

Permalink
feat: support different browsers at playwright starts (#398)
Browse files Browse the repository at this point in the history
* NOV-244340: expression for selecting in an array for a context storage (#388)

* chore: base code for selecting in an array using expresions like a.[key='value'].b

* fix: previous unitary tests with the new code

* test: add positive unitary tests for the new feature

* test: add negative unitary tests

* doc: updated methods docstring

* doc: updated changelog

* fix: flake8 format for unitary testing

* fix: flake8 format for dataset module

* chore: adapt code to codeclimate bot stupidity

* test: add unitary test for invalid list structure

* fix: upgrade Faker version to 25.9 (#394)

* fix: upgrade Faker version to 25.9

* fix Firefox unittests

* bug(QAWTO-212): fix result for action before the feature error with background (#397)

* bug(QAWTO-212): fix action before the feature with background output with error

* fix(QAWTO-212): fix flake8 linter messages

* fix(QAWTO-212): move behave import to selected function

* docs(QAWTO-212): update changelog

* feat: add initial integration of playwright (#387)

* feat: add initial integration of playwright

* avoid error in after_scenario

* feat: reuse toolium driver and pageobjects for playwright tests (#389)

* chore: include playwright module on setup (#391)

* chore: include playwright module on setup

* feat: playwright enable headless config (#392)

* feat: stop playwright when driver is closed (#393)

* playwright start by browser

* avoid duplications for ConfigDriver instance

* fix lint

---------

Co-authored-by: robertomier <[email protected]>
Co-authored-by: Rubén González Alonso <[email protected]>
Co-authored-by: Ricardo García Fernández <[email protected]>
  • Loading branch information
4 people authored Jul 15, 2024
1 parent 8604935 commit 35aade4
Show file tree
Hide file tree
Showing 9 changed files with 680 additions and 36 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ v3.1.5
*Release date: In development*

- Fix `export_poeditor_project` method allowing empty export response
- Add `key=value` expressions for selecting elements in the context storage
- Upgrade Faker version to 25.9.*
- Fix result for action before the feature with error and background to fail scenarios

v3.1.4
------
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ Appium-Python-Client~=2.3 # mobile tests
Pillow~=10.1 # visual testing
screeninfo~=0.8
lxml~=5.1
Faker~=18.3
Faker~=25.9
phonenumbers~=8.13
19 changes: 13 additions & 6 deletions toolium/behave/env_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ def execute_after_scenario_steps(self, context):
self.scenario_error = False
self.before_error_message = None
context.scenario.reset()
self.fail_first_step_precondition_exception(context.scenario)
self.fail_first_step_precondition_exception(context.scenario, error_message)
raise Exception(f'Before scenario steps have failed: {error_message}')

def execute_after_feature_steps(self, context):
Expand All @@ -298,18 +298,25 @@ def execute_after_feature_steps(self, context):
context.feature.reset()
for scenario in context.feature.walk_scenarios():
if scenario.should_run(context.config):
self.fail_first_step_precondition_exception(scenario)
self.fail_first_step_precondition_exception(scenario, error_message)
if len(scenario.background_steps) > 0:
context.logger.warn('Background from scenario status udpated to fail')
raise Exception(f'Before feature steps have failed: {error_message}')

def fail_first_step_precondition_exception(self, scenario):
def fail_first_step_precondition_exception(self, scenario, error_message):
"""
Fail first step in the given Scenario and add exception message for the output.
This is needed because xUnit exporter in Behave fails if there are not failed steps.
:param scenario: Behave's Scenario
:param error_message: Exception message
"""
# Behave is an optional dependency in toolium, so it is imported here
from behave.model_core import Status
if len(scenario.steps) > 0:
# Behave is an optional dependency in toolium, so it is imported here
from behave.model_core import Status
scenario.steps[0].status = Status.failed
scenario.steps[0].exception = Exception('Preconditions failed')
scenario.steps[0].error_message = str(self.before_error_message)
scenario.steps[0].error_message = str(error_message)
if len(scenario.background_steps) > 0:
scenario.background_steps[0].status = Status.failed
scenario.background_steps[0].exception = Exception('Preconditions failed')
scenario.background_steps[0].error_message = str(error_message)
266 changes: 264 additions & 2 deletions toolium/config_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,11 @@ def get_error_message_from_traceback(traceback):


class ConfigDriver(object):
def __init__(self, config, utils=None):
def __init__(self, config, utils=None, playwright=None):
self.logger = logging.getLogger(__name__)
self.config = config
self.utils = utils
self.playwright = playwright

def create_driver(self):
"""Create a selenium driver using specified config properties
Expand All @@ -92,6 +93,36 @@ def create_driver(self):

return driver

def create_playwright_browser(self):
"""
Create a playwright browser using specified config properties
:returns: a new playwright browser o persistent browser context
"""
driver_type = self.config.get('Driver', 'type')
try:
self.logger.info("Creating playwright driver (type = %s)", driver_type)
return self._create_playwright_browser()
except Exception as exc:
error_message = get_error_message_from_exception(exc)
self.logger.error("%s driver can not be launched: %s", driver_type.capitalize(), error_message)
raise

def create_playwright_persistent_browser_context(self):
"""
Create a playwright persistent browser context using specified config properties
:returns: a new playwright persistent browser context
"""
driver_type = self.config.get('Driver', 'type')
try:
self.logger.info("Creating playwright persistent context (type = %s)", driver_type)
return self._create_playwright_persistent_browser_context()
except Exception as exc:
error_message = get_error_message_from_exception(exc)
self.logger.error("%s driver can not be launched: %s", driver_type.capitalize(), error_message)
raise

def _create_remote_driver(self):
"""Create a driver in a remote server
View valid capabilities in https://www.selenium.dev/documentation/webdriver/drivers/options/
Expand Down Expand Up @@ -164,6 +195,76 @@ def _create_local_driver(self):

return driver

def _create_playwright_browser(self):
"""Create a browser in local machine using Playwright
:returns: a new browser Playwright
"""
driver_name = self.utils.get_driver_name()
if driver_name in ('android', 'ios', 'iphone'):
raise Exception('Playwright does not support mobile devices')
else:
if driver_name in ['chrome', 'chromium']:
browser = self._setup_playwright_chrome()
elif driver_name == 'firefox':
browser = self._setup_playwright_firefox()
elif driver_name in ['safari', 'webkit']:
browser = self._setup_playwright_webkit()
else:
raise Exception(f'Playwright does not support {driver_name} driver')
return browser

def _create_playwright_persistent_browser_context(self):
"""Create a browser in local machine using Playwright
:returns: a new persistent browser context Playwright
"""
driver_name = self.utils.get_driver_name()
if driver_name in ('android', 'ios', 'iphone'):
raise Exception('Playwright does not support mobile devices')
else:
if driver_name in ['chrome', 'chromium']:
browser_context = self._setup_playwright_persistent_chrome()
elif driver_name == 'firefox':
browser_context = self._setup_playwright_persistent_firefox()
elif driver_name in ['safari', 'webkit']:
browser_context = self._setup_playwright_persistent_webkit()
else:
raise Exception(f'Playwright does not support {driver_name} driver')
return browser_context

def get_playwright_context_options(self):
"""Get Playwright context options from properties file
:returns: Playwright context options
"""
context_options = {}
try:
for key, value in dict(self.config.items('PlaywrightContextOptions')).items():
self.logger.debug("Added Playwright context option: %s = %s", key, value)
context_options[key] = self._convert_property_type(value)
except NoSectionError:
pass
window_width = self.config.get_optional('Driver', 'window_width')
window_height = self.config.get_optional('Driver', 'window_height')
if window_width and window_height:
context_options['viewport'] = {'width': int(window_width), 'height': int(window_height)}
return context_options

def get_playwright_page_options(self):
"""Get Playwright page options from properties file
:returns: Playwright page options
"""
page_options = {}
try:
for key, value in dict(self.config.items('PlaywrightPageOptions')).items():
self.logger.debug("Added Playwright page option: %s = %s", key, value)
page_options[key] = self._convert_property_type(value)
except NoSectionError:
pass
return page_options

def _get_capabilities_from_driver_type(self):
"""Extract browserVersion and platformName from driver type and add them to capabilities
Expand Down Expand Up @@ -295,6 +396,69 @@ def _add_firefox_extensions(self, driver):
except NoSectionError:
pass

def _setup_playwright_firefox(self):
"""Setup Playwright Firefox browser
:returns: a new Playwright Firefox browser
"""
headless_mode = self.config.getboolean_optional('Driver', 'headless')
arguments = []
preferences = {}
self._add_playwright_firefox_arguments(arguments)
# Note: Playwright does not support Firefox extensions
self._add_playwright_firefox_preferences(preferences)
browser_options = self._get_playwright_browser_options()
browser_options = self._update_dict(browser_options, {'args': arguments})
browser_options = self._update_dict(browser_options, {'firefox_user_prefs': preferences})
return self.playwright.firefox.launch(
headless=headless_mode,
**browser_options
)

def _setup_playwright_persistent_firefox(self):
"""Setup Playwright Firefox persistent browser context
:returns: a new Playwright Firefox persistent browser context
"""
headless_mode = self.config.getboolean_optional('Driver', 'headless')
arguments = []
preferences = {}
self._add_playwright_firefox_arguments(arguments)
# Note: Playwright does not support Firefox extensions
self._add_playwright_firefox_preferences(preferences)
context_options = self.get_playwright_context_options()
context_options = self._update_dict(context_options, {'args': arguments})
context_options = self._update_dict(context_options, {'firefox_user_prefs': preferences})
return self.playwright.firefox.launch_persistent_context(
headless=headless_mode,
**context_options
)

def _add_playwright_firefox_arguments(self, arguments):
"""Add Firefox arguments from properties file prepared for Playwright
:param arguments: Firefox arguments object
"""
try:
for pref, pref_value in dict(self.config.items('FirefoxArguments')).items():
pref_value = '={}'.format(pref_value) if pref_value else ''
self.logger.debug("Added Firefox argument: %s%s", pref, pref_value)
arguments.append('--{}{}'.format(pref, self._convert_property_type(pref_value)))
except NoSectionError:
pass

def _add_playwright_firefox_preferences(self, preferences):
"""Add Firefox preferences from properties file prepared for Playwright
:param preferences: Firefox preferences object
"""
try:
for pref, pref_value in dict(self.config.items('FirefoxPreferences')).items():
self.logger.debug("Added Firefox preference: %s = %s", pref, pref_value)
preferences[pref] = self._convert_property_type(pref_value)
except NoSectionError:
pass

@staticmethod
def _convert_property_type(value):
"""Converts the string value in a boolean, integer or string
Expand Down Expand Up @@ -362,6 +526,80 @@ def _get_chrome_options(self, capabilities={}):

return options

def _get_playwright_browser_options(self):
"""
Get Playwright browser options from properties file
:returns: Playwright browser options
"""
browser_options = {}
try:
for key, value in dict(self.config.items('PlaywrightBrowserOptions')).items():
self.logger.debug("Added Playwright Browser option: %s = %s", key, value)
browser_options[key] = self._convert_property_type(value)
except NoSectionError:
pass
return browser_options

def _setup_playwright_chrome(self):
"""
Setup Playwright Chrome browser
:returns: a new Playwright Chrome browser
"""
headless_mode = self.config.getboolean_optional('Driver', 'headless')
arguments = []
self._add_playwright_chrome_arguments(arguments)
self._add_playwright_chrome_extensions(arguments)
browser_options = self._get_playwright_browser_options()
browser_options = self._update_dict(browser_options, {'args': arguments})
return self.playwright.chromium.launch(
headless=headless_mode,
**browser_options
)

def _setup_playwright_persistent_chrome(self):
"""
Setup Playwright Chrome persistent browser context
:returns: a new Playwright Chrome persistent browser context
"""
headless_mode = self.config.getboolean_optional('Driver', 'headless')
arguments = []
self._add_playwright_chrome_arguments(arguments)
self._add_playwright_chrome_extensions(arguments)
context_options = self.get_playwright_context_options()
context_options = self._update_dict(context_options, {'args': arguments})
return self.playwright.chromium.launch_persistent_context(
headless=headless_mode,
**context_options
)

def _add_playwright_chrome_arguments(self, arguments):
"""Add Chrome arguments from properties file prepared for Playwright
:param arguments: Chrome arguments object
"""
try:
for pref, pref_value in dict(self.config.items('ChromeArguments')).items():
pref_value = '={}'.format(pref_value) if pref_value else ''
self.logger.debug("Added Chrome argument: %s%s", pref, pref_value)
arguments.append('--{}{}'.format(pref, self._convert_property_type(pref_value)))
except NoSectionError:
pass

def _add_playwright_chrome_extensions(self, arguments):
"""Add Chrome extensions from properties file
:param arguments: Chrome options object
"""
try:
for pref, pref_value in dict(self.config.items('ChromeExtensions')).items():
self.logger.debug("Added Chrome extension: %s = %s", pref, pref_value)
arguments.append('--load-extension={}'.format(pref_value))
except NoSectionError:
pass

def _add_chrome_options(self, options, option_name):
"""Add Chrome options from properties file
Expand Down Expand Up @@ -427,7 +665,7 @@ def _update_dict(self, initial, update, initial_key=None):
:param initial: initial dict to be updated
:param update: new dict
:param initial_key: update only one key in initial dicts
:return: merged dict
:returns: merged dict
"""
for key, value in update.items():
if initial_key is None or key == initial_key:
Expand Down Expand Up @@ -459,6 +697,30 @@ def _get_safari_options(self, capabilities={}):
self._update_dict(options.capabilities, capabilities)
return options

def _setup_playwright_webkit(self):
"""Setup Playwright Webkit browser
:returns: a new Playwright Webkit browser
"""
headless_mode = self.config.getboolean_optional('Driver', 'headless')
browser_options = self._get_playwright_browser_options()
return self.playwright.webkit.launch(
headless=headless_mode,
**browser_options
)

def _setup_playwright_persistent_webkit(self):
"""Setup Playwright Webkit persistent browser context
:returns: a new Playwright Webkit persistent browser context
"""
headless_mode = self.config.getboolean_optional('Driver', 'headless')
context_options = self.get_playwright_context_options()
return self.playwright.webkit.launch_persistent_context(
headless=headless_mode,
**context_options
)

def _setup_explorer(self):
"""Setup Internet Explorer webdriver
Expand Down
Loading

0 comments on commit 35aade4

Please sign in to comment.