diff --git a/docs/assets/images/selene-for-page-objects-guide.md/with-autocomplete-py.png b/docs/assets/images/selene-for-page-objects-guide.md/with-autocomplete-py.png new file mode 100644 index 00000000..cf8b2104 Binary files /dev/null and b/docs/assets/images/selene-for-page-objects-guide.md/with-autocomplete-py.png differ diff --git a/docs/assets/images/selene-for-page-objects-guide.md/with-quick-fix-py.png b/docs/assets/images/selene-for-page-objects-guide.md/with-quick-fix-py.png new file mode 100644 index 00000000..237c2832 Binary files /dev/null and b/docs/assets/images/selene-for-page-objects-guide.md/with-quick-fix-py.png differ diff --git a/docs/assets/images/selene-for-page-objects-guide.md/without-autocomplete-py.png b/docs/assets/images/selene-for-page-objects-guide.md/without-autocomplete-py.png new file mode 100644 index 00000000..0a41d4b5 Binary files /dev/null and b/docs/assets/images/selene-for-page-objects-guide.md/without-autocomplete-py.png differ diff --git a/docs/assets/images/selene-in-action-tutorial.md/todomvc-app-inspect.png b/docs/assets/images/selene-in-action-tutorial.md/todomvc-app-inspect.png new file mode 100644 index 00000000..045c840e Binary files /dev/null and b/docs/assets/images/selene-in-action-tutorial.md/todomvc-app-inspect.png differ diff --git a/docs/assets/images/selene-in-action-tutorial.md/todomvc-app.png b/docs/assets/images/selene-in-action-tutorial.md/todomvc-app.png new file mode 100644 index 00000000..d80861e0 Binary files /dev/null and b/docs/assets/images/selene-in-action-tutorial.md/todomvc-app.png differ diff --git a/docs/assets/images/selene-in-action-tutorial.md/view-page-source-of-todomvc-with-no-todos-in-chrome.png b/docs/assets/images/selene-in-action-tutorial.md/view-page-source-of-todomvc-with-no-todos-in-chrome.png new file mode 100644 index 00000000..2685f1a5 Binary files /dev/null and b/docs/assets/images/selene-in-action-tutorial.md/view-page-source-of-todomvc-with-no-todos-in-chrome.png differ diff --git a/docs/assets/images/selene-quick-start-tutorial.md/autocomplete-browser.png b/docs/assets/images/selene-quick-start-tutorial.md/autocomplete-browser.png new file mode 100644 index 00000000..1ccb1189 Binary files /dev/null and b/docs/assets/images/selene-quick-start-tutorial.md/autocomplete-browser.png differ diff --git a/docs/assets/images/selene-quick-start-tutorial.md/autocomplete-condition.png b/docs/assets/images/selene-quick-start-tutorial.md/autocomplete-condition.png new file mode 100644 index 00000000..011c1c41 Binary files /dev/null and b/docs/assets/images/selene-quick-start-tutorial.md/autocomplete-condition.png differ diff --git a/docs/assets/images/selene-quick-start-tutorial.md/autocomplete-selectors.jpg b/docs/assets/images/selene-quick-start-tutorial.md/autocomplete-selectors.jpg new file mode 100644 index 00000000..23d01a6b Binary files /dev/null and b/docs/assets/images/selene-quick-start-tutorial.md/autocomplete-selectors.jpg differ diff --git a/docs/assets/images/selene-quick-start-tutorial.md/autocomplete-selene-element.png b/docs/assets/images/selene-quick-start-tutorial.md/autocomplete-selene-element.png new file mode 100644 index 00000000..2ff7a54c Binary files /dev/null and b/docs/assets/images/selene-quick-start-tutorial.md/autocomplete-selene-element.png differ diff --git a/docs/assets/images/selene-quick-start-tutorial.md/collapse-child-element.png b/docs/assets/images/selene-quick-start-tutorial.md/collapse-child-element.png new file mode 100644 index 00000000..5dac5d72 Binary files /dev/null and b/docs/assets/images/selene-quick-start-tutorial.md/collapse-child-element.png differ diff --git a/docs/assets/images/selene-quick-start-tutorial.md/compose-css-selector.png b/docs/assets/images/selene-quick-start-tutorial.md/compose-css-selector.png new file mode 100644 index 00000000..b13edc21 Binary files /dev/null and b/docs/assets/images/selene-quick-start-tutorial.md/compose-css-selector.png differ diff --git a/docs/assets/images/selene-quick-start-tutorial.md/compose-selector-child-element.png b/docs/assets/images/selene-quick-start-tutorial.md/compose-selector-child-element.png new file mode 100644 index 00000000..12d61986 Binary files /dev/null and b/docs/assets/images/selene-quick-start-tutorial.md/compose-selector-child-element.png differ diff --git a/docs/assets/images/selene-quick-start-tutorial.md/compose-selector-parent-element.png b/docs/assets/images/selene-quick-start-tutorial.md/compose-selector-parent-element.png new file mode 100644 index 00000000..69e94ec4 Binary files /dev/null and b/docs/assets/images/selene-quick-start-tutorial.md/compose-selector-parent-element.png differ diff --git a/docs/assets/images/selene-quick-start-tutorial.md/configuring-test-runner.png b/docs/assets/images/selene-quick-start-tutorial.md/configuring-test-runner.png new file mode 100644 index 00000000..972a5dc2 Binary files /dev/null and b/docs/assets/images/selene-quick-start-tutorial.md/configuring-test-runner.png differ diff --git a/docs/assets/images/selene-quick-start-tutorial.md/context-menu-inspect.png b/docs/assets/images/selene-quick-start-tutorial.md/context-menu-inspect.png new file mode 100644 index 00000000..c602158c Binary files /dev/null and b/docs/assets/images/selene-quick-start-tutorial.md/context-menu-inspect.png differ diff --git a/docs/assets/images/selene-quick-start-tutorial.md/context-menu-new-python-file.png b/docs/assets/images/selene-quick-start-tutorial.md/context-menu-new-python-file.png new file mode 100644 index 00000000..73f33cd6 Binary files /dev/null and b/docs/assets/images/selene-quick-start-tutorial.md/context-menu-new-python-file.png differ diff --git a/docs/assets/images/selene-quick-start-tutorial.md/find-parent-element.png b/docs/assets/images/selene-quick-start-tutorial.md/find-parent-element.png new file mode 100644 index 00000000..59581c70 Binary files /dev/null and b/docs/assets/images/selene-quick-start-tutorial.md/find-parent-element.png differ diff --git a/docs/assets/images/selene-quick-start-tutorial.md/html-element-highlighted.png b/docs/assets/images/selene-quick-start-tutorial.md/html-element-highlighted.png new file mode 100644 index 00000000..94c40fc6 Binary files /dev/null and b/docs/assets/images/selene-quick-start-tutorial.md/html-element-highlighted.png differ diff --git a/docs/assets/images/selene-quick-start-tutorial.md/new-python-file-created.png b/docs/assets/images/selene-quick-start-tutorial.md/new-python-file-created.png new file mode 100644 index 00000000..9e940483 Binary files /dev/null and b/docs/assets/images/selene-quick-start-tutorial.md/new-python-file-created.png differ diff --git a/docs/assets/images/selene-quick-start-tutorial.md/new-python-file-name.png b/docs/assets/images/selene-quick-start-tutorial.md/new-python-file-name.png new file mode 100644 index 00000000..c2b84d9e Binary files /dev/null and b/docs/assets/images/selene-quick-start-tutorial.md/new-python-file-name.png differ diff --git a/docs/assets/images/selene-quick-start-tutorial.md/run-test-from-pycharm.png b/docs/assets/images/selene-quick-start-tutorial.md/run-test-from-pycharm.png new file mode 100644 index 00000000..88950d8e Binary files /dev/null and b/docs/assets/images/selene-quick-start-tutorial.md/run-test-from-pycharm.png differ diff --git a/docs/assets/images/selene-quick-start-tutorial.md/search-for-html-element.png b/docs/assets/images/selene-quick-start-tutorial.md/search-for-html-element.png new file mode 100644 index 00000000..c5dfa2d0 Binary files /dev/null and b/docs/assets/images/selene-quick-start-tutorial.md/search-for-html-element.png differ diff --git a/docs/assets/images/selene-quick-start-tutorial.md/select-attribute.png b/docs/assets/images/selene-quick-start-tutorial.md/select-attribute.png new file mode 100644 index 00000000..13e28b8e Binary files /dev/null and b/docs/assets/images/selene-quick-start-tutorial.md/select-attribute.png differ diff --git a/docs/selene-cheetsheet-howto.md b/docs/selene-cheetsheet-howto.md new file mode 100644 index 00000000..64433b0d --- /dev/null +++ b/docs/selene-cheetsheet-howto.md @@ -0,0 +1,52 @@ +# Basic Selene commands cheatsheet + +## Find elements + +### Basic location +* `browser.element(selector)` – finds element by selector +* `element.element(selector)` – finds inner element inside another element denoted as element (e.g. saved as `element = browser.element(selector)`) +* `element.all(selector)` – finds inner collection of elements +* `browser.all(selector)` – finds collection of elements by selector + +### Filtering collections +* `collection.by(condition)` – filters collection by condition (where `collection` – saved collection, e.g. via `collection = browser.all(selector)`; `condition` – anything from `be.*` or `have.*`) +* `collection.element_by(condition)` – finds element of collection by condition +* `collection[index]` or `collection.element(index)` – selects element from collection by index (there are shortcuts – `.first` for `[0]` and `.second` for `[1]`) +* `collection[start:stop:step]` – makes slice of collection starting from `start`, ending before `stop`, with step `step` (there are shortcuts – `.odd` for `[::2]` and `.even` for `[1::2]`; there is an alias `sliced(start, stop, step)` for `[start:stop:step]`) + + +#### Advanced filtering + +Commands like ... + +* `collection.by_their(selector, condition)` +* `collection.element_by_its(selector, condition)` +* `collection.all(selector)` +* `collection.all_first(selector)` + +... are used less often, and docs about them can be read by diving into their implementation (there are detailed docstrings). Also you can find examples and explanations of these commands in FAQ as an answer to the question “How to find the desired row in the table by condition ...”. + +## Check conditions + +* `(element | collection | browser).should(condition)` – waits until condition is met, and fails if not +* `(element | collection | browser).wait_until(condition)` – waits until condition is met and returns `false` if not, otherwise `true` +* `(element | collection | browser).matching(condition)` – immediately checks condition and returns `false` if not, otherwise `true` + + +## Advanced + +### Acquiring information from elements + +In Selene you cannot just pull out the text or some attribute from the element. This is done on purpose, because Selene aims to promote the implementation of efficient tests. Good tests are those in which the tester knows in advance what will be on the UI at every moment in time, so he does not need to ask the element for its text or the value of a specific attribute, he either knows it, or performs a check through `.should(have.text('something'))` or `.should(have.attribute('data').value('bar'))`. And if you don't know what you have on your UI, then you are writing a crutch (~ workaround) :) And for crutches – Selene adds “extra API”, which is less concise and makes you write crutches more consciously. For these crutches, the following commands may be useful ... + +* `element.locate()` – will return a clean Selenium WebDriver WebElement, from which you can then get custom attributes, for example `element.locate().get_attribute('src')` +* `element()` – shortcut to `.locate()`, so in real code it may look like `browser.element('#foo')().get_attribute('src')` +* `element.get(query.*)` – waits for the command-query to be executed on the element and returns the result, for example: `browser.element('#foo').get(query.attribute('src'))`, unlike `browser.element('#foo').locate().get_attribute('src')` – waits for the element to appear at least in the DOM and returns the value of the `'src'` attribute of this element +* `collection.locate()` or `collection()` – will return a list of clean WebElements +* `collection.get(query.*)` – there is also, if you find a request for working specifically with a collection among `*` +* `browser.get(query.*)` – there is also, for example `browser.get(query.title)` should return the title of the page (the text inside the `title` tag inside the HTML) + + +### Extra commands on elements + +* `(element | collection | browser).perform(command.*)` – waits for the command to be executed, for example `browser.element('#approve').perform(command.js.click)` or `browser.all('.addvertisment').perform(command.js.remove)` \ No newline at end of file diff --git a/docs/selene-for-page-objects-guide.md b/docs/selene-for-page-objects-guide.md new file mode 100644 index 00000000..9b304e4f --- /dev/null +++ b/docs/selene-for-page-objects-guide.md @@ -0,0 +1,478 @@ + +# Selene for PageObjects + +*Libraries like Selene have one important feature that fundamentally changes the approach to [refactoring](https://en.wikipedia.org/wiki/Code_refactoring) tests with the goal of “encapsulating technical details of interaction with elements on the page in the browser” ( <=> [“PageObject pattern”](https://martinfowler.com/bliki/PageObject.html))...* + + +## Simple test in Selene and Selenium WebDriver + +Let's remember a simple test for searching in Google from the [“Selene: Quick Start”](./selene-quick-start-tutorial.md) tutorial: + +```python +# imports ... +# ... + + +def test_finds_selene(): + + browser.open('https://google.com/ncr') + browser.element(by.name('q')).should(be.blank) + + browser.element(by.name('q')).type('python selene') + .press_enter() + browser.all('#rso>div').should(have.size_greater_than_or_equal(6)) + browser.all('#rso>div').first.should(have.text('selene')) + + browser.all('#rso>div').first.element('h3').click() + browser.should(have.title_containing('selene')) + +``` + +And here is the approximate analog of the above code – fully rewritten in raw Selenium WebDriver: + +```python +# selenium-demo/tests/test_google.py +from selenium import webdriver +from selenium.webdriver import Keys +from selenium.webdriver.common.by import By +from selenium.webdriver.remote.webdriver import WebDriver +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as match + +driver: WebDriver | None + + +def setup_function(): + global driver + driver = webdriver.Chrome() + + +def teardown_function(): + driver.quit() + + +def test_finds_selene_webdriver_version(): + driver.get('https://google.com/ncr') + WebDriverWait(driver, 4).until(match.presence_of_element_located(By.NAME, 'q')) + assert driver.find_element(By.NAME, 'q').get_attribute('value') == '' + + driver.find_element(By.NAME, 'q').send_keys('github yashaka selene') + driver.find_element(By.NAME, 'q').send_keys(Keys.ENTER) + WebDriverWait(driver, 4).until(match.presence_of_element_located(By.CSS_SELECTOR, '#rso>div')) + assert len(driver.find_elements(By.CSS_SELECTOR, '#rso>div')) >= 6 + assert 'yashaka/selene' in driver.find_elements(By.CSS_SELECTOR, '#rso>div')[0].text + + driver.find_elements(By.CSS_SELECTOR, '#rso>div')[0].find_element(By.CSS_SELECTOR, 'h3').click() + WebDriverWait(driver, 4).until(match.title_contains('yashaka/selene')) +``` + +## API comparison: Selenium WebDriver vs Selene + +The analog is really approximate, because Selenium WebDriver does not do everything that Selene does in the context of the logic of our test: + +### driver vs browser + +`driver.*` – is an approximate analog of `browser.*` from Selene. + +### driver.get vs browser.open + +`driver.get(absolute_url)` loads a page as `browser.open(absolute_or_relative_url)`... + +But in `browser.open('/HERE/relative/partial-url')` you can pass a relative URL – relative to the base URL saved before in `browser.config.base_url = 'https://here.base-domain.url'` (respectively, in fact, will be loaded – `https://here.base-domain.url/HERE/relative/partial-url`). + +### driver.find_element vs browser.element + +driver.find_element finds the desired element using By locators. For example, to find the element "#rso>div", you will need the locator `By.CSS_SELECTOR`: `driver.find_element(By.CSS_SELECTOR(#rso>div))` + +browser.element can also use `By` locators: `browser.element(by.css(#rso>div))`. But there is also a much simpler way: just pass the CSS or XPath selector as a string: `browser.element('#rso>div')`. + + +### driver.find_elements vs browser.all + +The same applies to selectors for collections: driver.find_elements needs a `By` locator, while browser.all is able to find a collection of elements by a selector as a string. + +### webelement.browser.config.base_url vs element.get(query.attribute(name)) + +webelement.get_attribute(name) is used to check a specific attribute of the element, in this test value to check that the input field is empty. + +Selene can do the same, but in this test there is no need for this, because the logic of checking value is built into `browser.element(by.name('q)).should(be.blank)`. + +### driver.wait and assert vs browser.should + +`driver.wait(expected_conditions.*, 4000)`, officially named “explicit wait” – is approximately the same (i.e. “waiting check”), as `browser.should(have.*)` or `browser.should(be.*)`, which in Selene can be explained as “waiting checks”. + +Accordingly, `expected_conditions.*` does the same as `have.*`or `be.*` – encapsulates the “check logic”, in other words – the checked condition (“condition”). + +However, the available conditions among these `expected_conditions.*` are much less than in `have.*` and `be.*`. They are given little attention in Selenium WebDriver, because “conditions”, being **high-level** abstractions for **testing** purposes – do not quite correspond to the philosophy of Selenium WebDriver – “to be a **low-level** universal driver for **browser** automation” (i.e. not only for testing tasks). + +Accordingly, for asserts, for example, over elements, you have to use `assert`, which is less reliable than `should`, because `assert` does not know how to wait by timeout until the assert passes. Therefore, in the test, as the easiest solution (a more complex, but also powerful solution will be considered later) – you have to make an additional `driver.wait` before such `assert`, which waits at least partially (for completeness, not all conditions are among `expected_conditions.*`) to the desired state of the element before the assert. + +But the worst thing is that `expected_conditions.*` out of the box does not help in analyzing failing test errors – the most important part of the test development process, which affects the speed of their writing and maintenance. Pay attention to the difference in errors when failing: + +```python +browser.should(have.title_containing('selenium')) # FAIL with: ↙️ +# Timed out after 4s, while waiting for: +# browser.has title containing 'selenium' +# +# Reason: AssertionError: actual title: GitHub – yashaka/selene: User-oriented Web UI browser tests in Python + +# VS + +WebDriverWait(driver, 4).until(match.title_contains('selenium')) # FAIL with: ↙️ +# > raise TimeoutException(message, screen, stacktrace) +# selenium.common.exceptions.TimeoutException: Message: +``` + +Also, as we can see, in `should` we are not required to pass the timeout `4000` as in `wait` – in Selene its default value is 4 seconds and can be changed either globally through `browser.config.timeout = new_value` or as needed through `with` at the time of the call, as in the following examples: + +```python +browser.with_(timeout=10).should(condition) + +browser.element(selector_or_by).with_(timeout=10).should(condition) + +browser.all(selector_or_by).with_(timeout=10).should(condition) +``` + +### Commands over elements + +

Waiting to be documented...

+ +### Implicit waits + +

Waiting to be documented...

+ +### Explicit waits instead of implicit waits + +

Waiting to be documented...

+ +### Key difference in the nature of locators and elements + +But the main difference lies in how we get the elements for further work with them: + +* in Selenium WebDriver a way of finding elements is represented by locators like `By.NAME('q')` or `By.CSS_SELECTOR('#rso>div')`, which we pass to methods like `driver.find_element(locator)` and `driver.find_elements(locator)`, which **immediately** perform search for corresponding actual elements on the page. + +* In Selene a way of finding elements is represented by methods `browser.element(selector_or_by)` and `browser.all(selector_or_by)` – they themselves play the role of “locators” and **only describe the element** that will be found in the future by the corresponding method (by CSS-selector in a string like `'#rso>div'` or “selenium locator” like `by.name('q')`) – the actual search is not performed until we call either the `should` method or some action like `click`. Such “element-locators” are also called “lazy elements”, because their search is “postponed for later”, not performed at the moment of “describing the element” (if to be precise – at the moment of “creating an object representing the element”). + +## Refactoring locators + +Now, let's say we decided to refactor our tests in order to increase the readability of the code responsible for the ways of finding elements, encapsulate (hide) the corresponding details of the HTML structure, and remove the duplication of the corresponding code fragments that may potentially change in the future. The easiest way to do this is to extract locators into variables (or constants) and remove them from the test (to hide technical details from the test, increasing readability in the context of following the test logic). + +Based on what we already know about the differences between Selene and Selenium WebDriver, in the case of the latter, only the `By.*` locators can be moved to variables: + +```python +# imports ... + +# ... + +# we can't call and save: +# query = driver.find_element(By.NAME, 'q') +# because we have not opened any page yet +query = (By.NAME, 'q') +results = (By.CSS_SELECTOR, '#rso>div') +result_header = (By.CSS_SELECTOR, 'h3') + + +def test_finds_selene_webdriver_version(): + driver.get('https://google.com/ncr') + WebDriverWait(driver, 4).until(match.presence_of_element_located(query)) + assert driver.find_element(*query).get_attribute('value') == '' + + driver.find_element(*query).send_keys('github yashaka selene') + driver.find_element(*query).send_keys(Keys.ENTER) + WebDriverWait(driver, 4).until(match.presence_of_element_located(results)) + assert len(driver.find_elements(*results)) >= 6 + assert 'yashaka/selene' in driver.find_elements(*results)[0].text + + driver.find_elements(*results)[0].find_element(*result_header).click() + WebDriverWait(driver, 4).until(match.title_contains('yashaka/selene')) +``` + +In the case of Selene we can immediately assign its “lazy” elements (playing the role of locators): + +```python +# imports ... + +query = browser.element(by.name('q')) +results = browser.all('#rso>div') +first_result_header = results.first.element('h3') + +def test_finds_selene(): + browser.open('https://google.com/ncr') + query.should(be.blank) + + query.type('python selene').press_enter() + results.should(have.size_greater_than_or_equal(6)) + results.first.should(have.text('selene')) + + first_result_header.click() + browser.should(have.title_containing('yashaka/selene')) +``` + +Well, how was it? It seems that in the case of Selenium WebDriver it is very difficult to see the difference before and after refactoring, and even more so to see how the result of refactoring differs between Selenium WebDriver and Selene... Heh, that's right, because in addition to locators there is a lot of other complexity that, unlike in Selene, continues to stick the guts out of Selenium :). + +Let's, at least, simplify the result of refactoring to the code that concerns only locators... Here is an example of what can be achieved by encapsulating locators in variables for Selenium WebDriver: + +```python +from selenium.webdriver.common.by import By + +query = (By.NAME, 'q') + +# ... create driver instance + +driver.get('https://google.com/ncr') + +driver.find_element(*query).send_keys('selenium') + +driver.find_element(*query).send_keys(Keys.ENTER) + +# ... + +driver.find_element(*query).clear() +``` + +And here is the version for Selene, when we assign whole elements: + +```python +query = browser.element(by.name('q')) + +# ... maybe configure browser instance + +browser.open('https://google.com/ncr') + + +query.send_keys('python selene') + +query.press_enter() + +# ... + +query.clear() +``` + +– getting in the end when reusing much more concise and clean code in comparison with Selenium WebDriver! Now the difference is obvious :) + +## The path to PageObject + +The described above nature of Selene elements laziness does define the implementation of the PageObject pattern. + +Actually, remembering that the main principle on which the PageObject is built is encapsulation... + +> *Page objects are a classic example of encapsulation – they hide the details of the UI structure and widgetry from other components (the tests).* +(c) Martin Fowler in [“PageObject”](https://martinfowler.com/bliki/PageObject.html) from 10 September 2013 + +– then we can say that we have already achieved the goal, hiding the implementation details of the locators inside the variables (more precisely constants) outside the test: + +```python +# imports ... + +# ... + +query = (By.NAME, 'q') +results = (By.CSS_SELECTOR, '#rso>div') +result_header = (By.CSS_SELECTOR, 'h3') + + +def test_finds_selene_webdriver_version(): + driver.get('https://google.com/ncr') + WebDriverWait(driver, 4).until(match.presence_of_element_located(query)) + assert driver.find_element(*query).get_attribute('value') == '' + + driver.find_element(*query).send_keys('github yashaka selene') + driver.find_element(*query).send_keys(Keys.ENTER) + WebDriverWait(driver, 4).until(match.presence_of_element_located(results)) + assert len(driver.find_elements(*results)) >= 6 + assert 'yashaka/selene' in driver.find_elements(*results)[0].text + + driver.find_elements(*results)[0].find_element(*result_header).click() + WebDriverWait(driver, 4).until(match.title_contains('yashaka/selene')) +``` + +But let's expand the idea of encapsulation even further, coming to a more classic implementation of the well-known pattern. + +So, we can go even further, encapsulating more low-level technical locators inside a separate module... + +```python +# selene_demo/pages/google.py +# imports ... + +query = browser.element(by.name('q')) +results = browser.all('#rso>div') +first_result_header = results.first.element('h3') +``` + +– to use which... + +![](../assets/images/selene-for-page-objects-guide.md/without-autocomplete-py.png) + +↗️ *(autocompletion does not work...)* + +– you will have to add the import to the test manually + +```python +# selene-demo/tests/google_search_test.py +# imports ... +from selene_demo.pages import google + +# ... + +def test_finds_selene(): + browser.open('https://google.com/ncr') + google.query.should(be.blank) + + google.query.type('python selene').press_enter() + google.results.should(have.size_greater_than_or_equal(6)) + google.results.first.should(have.text('selene')) + + google.first_result_header.click() + browser.should(have.title_containing('yashaka/selene')) +``` +or with the help of the Quick Fix function by Command + . on Mac or Ctrl + . on Windows + +![](../assets/images/selene-for-page-objects-guide.md/with-quick-fix-py.png) + + +– so that the autocompletion starts to work in the IDE for `google`: + + +![](../assets/images/selene-for-page-objects-guide.md/with-autocomplete-py.png) + + +Another friendly for IDE option is to encapsulate the locators + + +* as fields of an **class** object (without intermediate variables inside the module, and **with the ability to use access to neighboring fields right in the moment of initializing fields**): + +```python +# selene_demo/pages/google.py +# imports ... + +class Google: + query = browser.element(by.name('q')) + results = browser.all('#rso>div') + first_result_header = results.first.element('h3') + +google = Google() +``` + +The test code remains the same even after introducing classes: + +```python +# selene-demo/__tests__/google_search_test.py +# ... +from selene_demo.pages.google import google + + +def test_finds_selene(): + browser.open('https://google.com/ncr') + google.query.should(be.blank) + + google.query.type('python selene').press_enter() + google.results.should(have.size_greater_than_or_equal(6)) + google.results.first.should(have.text('selene')) + + google.first_result_header.click() + browser.should(have.title_containing('yashaka/selene')) +``` + +The last code: + +```python +# selene-demo/__tests__/google_search_test.py +# ... +from selene_demo.pages.google import google + + +def test_finds_selene(): + browser.open('https://google.com/ncr') + google.query.should(be.blank) + + google.query.type('python selene').press_enter() + google.results.should(have.size_greater_than_or_equal(6)) + google.results.first.should(have.text('selene')) + + google.first_result_header.click() + browser.should(have.title_containing('yashaka/selene')) +``` + +– you can make it even more “high-level”, encapsulating the interaction logic with the corresponding elements in the context of the relevant business steps of the user... + +```python +# selene-demo/__tests__/google_search_test.py +# ... +from selene_demo.pages.google import google + +# ... + + +def test_finds_selene(): + browser.open('https://google.com/ncr') + google.query.should(be.blank) + + google.search('python selene') + google.results.should(have.size_greater_than_or_equal(6)) + google.result(1).should(have.text('selene')) + + google.follow_link_of_result(number=1) + browser.should(have.title_containing('yashaka/selene')) +``` + +in the class: + + +```python +# selene_demo/pages/google.py +# ... + +class Google: + query = browser.element(by.name('q')) + results = browser.all('#rso>div') + first_result_header = results.first.element('h3') + + def result(self, number): + return self.results[number - 1] + + def search(self, text): + self.query.type(text) + self.query.press_enter() + + def follow_link_of_result(self, number): + self.result(number).element('h3').click() + + +google = Google() +``` + +Perhaps someone will be tempted to implement a certain pattern to simulate “private fields of an object” that are not planned to be accessed from tests: + + +```python +# selene_demo/pages/google.py +# imports ... + +class Google: + query = browser.element(by.name('q')) + results = browser.all('#rso>div') + first_result_header = results.first.element('h3') + submit_button = browser.all(by.name('btnK')).first + + def result(self, number): + return self.results[number - 1] + + def search(self, text): + self.query.type(text) + self.__submit_button.click() + + def follow_link_of_result(self, number): + self.result(number).element('h3').click() + + +google = Google() +``` + +↗️ using a class + +And here it is important that in most cases such premature encapsulation contradicts KISS. Why do we really need to hide something here? :) From whom? :) On some project, if some manual testers write these tests, and we want to allow them to use only step functions – then yes, it could be... But if not, why this premature optimization? (which is the root of all evil). Why not simplify your life and embed everything in one object returned from a function (or class). + +For the possibility of refactoring elements and actions on them, or more precisely – for the ability to use all the power and variability of Python without restrictions from the automation tool of user steps in the browser – just corresponds to the peculiarity of Selene elements to be “lazy”, that is, “not to be found immediately at the moment of their definition”, which equates them to locators of the type `(By.NAME,'q')`. + +Amen ;) diff --git a/docs/selene-in-action-tutorial.md b/docs/selene-in-action-tutorial.md new file mode 100644 index 00000000..766969e5 --- /dev/null +++ b/docs/selene-in-action-tutorial.md @@ -0,0 +1,519 @@ +# Selene in action + +After the [quick introduction to the basics](./selene-quick-start-tutorial.md) of working with Selene, let's get more acquainted with the most commonly used elements of their syntax on the example of implementing a test scenario for the application – task manager – [TodoMVC](https://todomvc-emberjs-app.autotest.how/). + +```python +# selene-intro/tests/test_todomvc.py + +# ... imports + + + + +def test_adds_and_completes_todos(): + # open TodoMVC page + + # add todos: 'a', 'b', 'c' + # todos should be 'a', 'b', 'c' + + # toggle 'b' + # completed todos should be 'b' + # active todos should be 'a', 'c' + pass +``` + +Let's play with this application, let's try to repeat this scenario manually... + +![](../assets/images/selene-in-action-tutorial.md/todomvc-app.png) + + +And immediately let's dig into the structure of html for such a set of tasks... + +![](../assets/images/selene-in-action-tutorial.md/todomvc-app-inspect.png) + +... paying attention to the elements with which we interact, and ignoring those elements that we are not interested in yet: + +```html +
+ +
+ + +
+ +
+``` + +Here they are, these elements, needed for the corresponding actions of our scenario: + +- add "a", "b", "c" + +```html + +``` + +- todos should be "a", "b", "c" + +```html + +``` + +- toggle "b" + +```html +
  • +
    + + +``` + +- completed todos should be b + +```html +
  • +
    + + + +
    + +``` + +- active todos should be a, c + +```html + +``` + +Now, knowing the “characteristics” of our elements – their attributes, we [will be able to find them](https://autotest.how/qaest/css-selectors-guide-md) in the code and perform the necessary actions, only if we knew how to find them and how to interact with them. + +Well, let's go, let's implement our scenario: + +```python + +def test_adds_and_completes_todos(): + + # open TodoMVC page + + # add todos: 'a', 'b', 'c' + # todos should be 'a', 'b', 'c' + + # toggle 'b' + # completed todos should be 'b' + # active todos should be 'a', 'c' + pass +``` + +For the first step, the command is obvious, we just need to pass the required URL as a parameter to `open`: + +```python +# open TodoMVC page +browser.open('https://todomvc-emberjs-app.autotest.how/') + +# add todos: 'a', 'b', 'c' +# todos should be 'a', 'b', 'c' + +# toggle 'b' +# completed todos should be 'b' +# active todos should be 'a', 'c' +``` + +For the next step, we need to remember the sequence of actions for adding one task: + +```python +#... + +# add todo 'a': +# 1. find "new todo" text field +# 2. type 'a' +# 3. press Enter +``` + +Now, translating this into the “language of selene” will not be difficult, using IDE hints to find the commands we need to perform actions on the element: + +```python +#... + +# add todo 'a': +#1. find "new todo" text field +# 2. type 'a' +# 3. press Enter +browser.element(by.id('new-todo')).type('a').press_enter() + +``` + +We use the `browser.element` command to access the element on the page ... + +```html + +``` + +... by locator, finding the element by a unique identifier: + +```python + +by.id('new-todo') +``` + +You can use a CSS selector, passing it directly to the `browser.element` command, and get a slightly more concise code: + +```python + +browser.element('#new-todo').type('a').press_enter() +``` + +Even more concisely, you can write the same line using the `s` command: + +```python +s('#new-todo').type('a').press_enter() +``` + +But let's limit ourselves to the previous version for now, let it be less concise, but more readable for most beginners, who probably do not know yet, ;) that in the world of frontend – the dollar is a “standard” command to search for elements by selector, which is usually available in the browser console. + +Now you can add other todos using a very useful approach – “Copy & Paste Driven Development” ;) + +```python +#... + +# add todos 'a', 'b', 'c' +browser.element('#new-todo').type('a').press_enter() +browser.element('#new-todo').type('b').press_enter() +browser.element('#new-todo').type('c').press_enter() + +# todos should be 'a', 'b', 'c' +# ... +``` + +Now let's find all todos in the list using the `browser.all` command and check that they have the corresponding texts: + + +There is also a corresponding “shortcut” for this command: + +```python +ss('#todo-list>li').should(have.exact_texts('a', 'b', 'c')) +``` + +As you can see, the “selene language” is not very different from the “English” ;) + +Checks (aka “assertions” in test automation) on elements or collections of elements, as in the last case, are performed using the `should` command, which is passed a condition to check. + + +```python +# add todos 'a', 'b', 'c' +browser.element('#new-todo').type('a').press_enter() +browser.element('#new-todo').type('b').press_enter() +browser.element('#new-todo').type('c').press_enter() + +# todos should be 'a', 'b', 'c' +browser.all('#todo-list>li').should(have.exact_texts('a', 'b', 'c')) +``` + +Condition sets are available through the `have.*` syntax, which allows you to compose code in the form of readable English phrases. + +But “should + have” is probably not enough to cover all the options for telling the story about checking elements in English language ;). So another set of conditions lives in `be.*`. Let's come up with an example for using `be.*` in our test too ;) + +Pay attention that if you look at the “source” of the TodoMVC web page in the browser: + +![](../assets/images/selene-in-action-tutorial.md/view-page-source-of-todomvc-with-no-todos-in-chrome.png) + +```html + + + + + +Todomvc + + + + + + + + + + + + + + + + +``` + +– it turns out that there are no familiar to us elements there, like the `#new-todo` one! This means, at least, that these elements are added dynamically after loading the html page using JavaScript. That is, they are really “appearing on the page after loading”, not immediately. And, of course, it takes some time. Maybe we just got lucky – nothing was slowing down, and JavaScript dynamically added our `#new-todo` element fast enough so that we could continue our scenario. Well, maybe... – it is not always lucky for us, and it is worth anticipating this moment and waiting for the visibility of the element before performing the necessary actions: + +```python +browser.element('#new-todo').should(be.visible).type('a').press_enter() +``` + +The `should` command plays exactly the role of “explicit waiting for the moment when the element will satisfy the condition”, and not only performs a check by the condition... You can also say that in checks using `should` there is an “implicit waiting” built in. + +And now the good news - in fact, you don't need to write `.should(be.visible)` before each action. The library engine itself will wait until the element is available to perform an action on it, so it is enough: + +```python +browser.element('#new-todo').type('a').press_enter() +``` + +In such cases we say that more powerful “implicit waits” are built into the actions themselves, in addition to the waits built into the checks on elements (aka “assertions”). This is one of the main differences between Selene and “pure” Selenium Webdriver. Selenium implicit waits are disabled by default and if enabled – wait only until the element appears in the DOM (html page), but at the same time the element may still be invisible and therefore not available for interaction, which will lead to a test failure. Or – till the end of loading – the element may be covered by another element, and therefore still will not be available for performing certain actions on it, for example `click`. In Selene, however, the expectations wait until the “action or check passes”. By default, they are enabled, with a default timeout of `4` seconds. The timeout can be specified explicitly either when configuring the browser: + +```python +browser.config.timeout = 6 + +# ... + +browser.element('#new-todo').type('a').press_enter() +``` +or when customizing a specific element or collection of elements: + +```python +# ... + + +browser.element('#new-todo').with_(timeout=6).type('a').press_enter() + +# ... + +browser.all('#todo-list>li').with_(timeout=2).should(have.exact_texts('a', 'b', 'c')) + +# the following will also work: +# browser.with_(timeout=6).all('#todo-list>li').should(have.exact_texts('a', 'b', 'c')) +``` +This `browser.config` is a whole warehouse of various options for configuring the behavior of the library and its commands, be sure to explore all the possible options ;) + +For example, by changing the corresponding option, you can change the type of browser used, and instead of running tests in Chrome, run them in Firefox: + +```python + + +browser.config.browser_name = 'firefox' +``` +Let's go back to our scenario... + +```python +# open TodoMVC page +browser.open('https://todomvc-emberjs-app.autotest.how/') + +# add todos: 'a', 'b', 'c' +browser.element('#new-todo').type('a').press_enter() +browser.element('#new-todo').type('b').press_enter() +browser.element('#new-todo').type('c').press_enter() + +# todos should be 'a', 'b', 'c' +browser.all('#todo-list>li').should(have.exact_texts('a', 'b', 'c')) + +# toggle 'b' +# completed todos should be 'b' +# active todos should be 'a', 'c' +``` +– and finish its implementation ;) + +The next step is to mark the task as “completed”. To do this, you need: + +```python +# ... + +# toggle b: + +# 1:among all todos +# 2:find the one with text 'b' +#3: find its 'toggle' checkbox +# 4:click it +``` +In Selene, this is very easy: + +```python +# ... + +# toggle b: + +# 1:among all todos +# 2:find the one with text 'b' +browser.all('#todo-list>li').element_by(have.exact_text('b'))\ + .element('.toggle').click() +# 4:click it +#3: find its 'toggle' checkbox +``` +As you can see, to find the required element among other elements of the collection, we use the same type of condition (“condition”) as for “waiting-checks”. Without such possibilities, in ordinary Selenium Webdriver, we would have to use bulkier and less readable XPath-selector. + +The final steps are responsible for checking the result of the previous action: + +```python +# ... + +# todos should be 'a', 'b', 'c' +browser.all('#todo-list>li').should(have.exact_texts('a', 'b', 'c')) + +# toggle 'b' +browser.all('#todo-list>li').element_by(have.exact_text('b'))\ + .element('.toggle').click() + +# completed todos should be 'b': + # among all todos – filter only completed ones – check their texts +# active todos should be 'a', 'c': + # among all todos – filter only not completed ones – check their texts +``` +The translation into the “selene language” is still just as simple: + +```python +# ... + +# completed todos should be 'b': + +#1:among all todos + #2:filter only completed ones +browser.all('#todo-list>li').by(have.css_class('completed'))\ + .should(have.exact_texts('b')) + #3:check their texts + +# active todos should be 'a', 'c': + +#1:among all todos + #2:filter not completed ones +browser.all('#todo-list>li').by(have.no.css_class('completed'))\ + .should(have.exact_texts('a', 'c')) + #3:check their texts +``` + +As you can see, in general, the code really becomes so readable that our current comments... + +```python +# open TodoMVC page +browser.open('https://todomvc-emberjs-app.autotest.how/') + +# add todos: 'a', 'b', 'c' +browser.element('#new-todo').type('a').press_enter() +browser.element('#new-todo').type('b').press_enter() +browser.element('#new-todo').type('c').press_enter() + +# todos should be 'a', 'b', 'c' +browser.all('#todo-list>li').should(have.exact_texts('a', 'b', 'c')) + +# toggle 'b' +browser.all('#todo-list>li').element_by(have.exact_text('b'))\ + .element('.toggle').click() + +# completed todos should be 'b' +browser.all('#todo-list>li').by(have.css_class('completed'))\ + .should(have.exact_texts('b')) + +# active todos should be 'a', 'c' +browser.all('#todo-list>li').by(have.no.css_class('completed'))\ + .should(have.exact_texts('a', 'c')) +``` +are no longer needed: + +```python +browser.open('https://todomvc-emberjs-app.autotest.how/') + +browser.element('#new-todo').type('a').press_enter() +browser.element('#new-todo').type('b').press_enter() +browser.element('#new-todo').type('c').press_enter() +browser.all('#todo-list>li').should(have.exact_texts('a', 'b', 'c')) + +browser.all('#todo-list>li').element_by(have.exact_text('b'))\ + .element('.toggle').click() +browser.all('#todo-list>li').by(have.css_class('completed'))\ + .should(have.exact_texts('b')) +browser.all('#todo-list>li').by(have.no.css_class('completed'))\ + .should(have.exact_texts('a', 'c')) +``` + +These basic commands, support for functions “Autocomplete” and “Quick Fix” from IDE, the courage to dig into the code, and finally, the official documentation - should be quite enough to start writing the first tests with Selene. + +Good luck ;) \ No newline at end of file diff --git a/docs/selene-quick-start-tutorial.md b/docs/selene-quick-start-tutorial.md new file mode 100644 index 00000000..f49061da --- /dev/null +++ b/docs/selene-quick-start-tutorial.md @@ -0,0 +1,480 @@ +# Selene: Quick Start + +## What and where? + +Selene in Python is a tool for automating user actions in the browser, oriented towards the convenience and ease of implementing business logic in automated tests, in the language of the user, without distracting from the technical details of working with the “browser driver”. For example, technical details can include working with element waits when automating testing of dynamic web applications, implementing high-level actions over elements, complex locators based on low-level selectors, and so on. + +Under the hood it uses Selenium WebDriver as the main tool for interacting with the browser. Therefore, it is also called more high-level “wrapper” around more low-level tools such as Selenium WebDriver + +Let’s get acquainted with the features of it's use. + +## When? (prerequisites) + +So, owning [basic skills in programming](https://autotest.how/start-programming.guide.md) and having installed the following tools (you can google how – yourself): + + +* [Python](https://autotest.how/qaest/install-python-howto-md) + * [pyenv + python](https://github.com/pyenv/pyenv) + * [poetry](https://poetry.eustace.io/docs/#installation) + +* [PyCharm Community Edition](https://www.jetbrains.com/pycharm/) + +* [Git](https://git-scm.com/) +* [Chrome Browser](https://www.google.com/chrome/) + * [ChromeDriver](https://sites.google.com/a/chromium.org/chromedriver/getting-started) – not required, it will be installed from the code automatically + + +## Initialize project + +In unix-terminal (under Windows available via “Windows Subsystem for Linux” or via “git bash”) we will execute the following... + +Make sure the required version of Python is in place (in the examples below – 3.7.3, but you probably want to choose the latest one): + +```bash +> pyenv install 3.7.3 +> pyenv global 3.7.3 +> python --version +Python 3.7.3 +``` + +Create new project: + +```bash +> poetry new selene-quick-start +Created package selene-quick-start in selene-quick-start +> cd selene-quick-start +> ls +README.rst pyproject.toml selene_quick_start tests +``` + +Install the required local version of python: + +```bash +> pyenv local 3.7.3 +> python -V +Python 3.7.3 +``` + +Opening the project in PyCharm, we will see a fairly simple project structure: + +```text +selene-quick-start +├── .python-version +├── pyproject.toml +├── README.rst +├── selene_quick_start +│ └── __init__.py +└── tests + ├── __init__.py + └── test_poetry_demo.py +``` + +Where... + +`selene-quick-start` – folder with project. ... + +### Project structure + + +#### `.python-version` + +– file with saved local version of Python + +#### `README.rst` + +– basic documentation in [ReST](https://en.wikipedia.org/wiki/ReStructuredText) format (replacing the `.rst` extension with `.md` you can change the format to [Markdown](https://en.wikipedia.org/wiki/Markdown) if you like it better) + +#### `selene_quick_start` + +– the main root module of our project, in which the `init`-file stores the version of the project: + +```python +__version__ = '0.1.0' +``` + +#### `tests` + +– module with tests, with an example of a simple test that checks the current version: + +```pythom +from selene_quick_start import __version__ + +def test_version(): + assert __version__ == '0.1.0' +``` + +#### `pyproject.toml` + +– project configuration file in [TOML](https://en.wikipedia.org/wiki/TOML) + +The following project configurations were generated automatically when the project was created, and their values are obvious and speak for themselves: + +```toml +[tool.poetry] +name = "selene-quick-start" +version = "0.1.0" +description = "" +authors = ["yashaka "] + +[tool.poetry.dependencies] +python = "^3.7" + +[tool.poetry.dev-dependencies] +pytest = "^3.0" + +[build-system] +requires = ["poetry>=1.0.0"] +build-backend = "poetry.masonry.api" +``` +In dependencies... + +```toml +# ... +[tool.poetry.dependencies] +python = "^3.7" + +[tool.poetry.dev-dependencies] +pytest = "^3.0" +# ... +``` + +the [pytest](http://pytest.org/en/latest/) library is already connected, which we will use to [organize tests and run them](https://habr.com/en/post/426699/). + +Now, all you need to do to start working with [Selene](https://github.com/yashaka/selene) is to add it to the project dependencies: + +```toml +# ... +[tool.poetry.dependencies] +python = "^3.7" +selene = {version = "^2.*", allow-prereleases = true} + +[tool.poetry.dev-dependencies] +pytest = "^3.0" +# ... +``` + +You can also add pytest if this dependency was not generated automatically, or specify another version. + +And all this additionally install from the terminal: + +```bash +> poetry install +``` + +Or make the last two steps one command: + +```bash +> poetry add selene --allow-prereleases +``` + +#### `poetry.lock` + +During the installation, poetry will create a virtual environment and install all the necessary dependencies there, [saving their current versions in the `poetry.lock` file](https://poetry.eustace.io/docs/basic-usage/#installing-without-poetrylock). + +If you use `git`, [don't forget to add this file to version control](https://poetry.eustace.io/docs/basic-usage/#commit-your-poetrylock-file-to-version-control) ;) + +#### virtualenv + +Now in PyCharm you should specify in `Preferences > Project: selene-quick-start > Project Interpreter` the path to the created virtual environment, so that the IDE understands where to look for the dependencies used in the project. Most likely, the list of the just created environment will not be, and you need to add a new one by clicking on the “gear” next to the value of the “Project Interpreter” field. And then set the desired path `Add Python Interpreter > Virtual Environment > Existing environment > Interpreter`. The path itself can be taken from the log of the `poetry install` command: + +```bash +> poetry install +Creating virtualenv selene-quick-start-py3.7 in /Users/yashaka/Library/Caches/pypoetry/virtualenvs +Updating dependencies +Resolving dependencies... (6.5s) + +Writing lock file +... +``` + +If you didn't find it, specify the name and path of the current activated “env”: + +```bash +> poetry env list --full-path +/Users/yashaka/Library/Caches/pypoetry/virtualenvs/selene-quick-start-9GqV_4P_-py3.7 (Activated) +``` + + +## Brief introduction to the Selene API + +Now, to use all the beauty of Selene, in the code it is enough to simply make a couple of imports like: + +```python +from selene import browser, by, be, have + +# ... + + +``` + +Object `browser` is the main entry point to the API Selene. Here are its most basic “commands”: + +* `browser.open(url)` - loads the page by URL (and, by default, opens the Chrome browser automatically, if it is not yet open, and if the Chrome driver is installed in the system), + +* `browser.element(selector)`(or `s` as in [JQuery](https://jquery.com/)) - finds an **element** by selector (CSS, XPath or a special selector from `by`), + +* `browser.all(selector)` (or `ss` as in [JQuery](https://jquery.com/)) - finds a **collection of elements** by selector (CSS, XPath, or a special selector from the `by` module), + +Specialized named selectors, such as search by specific attributes (e.g. `by.name('q')`) or by text (e.g. `by.text('Google Search')`) live in the module `by`. + +The be.* and have.* modules contain conditions for checking elements using the `.should*` methods, for example: + +* to check the corresponding exact text of the element with the value `"submit"` of the `type` attribute: + +```python + +browser.element('[type=submit]').should(have.exact_text('Google Search')) +``` + +* to check that the element with the value `"q"` of the `name` attribute is “empty” (i.e. “blank”): + +```python + +browser.element(by.name('q')).should(be.blank) +``` + +In principle, this is enough for a more experienced or just curious specialist to start working. All other commands will be suggested by your favorite IDE after entering a dot in the code. + +![](../assets/images/selene-quick-start-tutorial.md/autocomplete-browser.png) + +![](../assets/images/selene-quick-start-tutorial.md/autocomplete-selectors.png) + +![](../assets/images/selene-quick-start-tutorial.md/autocomplete-selene-element.png) + +![](../assets/images/selene-quick-start-tutorial.md/autocomplete-condition.png) + + +Thus, Selene provides a set of tools for automating user scenarios in his own language, using the terminology familiar to him, which is quite important, for example, for acceptance testing: + +```python + +browser.element(by.name('q')).should(be.blank) \ + .type('selenium').press_enter() + +``` + +### First test with Selene + Pytest + +In such an exploratory mode, you can even test google by performing the corresponding... + +#### Selector composition in the browser inspector + +![](../assets/images/selene-quick-start-tutorial.md/context-menu-inspect.png) + +↗️ *having called the context menu on the query field and selecting “Inspect”...* + +____________ + +![](../assets/images/selene-quick-start-tutorial.md/html-element-highlighted.png) + +↗️ *having seen the highlighted blue html element in the inspector...* + +____________ + +![](../assets/images/selene-quick-start-tutorial.md/select-attribute.png) + +↗️ *having selected a more or less readable and unique pair `attribute="value"`...* + +____________ + +![](../assets/images/selene-quick-start-tutorial.md/compose-css-selector.png) + +↗️ *having composed the corresponding [CSS-selector](https://www.w3schools.com/cssref/css_selectors.asp) for finding the element (`[name="q"]` or `[name=q]` – double quotes in this case are not required) – and having made sure that exactly one element will be found, the one we need (in the future we can also use the locator `by.name('q')` instead of the selector `'[name=q]'`)...* + +![](../assets/images/selene-quick-start-tutorial.md/search-for-html-element.png) + +↗️ *having performed a search for something like “selenium” – using the “magnifying glass” (the last button highlighted in blue in the menu on the left of the “Elements” tab) – inspect one of the sub-elements of one of the found results...* + +![](../assets/images/selene-quick-start-tutorial.md/find-parent-element.png) + +↗️ *having risen up the html-tree – find the parent element that unites all elements with results...* + +![](../assets/images/selene-quick-start-tutorial.md/collapse-child-element.png) + +↗️ *having collapsed the first “child-element-result” (inspected earlier) – make sure that the other “results” live next to it – and after having not found adequate “attribute-value” pairs for them...* +____________ + +![](../assets/images/selene-quick-start-tutorial.md/compose-selector-parent-element.png) + +↗️ *first having built a CSS-selector to find the “parent” that has a more or less adequate “attribute-value” pair (`id="rso"` – later, in the code, we can use the abbreviated selector `'#rso'`)...* +____________ + +![](../assets/images/selene-quick-start-tutorial.md/compose-selector-child-element.png) + +↗️ *then having specified the selector to find the “children of the parent by tag” (`div`) on the first depth of nesting (`>`) and making sure of the correct number of elements found by the selector (on the picture – 8, but you may have a different number, depending on what google will personally recommend to you)...* + +____________ + +– add to the project in the `tests` folder in the test file `test_google.py` corresponding... + + +![](../assets/images/selene-quick-start-tutorial.md/context-menu-new-python-file.png) + +![](../assets/images/selene-quick-start-tutorial.md/new-python-file-name.png) + +![](../assets/images/selene-quick-start-tutorial.md/new-python-file-created.png) + +#### Implementation code of the scenario + + +```python +# selene-quick-start/tests/test_google.py + +from selene import browser, by, be, have + +import pytest + + +def test_finds_selene(): + + browser.open('https://google.com/ncr') + browser.element(by.name('q')).should(be.blank) + + browser.element(by.name('q')).type('python selene').press_enter() + browser.all('#rso>div').should(have.size_greater_than_or_equal(6)) + browser.all('#rso>div').first.should(have.text('selene')) + + browser.all('#rso>div').first.element('h3').click() + browser.should(have.title_containing('selene')) +``` + +If you had time to set up the test runner in PyCharm: + +![](../assets/images/selene-quick-start-tutorial.md/configuring-test-runner.png) + +Then you can run the test right from the IDE: + +![](../assets/images/selene-quick-start-tutorial.md/run-test-from-pycharm.png) + +Another way is the good old launch from the terminal: + +```shell +> poetry run pytest tests/test_google.py +``` + +Or after... + +```shell +> poetry shell +``` + +– a little shorter: + +```shell +> pytest tests/test_google.py +``` + +Each of the methods should show us a beautiful movie in the Chrome browser ;) + +The `browser.element` (or `s`) and `browser.all` (or `ss`) commands play the role of “high-level locators”, in other words - “ways to find elements”, that is, the result of the command will not lead to an attempt to find the element in the browser at the time of the call, and therefore the result of such commands can be saved in variables, for example + +```python + + +query = browser.element(by.name('q')) + + +``` + +even before opening the browser for more convenient reuse in the future, increasing the readability of the code and removing potential selector duplicates that may change, and cause trouble when updating tests accordingly in many places throughout the project... + +```python +# selene-quick-start/tests/test_google.py + +from selene import browser, by, be, have + +import pytest + + +query = browser.element(by.name('q')) +results = browser.all('#rso>div') +first_result_header = results.first.element('h3') + + +def test_finds_selene(): + browser.open('https://google.com/ncr') + query.should(be.blank) + + query.type('python selene').press_enter() + results.should(have.size_greater_than_or_equal(6)) + results.first.should(have.text('selene')) + + first_result_header.click() + browser.should(have.title_containing('selene')) +``` + +Perhaps, repeating these same steps, your test will not pass. Consider yourself lucky – you have an additional task – to find the error and fix the test. Probably, something has changed in the implementation of the page or search algorithms, and it's time to make updates to the implementation. + +### A word on documentation and configuration ;) + +If you don't have enough information about how it works, you can always “drill down” (`Cmd+Click` on Mac OS, `Ctrl+Click` on Windows) into the code of the implementation of the necessary commands/methods, read comments with documentation (if any), or just figure it out with the code. + + +For example, you can find out what else is included in the API by falling into the lines with imports in `from selene`. + +There you will find, in addition to the brethren `browser`, `by`, `be`, `have`, such as `element`, `should`, `perform`, `Browser`, `Configuration`, even a small documentation with examples ;) There you can also find some tricks that will allow you to simplify the code written by us above in places, at least - reduce the number of lines :) + +Don't be afraid to explore the Selene's API on your own, all commands with parameters are named as naturally as possible in accordance with the value of the corresponding English phrases, and in most cases it is quite possible to understand what a particular command does, just by looking at its [“signature”](https://en.wikipedia.org/wiki/API#Function_signature) and description. + +For example, exploring the “insides” of `browser.*` in IDE: + +– you can quickly find the corresponding methods for setting the browser window size: + +```python +# ... +from selene import browser + + +# ... + + browser.config.window_width = 1024 + browser.config.window_height = 768 + +# ... +``` + +– and configure their execution before each test using setup fixtures: + +```python +# ... +import pytest + +# ... + +@pytest.fixture(scope='function', autouse=True) +def setup(): # code inside this function (before yield) ... + # will be executed before each test + # to ensure neededed config options + # regardless of what might be changed + # inside previous test + + + browser.config.window_width = 1024 + browser.config.window_height = 768 + yield + + +def test_finds_selene(): + # ... + +``` + +There is a brief description of the main [Selene's API](./selene-cheetsheet-howto.md). Be sure to read this before using Selene in a real project. + +And support can be obtained in the official [telegram chat](https://t.me/selene_py). + +More detailed practical application of basic commands is explained in the [“Selene in Action”](./selene-in-action-tutorial.md) tutorial. + +Continue to refactor the test from this tutorial in the context of applying the principle of encapsulation, you can in the [“Selene for PageObjects”](./selene-for-page-objects-guide.md) guide, along the way, figuring out the basic differences in the API Selene from Selenium WebDriver. + + + + + +If you are just starting to learn automation testing, then [this set of materials](https://autotest.how/start-programming-guide-md) on the basics of programming can help you too. + +Enjoy! ;) + +P.S. + +Perhaps this guide is the beginning of your path on learning Test Automation or SDET... – keep this [learning map](https://autotest.how/map) as a bonus. Perhaps it will help you make this path more interesting... If you want to go this way with [us](https://autotest.how/team) in the mentoring format starting from any level and reaching up to “Black Belt in SDET”, then not hesitate to get acquainted by filling out [this form](https://forms.gle/VsfLdHcdDfMkPPTw7)😉. \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 40f105bc..0cebb615 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -13,6 +13,10 @@ edit_uri: edit/master/docs/ # Page tree nav: - Overview: index.md + - Selene Quick Start: selene-quick-start-tutorial.md + - Selene in Action: selene-in-action-tutorial.md + - Selene for PageObjects: selene-for-page-objects-guide.md + - Selene Cheetsheet: selene-cheetsheet-howto.md # - Learn Basics: # - Stub Title 1: learn-basics/automate-testing-guide.md # - Learn Advanced: