Git Product home page Git Product logo

selenium-extended's Introduction

test Coverage Status GitHub

selenium-extended

Extends the Selenium webdriver with additional functionality.

  • Finding elements by text and ancestry
  • Key press interface
  • Time-delayed text typing
  • Screenshotting elements
  • Automatic webdriver updates
  • Implicit wait getters, setters and decorators
  • Chrome profile selector

Tested on Python 3.8, 3.9 and 3.10.

Example usage

Initialization

from selex import get_driver, Browser
driver = get_driver(Browser.CHROME)
driver.get("https://github.com/")

Find element(s) by text

A convenient way is provided to locate elements by the text they contain, bypassing the need to use xpath selectors. Optional parameter exact_match controls the strictness of the search.

driver.find_element(By.TEXT, "GitHub")  # returns the first element whose text contains the phrase "GitHub"
driver.find_elements(By.TEXT, "GitHub")  # returns all elements whose text contains the phrase "GitHub"
driver.find_elements(By.TEXT, "GitHub", exact_match = True)  # returns all elements whose text is precisely "GitHub"

Find ancestor

Web elements can return their n-th ancestor. The ancestor's generation is selected using the level parameter.

elem = driver.find_element(By.ID, "input")  # locate a sample element
elem.find_ancestor(level = 1)	# returns the parent (1 level up)
elem.find_ancestor(level = 2)	# returns the grandparent (2 levels up)

Both recursive and non-recursive search is supported. Recursive search is slower but always returns a result. Non-recursive search raises an exception when the ancestor's generation exceeds the document's boundaries.

elem.find_ancestor(level = 999, recursive = True)	# (virtually always) returns the whole web page
elem.find_ancestor(level = 999, recursive = False)	# raises NoSuchElementException

Typing and pressing keys

Driver.press sub-class emulates key presses without the need to use clunky ActionChains. Keys are pressed on a browser level e.g. not directed to any particular element. To send keys to a particular web element, invoke the equivalent WebElement.press methods. All keys from selenium.webdriver.common.keys.Keys are available.

driver.press.ENTER()  # simulates the ENTER keypress
driver.press.TAB()	 # simulates the TAB keypress
elem = driver.find_element(By.ID, "form")  # locate a sample element
elem.press.DELETE()	 # press DELETE with the element in focus

Longer key press sequences can be emulated using the type_in method.

driver.type_in("This text goes to the browser...")

More realistic human typing can be simulated using the slow_type method. Delays between key presses randomly sampled from the specified time range.

driver.slow_type("I am a human!")  # sent to the browser
elem.slow_type("Typing slowly.", max_delay=1, min_delay=0.3)  # sent to the element with additional parameters

Screenshotting elements

Save web elements as png images with zero effort.

elem.save_as_png("Pretty element.png")

Automatic webdriver updates

You know the feeling very well... Your Python script fails because Chrome has automatically updated to a new major release and left the incompatible ChromeDriver in the dust. Now you have to manually download the new ChromeDriver release and replace the existing chromedriver.exe located somewhere deep on the sys path. Repeat once a month... Not with Selex though! Selex Driver will automatically download and replace the existing chromedriver/geckodriver.exe if it detects an update is required and restart itself.

Webdrivers can also be updated manually like so:

from selex.updater import update_chromedriver, update_geckodriver
update_chromedriver()
update_geckodriver()

Implicit wait

The implicit_wait property simplifies interacting with the webdriver's implicitly_wait() mechanic.

current_wait = driver.implicit_wait  # retrieves the current implicit wait time setting
driver.implicit_wait = 5  # sets the implicit wait to 5 seconds

The @wait(time) decorator can be used to force the user-specified implicit_wait time on a class method's execution.

class NewDriver(Driver):
	# __init__ goes here
	@wait(3)
	def search_for_something():
		# do some (soul) searching
new_driver = NewDriver("Chrome")
new_driver.search_for_something()	# waits for 3 seconds before timing out

When a custom class has the Selex Driver as an attribute (rather than it being a parent class), a custom @wait(time) decorator can be manufactured using the wait_factory function.

class BankRobbery():
	def __init__(self):
		self.hillary = get_driver(Browser.CHROME)
	@wait(10)
	def be_useless():
		# die and make Tommy do everything

wait = wait_factory("hillary")  # tells the wait decorator to find the Driver instance at self.hillary

Starting Chrome with a custom profile

Starting Chrome with a custom user profile is made easier by the chrome_options method.

from selex import chrome_options
options = chrome_options(user_data_path = PATH, profile_name = "Tanner")  # PATH points to '...\Google\Chrome\User Data'
driver = get_driver(Browser.CHROME, options=options)  # starts Chromedriver using the custom profile

selenium-extended's People

Contributors

mzaja avatar

Stargazers

Vedran Mihočinec avatar

Watchers

 avatar

selenium-extended's Issues

Implicit wait

Expand the implicit wait functiionality:

  • Create a private attribute 'implicit_wait' storing the value of implicit wait. Make it a property and define a getter function.
  • Define a setter function. Changing the value of the 'implicit_wait' attribute automatically changes the implicit wait time of the webdriver.
  • Create decorator functions for forcing or bypassing implicit wait.

Fix CI and add Python3.10 support

Three minor tasks:

  • Fix CI is failling multiple tests with the same issue FileNotFoundError: [Errno 2] No such file or directory: 'chrome'.
  • Fix Selenium's deprecation warning.
  • Update CI to add Python 3.10 support.

Full CI output is below:

Run xvfb-run --auto-servernum coverage run --source=selex -m unittest discover
......../opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/site-packages/selenium/webdriver/chrome/webdriver.py:70: DeprecationWarning: executable_path has been deprecated, please pass in a Service object
  super(WebDriver, self).__init__(DesiredCapabilities.CHROME['browserName'], "goog",
/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/site-packages/selenium/webdriver/chrome/webdriver.py:70: DeprecationWarning: firefox_binary has been deprecated, please pass in a Service object
  super(WebDriver, self).__init__(DesiredCapabilities.CHROME['browserName'], "goog",
/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/site-packages/selenium/webdriver/chrome/webdriver.py:70: DeprecationWarning: firefox_profile has been deprecated, please pass in an Options object
  super(WebDriver, self).__init__(DesiredCapabilities.CHROME['browserName'], "goog",
/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/site-packages/selenium/webdriver/chrome/webdriver.py:70: DeprecationWarning: service_log_path has been deprecated, please pass in a Service object
  super(WebDriver, self).__init__(DesiredCapabilities.CHROME['browserName'], "goog",
/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/site-packages/selenium/webdriver/chrome/webdriver.py:70: DeprecationWarning: service_args has been deprecated, please pass in a Service object
  super(WebDriver, self).__init__(DesiredCapabilities.CHROME['browserName'], "goog",
/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/site-packages/selenium/webdriver/firefox/webdriver.py:158: DeprecationWarning: firefox_profile has been deprecated, please use an Options object
  firefox_profile = FirefoxProfile(firefox_profile)
E/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/unittest/suite.py:176: ResourceWarning: unclosed file <_io.BufferedWriter name='/dev/null'>
  self._createClassOrModuleLevelException(result, e,
ResourceWarning: Enable tracemalloc to get the object allocation traceback
EEEE/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/unittest/case.py:614: ResourceWarning: unclosed file <_io.BufferedWriter name='/dev/null'>
  outcome.errors.clear()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
EEE.................................s.....
======================================================================
ERROR: setUpClass (tests.unit.test_find_ancestor.ElemFindAncestorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/runner/work/selenium-extended/selenium-extended/tests/unit/test_find_ancestor.py", line 27, in setUpClass
    super().setUpClass()
  File "/home/runner/work/selenium-extended/selenium-extended/tests/setup.py", line 40, in setUpClass
    cls.driver, cls.elem = test_setup()
  File "/home/runner/work/selenium-extended/selenium-extended/tests/setup.py", line 30, in test_setup
    driver = Driver("Chrome")
  File "/home/runner/work/selenium-extended/selenium-extended/selex/driver.py", line 37, in __init__
    getattr(webdriver, browser).__init__(self, **kwargs)
  File "/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/site-packages/selenium/webdriver/chrome/webdriver.py", line 70, in __init__
    super(WebDriver, self).__init__(DesiredCapabilities.CHROME['browserName'], "goog",
  File "/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/site-packages/selenium/webdriver/firefox/webdriver.py", line 158, in __init__
    firefox_profile = FirefoxProfile(firefox_profile)
  File "/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/site-packages/selenium/webdriver/firefox/firefox_profile.py", line 74, in __init__
    shutil.copytree(self.profile_dir, newprof,
  File "/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/shutil.py", line 564, in copytree
    with os.scandir(src) as itr:
FileNotFoundError: [Errno 2] No such file or directory: 'chrome'

======================================================================
ERROR: setUpClass (tests.unit.test_find_elements_by_text.DriverFindElementsByTextTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/runner/work/selenium-extended/selenium-extended/tests/unit/test_find_elements_by_text.py", line 57, in setUpClass
    super().setUpClass()
  File "/home/runner/work/selenium-extended/selenium-extended/tests/setup.py", line 40, in setUpClass
    cls.driver, cls.elem = test_setup()
  File "/home/runner/work/selenium-extended/selenium-extended/tests/setup.py", line 30, in test_setup
    driver = Driver("Chrome")
  File "/home/runner/work/selenium-extended/selenium-extended/selex/driver.py", line 37, in __init__
    getattr(webdriver, browser).__init__(self, **kwargs)
  File "/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/site-packages/selenium/webdriver/chrome/webdriver.py", line 70, in __init__
    super(WebDriver, self).__init__(DesiredCapabilities.CHROME['browserName'], "goog",
  File "/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/site-packages/selenium/webdriver/firefox/webdriver.py", line 158, in __init__
    firefox_profile = FirefoxProfile(firefox_profile)
  File "/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/site-packages/selenium/webdriver/firefox/firefox_profile.py", line 74, in __init__
    shutil.copytree(self.profile_dir, newprof,
  File "/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/shutil.py", line 564, in copytree
    with os.scandir(src) as itr:
FileNotFoundError: [Errno 2] No such file or directory: 'chrome'

======================================================================
ERROR: setUpClass (tests.unit.test_find_elements_by_text.ElemFindElementsByTextTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/runner/work/selenium-extended/selenium-extended/tests/unit/test_find_elements_by_text.py", line 89, in setUpClass
    super().setUpClass()
  File "/home/runner/work/selenium-extended/selenium-extended/tests/setup.py", line 40, in setUpClass
    cls.driver, cls.elem = test_setup()
  File "/home/runner/work/selenium-extended/selenium-extended/tests/setup.py", line 30, in test_setup
    driver = Driver("Chrome")
  File "/home/runner/work/selenium-extended/selenium-extended/selex/driver.py", line 37, in __init__
    getattr(webdriver, browser).__init__(self, **kwargs)
  File "/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/site-packages/selenium/webdriver/chrome/webdriver.py", line 70, in __init__
    super(WebDriver, self).__init__(DesiredCapabilities.CHROME['browserName'], "goog",
  File "/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/site-packages/selenium/webdriver/firefox/webdriver.py", line 158, in __init__
    firefox_profile = FirefoxProfile(firefox_profile)
  File "/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/site-packages/selenium/webdriver/firefox/firefox_profile.py", line 74, in __init__
    shutil.copytree(self.profile_dir, newprof,
  File "/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/shutil.py", line 564, in copytree
    with os.scandir(src) as itr:
FileNotFoundError: [Errno 2] No such file or directory: 'chrome'

======================================================================
ERROR: setUpClass (tests.unit.test_implicit_wait.DriverImplicitWaitFactoryTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/runner/work/selenium-extended/selenium-extended/tests/setup.py", line 40, in setUpClass
    cls.driver, cls.elem = test_setup()
  File "/home/runner/work/selenium-extended/selenium-extended/tests/setup.py", line 30, in test_setup
    driver = Driver("Chrome")
  File "/home/runner/work/selenium-extended/selenium-extended/selex/driver.py", line 37, in __init__
    getattr(webdriver, browser).__init__(self, **kwargs)
  File "/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/site-packages/selenium/webdriver/chrome/webdriver.py", line 70, in __init__
    super(WebDriver, self).__init__(DesiredCapabilities.CHROME['browserName'], "goog",
  File "/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/site-packages/selenium/webdriver/firefox/webdriver.py", line 158, in __init__
    firefox_profile = FirefoxProfile(firefox_profile)
  File "/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/site-packages/selenium/webdriver/firefox/firefox_profile.py", line 74, in __init__
    shutil.copytree(self.profile_dir, newprof,
  File "/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/shutil.py", line 564, in copytree
    with os.scandir(src) as itr:
FileNotFoundError: [Errno 2] No such file or directory: 'chrome'

======================================================================
ERROR: test_decorator (tests.unit.test_implicit_wait.DriverImplicitWaitTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/runner/work/selenium-extended/selenium-extended/tests/unit/test_implicit_wait.py", line 45, in test_decorator
    driver = self.TestDriver("Chrome")
  File "/home/runner/work/selenium-extended/selenium-extended/selex/driver.py", line 37, in __init__
    getattr(webdriver, browser).__init__(self, **kwargs)
  File "/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/site-packages/selenium/webdriver/chrome/webdriver.py", line 70, in __init__
    super(WebDriver, self).__init__(DesiredCapabilities.CHROME['browserName'], "goog",
  File "/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/site-packages/selenium/webdriver/firefox/webdriver.py", line 158, in __init__
    firefox_profile = FirefoxProfile(firefox_profile)
  File "/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/site-packages/selenium/webdriver/firefox/firefox_profile.py", line 74, in __init__
    shutil.copytree(self.profile_dir, newprof,
  File "/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/shutil.py", line 564, in copytree
    with os.scandir(src) as itr:
FileNotFoundError: [Errno 2] No such file or directory: 'chrome'

======================================================================
ERROR: setUpClass (tests.unit.test_keypress.KeypressTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/runner/work/selenium-extended/selenium-extended/tests/setup.py", line 40, in setUpClass
    cls.driver, cls.elem = test_setup()
  File "/home/runner/work/selenium-extended/selenium-extended/tests/setup.py", line 30, in test_setup
    driver = Driver("Chrome")
  File "/home/runner/work/selenium-extended/selenium-extended/selex/driver.py", line 37, in __init__
    getattr(webdriver, browser).__init__(self, **kwargs)
  File "/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/site-packages/selenium/webdriver/chrome/webdriver.py", line 70, in __init__
    super(WebDriver, self).__init__(DesiredCapabilities.CHROME['browserName'], "goog",
  File "/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/site-packages/selenium/webdriver/firefox/webdriver.py", line 158, in __init__
    firefox_profile = FirefoxProfile(firefox_profile)
  File "/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/site-packages/selenium/webdriver/firefox/firefox_profile.py", line 74, in __init__
    shutil.copytree(self.profile_dir, newprof,
  File "/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/shutil.py", line 564, in copytree
    with os.scandir(src) as itr:
FileNotFoundError: [Errno 2] No such file or directory: 'chrome'

======================================================================
ERROR: setUpClass (tests.unit.test_save_as_png.ElemSaveAsPngTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/runner/work/selenium-extended/selenium-extended/tests/unit/test_save_as_png.py", line 31, in setUpClass
    super().setUpClass()
  File "/home/runner/work/selenium-extended/selenium-extended/tests/setup.py", line 40, in setUpClass
    cls.driver, cls.elem = test_setup()
  File "/home/runner/work/selenium-extended/selenium-extended/tests/setup.py", line 30, in test_setup
    driver = Driver("Chrome")
  File "/home/runner/work/selenium-extended/selenium-extended/selex/driver.py", line 37, in __init__
    getattr(webdriver, browser).__init__(self, **kwargs)
  File "/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/site-packages/selenium/webdriver/chrome/webdriver.py", line 70, in __init__
    super(WebDriver, self).__init__(DesiredCapabilities.CHROME['browserName'], "goog",
  File "/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/site-packages/selenium/webdriver/firefox/webdriver.py", line 158, in __init__
    firefox_profile = FirefoxProfile(firefox_profile)
  File "/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/site-packages/selenium/webdriver/firefox/firefox_profile.py", line 74, in __init__
    shutil.copytree(self.profile_dir, newprof,
  File "/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/shutil.py", line 564, in copytree
    with os.scandir(src) as itr:
FileNotFoundError: [Errno 2] No such file or directory: 'chrome'

======================================================================
ERROR: setUpClass (tests.unit.test_typing.TypingTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/runner/work/selenium-extended/selenium-extended/tests/setup.py", line 40, in setUpClass
    cls.driver, cls.elem = test_setup()
  File "/home/runner/work/selenium-extended/selenium-extended/tests/setup.py", line 30, in test_setup
    driver = Driver("Chrome")
  File "/home/runner/work/selenium-extended/selenium-extended/selex/driver.py", line 37, in __init__
    getattr(webdriver, browser).__init__(self, **kwargs)
  File "/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/site-packages/selenium/webdriver/chrome/webdriver.py", line 70, in __init__
    super(WebDriver, self).__init__(DesiredCapabilities.CHROME['browserName'], "goog",
  File "/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/site-packages/selenium/webdriver/firefox/webdriver.py", line 158, in __init__
    firefox_profile = FirefoxProfile(firefox_profile)
  File "/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/site-packages/selenium/webdriver/firefox/firefox_profile.py", line 74, in __init__
    shutil.copytree(self.profile_dir, newprof,
  File "/opt/hostedtoolcache/Python/3.9.10/x64/lib/python3.9/shutil.py", line 564, in copytree
    with os.scandir(src) as itr:
FileNotFoundError: [Errno 2] No such file or directory: 'chrome'

----------------------------------------------------------------------
Ran 48 tests in 0.553s

FAILED (errors=8, skipped=1)
call('wmic datafile where name="C:\\\\Program Files (x86)\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe" get Version /value', shell=True)
[call('wmic datafile where name="C:\\\\Program Files\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe" get Version /value', shell=True),
 call('wmic datafile where name="C:\\\\Program Files (x86)\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe" get Version /value', shell=True)]
Error: Process completed with exit code 1.

Browser starts when importing from the module

For some reason, browser briefly starts and closes when importing from the module. This should not be happening. There is probably a piece of code which, when compiled to bytecode, launches the selenium webdriver. The issue is also observed when editing test files with autosave enabled.

Speed up unit tests

All unit test cases based on the BaseTestCase require starting up Chrome and ChromeDriver at the start of each case. This is generally unnecessary and greatly prolongs the time required to test the module.

A better solution would be to create a custom test suite and spawn the Chrome and ChromeDriver instances only once, at the start of the testing session. However, the question is whether this will work properly with GitHub Actions, coverage and Coveralls.

The concerned test files are:

  • test_find_ancestor.py
  • test_find_elements_by_text.py
  • test_implicit_wait.py
  • test_keypress.py
  • test_save_as_png.py
  • test_typing.py

Implement metaclass

Solve the problem of dynamic inheritance by utilising a metaclass, as opposed to a function which returns an inner class. It would be better to have API like driver = Driver(BrowserType.CHROME) to signify that the returned object is a webdriver instance, rather than using a more obscure driver = get_driver(BrowserType.CHROME) call.

Find elements by text

Create convenienve functions for finding webpage elements by text. Implement both exact match and contains() function via xpath selector.

Update _by_text method API

Update find_element_by_text and find_elements_by_text API to use By.TEXT or By.PARTIAL_TEXT as in the new Selenium API.

Automatically update chromedriver and geckodriver

Write routines which will check the current version of Chrome/Firefox and update the chrome/gecko drivers if a version mismatch is detected. Add an optional parameter in the Driver() constructor to activate this routine if a version mismatch error is raised.

Outstanding tasks:

  • Write ChromeDriver updater.
  • Write GeckoDriver updater.
  • Add an option to upgrade ChromeDriver on minor version mismatch (currently upgrades only on major version mismatch).
  • Update tests to achieve 100% code coverage again.
  • Update documentation to outline the new features.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.