Package: selenium-actions
Selenium ActionsPyPI -https://pypi.org/project/selenium-actions/Simple action framework using selenium 🚀Real life examplesbehave -https://github.com/bigSAS/selenium-actions-behave-examplepytest - todoActionsCreate actions objectfromseleniumactionsimportActions,FluentFinder# all timeouts are in secondstimeouts={"short":2,"medium":3,"long":5,"absurd":10}finder=FluentFinder(driver,timeouts=timeouts,default_timeout=timeouts["medium"])actions=Actions(finder,wait_for_condition_timeout=15,wait_between=0.5)ExamplesfromseleniumactionsimportActions,LocatorExists# locators (tuples)main_header=('xpath','//h1[.="Home"]')menu=('id','menu')news_option=('xpath','//menu-option[.="News"]')search_input=('xpath','//search-news//input')form=('xpath','//form')# take some actions 🚀actions.goto('https://some.site.io')# open siteactions.wait_for(LocatorExists(main_header))# wait for condition to be met with default timeout from configuration appliedactions.click(menu)# default timeout from configuration appliedactions.click(news_option,timeout='medium')# 'medium' timeout from configuration appliedactions.type_text(search_input,text='python',explicit_timeout=3)# explicit timeout in seconds (always overrides any timeout from configuration)actions.submit()# submit default form (//form) - works when theres only one form on page# actions.submit(form)  # u can pass form locator alsoactions.wait_for(LocatorExists(('xpath','//search-results')),timeout='long')# wait for condition with 'long' timeout from configuration applied# assert ect...LocatorsLest say we have HTML component (simplified for example 👀)<ul><liclass="menu">Foo</li><liclass="menu">Bar</li><liclass="menu">Baz</li></ul>We want to  be able to click each one of them. So the xpath values for them will be:foo_xpath="//ul/li[@class='menu' and contains(., 'Foo')]"bar_xpath="//ul/li[@class='menu' and contains(., 'Bar')]"baz_xpath="//ul/li[@class='menu' and contains(., 'Baz')]"It can be painfull. Of course you can write a function and parametrize the stringdefget_menu_xpath(label:str):returnf"//ul/li[@class='menu' and contains(., '{label}')]"It's kind of frustrating...We can use Locator class to solve that problemfromseleniumactionsimportLocator# You can define any parameters to a locator value templatemenu_element=Locator(Using.XPATH,"//ul/li[@class='{class_name}' and contains(., '{label}')]")menu_element.get_by(class_name='menu',label='Foo')# >>> ("xpath", "//ul/li[@class='menu' and contains(., 'Foo')]")menu_element.get_by(class_name='active-menu',label='Bar')# >>> ("xpath", "//ul/li[@class='active-menu' and contains(., 'Bar')]")# When you forget to pass values you'll get clear errorbutton=Locator(Using.NAME,'{action}-{foo}')button.get_by()# ValueError: get_by method is missing keyword arguments: ['action', 'foo']button.get_by(action='goto')# ValueError: get_by method is missing keyword argument: foo# cool! 🕹 we can play with it!Examples (advanced)We can go step further and implement our own custom locators 🚀# utils/locators.pyfromseleniumactionsimportUsing,LocatorclassButtonByLabel(Locator):def__init__(self)->None:super().__init__(Using.XPATH,"//button[.='{label}']")classButtonSubmit(Locator):def__init__(self)->None:super().__init__(Using.XPATH,"//button[@type='submit']")classLinkByExactText(Locator):def__init__(self)->None:super().__init__(Using.XPATH,"//a[.='{text}']")classLinkByContainedText(Locator):def__init__(self)->None:super().__init__(Using.XPATH,"//a[contains(.='{text}')]")classHeaderByExactText(Locator):def__init__(self)->None:super().__init__(Using.XPATH,"//h*[.='{text}']")classLocators:# for importing and better intellisence in other moduleslink=LinkByExactText()link_contains=LinkByContainedText()button=ButtonByLabel()submit_button=ButtonSubmit()header=HeaderByExactText()############################################ test.pyfromutlis.locatorsimportLocatorsasLoc# raw callingactions.click(Loc.link.get_by(text='HOME'))actions.wait_for(Loc.header.get_by(text='Welcome home!'))actions.click(Loc.link_contains.get_by(text='see more...'))actions.click(Loc.button.get_by(label='Next'))actions.click(Loc.submit_button.get_by())# or for more readabilityhome_link=Loc.link.get_by(text='HOME')see_more_link=Loc.link_contains.get_by(text='see more...')next_button=Loc.button.get_by(label='Next')submit_button=Loc.submit_button.get_by()home_header=Loc.header.get_by(text='Welcome home!')actions.click(home_button)actions.wait_for(home_header)actions.click(see_more_link)actions.click(next_button)actions.click(submit_button)# SUPER DRY!