From 9e120129fa2dc0d4c0cc669b62ccd9100c97aa69 Mon Sep 17 00:00:00 2001 From: Peter Song Date: Thu, 5 Sep 2024 10:45:07 +0800 Subject: [PATCH 1/8] Fix the Hugo installation link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b6b50c176af7..581761b8ff6f 100755 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ to use Hugo 0.125.4 Steps needed to have this working locally and work on it: -- Follow the [Install Hugo](https://www.docsy.dev/docs/get-started/other-options/#install-hugo) instructions from Docsy +- [Install Hugo](https://gohugo.io/installation/) and follow the [Get Started](https://www.docsy.dev/docs/get-started/) instructions from Docsy - [Install go](https://go.dev/doc/install) - Clone this repository - Run `cd website_and_docs` From 3b5d7f9a8c5e15b99084c5df8a977ee51159dbf6 Mon Sep 17 00:00:00 2001 From: Peter Song Date: Thu, 5 Sep 2024 10:48:02 +0800 Subject: [PATCH 2/8] Add an example for the test practices Use `python + pytest + selenium` to test the Todo page with the best test practices: "ActionBot" and "LoadableComponent" --- .../test_practices/design_strategies.en.md | 280 ++++++++++++++++++ 1 file changed, 280 insertions(+) diff --git a/website_and_docs/content/documentation/test_practices/design_strategies.en.md b/website_and_docs/content/documentation/test_practices/design_strategies.en.md index e154621960a3..04c9c47dc67f 100644 --- a/website_and_docs/content/documentation/test_practices/design_strategies.en.md +++ b/website_and_docs/content/documentation/test_practices/design_strategies.en.md @@ -375,3 +375,283 @@ public class ActionBot { ``` Once these abstractions have been built and duplication in your tests identified, it's possible to layer PageObjects on top of bots. + +## Example + +An example of `python + pytest + selenium`. + +```python +import pytest +from selenium import webdriver +from selenium.common import ( + ElementNotInteractableException, + NoSuchElementException, + StaleElementReferenceException, +) +from selenium.webdriver import ActionChains +from selenium.webdriver.common.by import By +from selenium.webdriver.remote.webelement import WebElement +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait + + +@pytest.fixture(scope="function") +def chrome_driver(): + driver = webdriver.Chrome() + driver.set_window_size(1024, 768) + driver.implicitly_wait(0.5) + yield driver + driver.quit() + + +class ActionBot: + def __init__(self, driver) -> None: + self.driver = driver + self.wait = WebDriverWait( + driver, + timeout=10, + poll_frequency=2, + ignored_exceptions=[ + NoSuchElementException, + StaleElementReferenceException, + ElementNotInteractableException, + ], + ) + + def element(self, locator: tuple) -> WebElement: + self.wait.until(lambda driver: driver.find_element(*locator)) + return self.driver.find_element(*locator) + + def elements(self, locator: tuple) -> list[WebElement]: + return self.driver.find_elements(*locator) + + def hover(self, locator: tuple) -> None: + element = self.element(locator) + # hover_script = """ + # var event = new MouseEvent('mouseover', {bubbles: true, cancelable: true}); + # arguments[0].dispatchEvent(event); + # """ + # self.driver.execute_script(hover_script, element) + ActionChains(self.driver).move_to_element(element).perform() + + def click(self, locator: tuple) -> None: + element = self.element(locator) + element.click() + + def type(self, locator: tuple, value: str) -> None: + element = self.element(locator) + element.clear() + element.send_keys(value) + + def text(self, locator: tuple) -> str: + element = self.element(locator) + return element.text + + +class LoadableComponent: + def load(self): + raise NotImplementedError("Subclasses must implement this method") + + def is_loaded(self): + raise NotImplementedError("Subclasses must implement this method") + + def get(self): + if not self.is_loaded(): + self.load() + if not self.is_loaded(): + raise Exception("Page not loaded properly.") + return self + + +class TodoPage(LoadableComponent): + url = "https://todomvc.com/examples/react/dist/" + + new_todo_by = (By.CSS_SELECTOR, "input.new-todo") + count_todo_left_by = (By.CSS_SELECTOR, "span.todo-count") + todo_items_by = (By.CSS_SELECTOR, "ul.todo-list>li") + + view_all_by = (By.LINK_TEXT, "All") + view_active_by = (By.LINK_TEXT, "Active") + view_completed_by = (By.LINK_TEXT, "Completed") + + toggle_all_by = (By.CSS_SELECTOR, "input.toggle-all") + clear_completed_by = (By.CSS_SELECTOR, "button.clear-completed") + + @staticmethod + def build_todo_by(s: str) -> tuple: + p = f"//li[.//label[contains(text(), '{s}')]]" + return By.XPATH, p + + @staticmethod + def build_todo_item_label_by(s: str) -> tuple: + p = f"//label[contains(text(), '{s}')]" + return By.XPATH, p + + @staticmethod + def build_todo_item_toggle_by(s: str) -> tuple: + by, using = TodoPage.build_todo_item_label_by(s) + p = f"{using}/../input[@class='toggle']" + return by, p + + @staticmethod + def build_todo_item_delete_by(s: str) -> tuple: + by, using = TodoPage.build_todo_item_label_by(s) + p = f"{using}/../button[@class='destroy']" + return by, p + + @staticmethod + def build_count_todo_left(count: int) -> str: + if count == 1: + return "1 item left!" + else: + return f"{count} items left!" + + def __init__(self, driver): + self.driver = driver + self.bot = ActionBot(driver) + + def load(self): + self.driver.get(self.url) + + def is_loaded(self): + try: + WebDriverWait(self.driver, 10).until( + EC.visibility_of_element_located(self.new_todo_by) + ) + return True + except: + return False + + # business domain below + def count_todo_items_left(self) -> str: + return self.bot.text(self.count_todo_left_by) + + def todo_count(self) -> int: + return len(self.bot.elements(self.todo_items_by)) + + def new_todo(self, s: str): + self.bot.type(self.new_todo_by, s + "\n") + + def toggle_todo(self, s: str): + self.bot.click(self.build_todo_item_toggle_by(s)) + + def hover_todo(self, s: str) -> None: + """ + should hover on the
  • tag with specific label `s` + """ + self.bot.hover(self.build_todo_by(s)) + + def delete_todo(self, s: str): + self.hover_todo(s) + self.bot.click(self.build_todo_item_delete_by(s)) + + def clear_completed_todo(self): + self.bot.click(self.clear_completed_by) + + def toggle_all_todo(self): + self.bot.click(self.toggle_all_by) + + def view_all_todo(self): + self.bot.click(self.view_all_by) + + def view_active_todo(self): + self.bot.click(self.view_active_by) + + def view_completed_todo(self): + self.bot.click(self.view_completed_by) + + +@pytest.fixture +def page(chrome_driver) -> TodoPage: + driver = chrome_driver + return TodoPage(driver).get() + + +class TestTodoPage: + def test_new_todo(self, page: TodoPage): + page.new_todo("aaa") + assert page.count_todo_items_left() == page.build_count_todo_left(1) + + def test_todo_toggle(self, page: TodoPage): + s = "aaa" + page.new_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(1) + + page.toggle_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(0) + + page.toggle_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(1) + + def test_todo_delete(self, page: TodoPage): + s1 = "aaa" + s2 = "bbb" + page.new_todo(s1) + page.new_todo(s2) + assert page.count_todo_items_left() == page.build_count_todo_left(2) + + page.delete_todo(s1) + assert page.count_todo_items_left() == page.build_count_todo_left(1) + + page.delete_todo(s2) + assert page.todo_count() == 0 + + def test_new_100_todo(self, page: TodoPage): + for i in range(100): + s = f"ToDo{i}" + page.new_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(100) + + def test_toggle_all_todo(self, page: TodoPage): + for i in range(10): + s = f"ToDo{i}" + page.new_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(10) + assert page.todo_count() == 10 + + page.toggle_all_todo() + assert page.count_todo_items_left() == page.build_count_todo_left(0) + assert page.todo_count() == 10 + + page.toggle_all_todo() + assert page.count_todo_items_left() == page.build_count_todo_left(10) + assert page.todo_count() == 10 + + def test_clear_completed_todo(self, page: TodoPage): + for i in range(10): + s = f"ToDo{i}" + page.new_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(10) + assert page.todo_count() == 10 + + for i in range(5): + s = f"ToDo{i}" + page.toggle_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(5) + assert page.todo_count() == 10 + + page.clear_completed_todo() + assert page.count_todo_items_left() == page.build_count_todo_left(5) + assert page.todo_count() == 5 + + def test_view_todo(self, page: TodoPage): + for i in range(10): + s = f"ToDo{i}" + page.new_todo(s) + for i in range(4): + s = f"ToDo{i}" + page.toggle_todo(s) + + page.view_all_todo() + assert page.count_todo_items_left() == page.build_count_todo_left(6) + assert page.todo_count() == 10 + + page.view_active_todo() + assert page.count_todo_items_left() == page.build_count_todo_left(6) + assert page.todo_count() == 6 + + page.view_completed_todo() + assert page.count_todo_items_left() == page.build_count_todo_left(6) + assert page.todo_count() == 4 + +``` From 62bc307e3a8596dabbbffce8e92566ff3d2c4a31 Mon Sep 17 00:00:00 2001 From: Peter Song Date: Thu, 5 Sep 2024 17:19:58 +0800 Subject: [PATCH 3/8] Update design_strategies.en.md As suggested, 1. Break the long and complex example down into smaller, more manageable sections or separate files for better readability and maintainability 2. Use a `with` context manager for WebDriver to ensure proper resource cleanup 3. Use specific exception handling instead of a bare except clause 4. Convert static method to instance method for consistency and flexibility 5. Add assertions to verify initial state in test methods 6. Remove the commented code in the `hover` method --- .../test_practices/design_strategies.en.md | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/website_and_docs/content/documentation/test_practices/design_strategies.en.md b/website_and_docs/content/documentation/test_practices/design_strategies.en.md index 04c9c47dc67f..2934d25ff94f 100644 --- a/website_and_docs/content/documentation/test_practices/design_strategies.en.md +++ b/website_and_docs/content/documentation/test_practices/design_strategies.en.md @@ -378,7 +378,10 @@ Once these abstractions have been built and duplication in your tests identified ## Example -An example of `python + pytest + selenium`. +An example of `python + pytest + selenium` +which implemented "**Action Bot**, **Loadable Component** and **Page Object**". + +A `pytest` fixture `chrome_driver`. ```python import pytest @@ -397,13 +400,15 @@ from selenium.webdriver.support.ui import WebDriverWait @pytest.fixture(scope="function") def chrome_driver(): - driver = webdriver.Chrome() - driver.set_window_size(1024, 768) - driver.implicitly_wait(0.5) - yield driver - driver.quit() + with webdriver.Chrome() as driver: + driver.set_window_size(1024, 768) + driver.implicitly_wait(0.5) + yield driver +``` +"**Action Bot**" implementation. +```python class ActionBot: def __init__(self, driver) -> None: self.driver = driver @@ -427,11 +432,6 @@ class ActionBot: def hover(self, locator: tuple) -> None: element = self.element(locator) - # hover_script = """ - # var event = new MouseEvent('mouseover', {bubbles: true, cancelable: true}); - # arguments[0].dispatchEvent(event); - # """ - # self.driver.execute_script(hover_script, element) ActionChains(self.driver).move_to_element(element).perform() def click(self, locator: tuple) -> None: @@ -446,8 +446,11 @@ class ActionBot: def text(self, locator: tuple) -> str: element = self.element(locator) return element.text +``` +"**Loadable Component** definition. +```python class LoadableComponent: def load(self): raise NotImplementedError("Subclasses must implement this method") @@ -461,8 +464,11 @@ class LoadableComponent: if not self.is_loaded(): raise Exception("Page not loaded properly.") return self +``` +"**Loadable Component** and **Page Object**" implementation. +```python class TodoPage(LoadableComponent): url = "https://todomvc.com/examples/react/dist/" @@ -499,8 +505,7 @@ class TodoPage(LoadableComponent): p = f"{using}/../button[@class='destroy']" return by, p - @staticmethod - def build_count_todo_left(count: int) -> str: + def build_count_todo_left(self, count: int) -> str: if count == 1: return "1 item left!" else: @@ -536,9 +541,6 @@ class TodoPage(LoadableComponent): self.bot.click(self.build_todo_item_toggle_by(s)) def hover_todo(self, s: str) -> None: - """ - should hover on the
  • tag with specific label `s` - """ self.bot.hover(self.build_todo_by(s)) def delete_todo(self, s: str): @@ -559,8 +561,11 @@ class TodoPage(LoadableComponent): def view_completed_todo(self): self.bot.click(self.view_completed_by) +``` +Test cases implementation with `pytest`. +```python @pytest.fixture def page(chrome_driver) -> TodoPage: driver = chrome_driver @@ -569,6 +574,7 @@ def page(chrome_driver) -> TodoPage: class TestTodoPage: def test_new_todo(self, page: TodoPage): + assert page.todo_count() == 0 page.new_todo("aaa") assert page.count_todo_items_left() == page.build_count_todo_left(1) @@ -653,5 +659,4 @@ class TestTodoPage: page.view_completed_todo() assert page.count_todo_items_left() == page.build_count_todo_left(6) assert page.todo_count() == 4 - ``` From 6a248565f6e6d3285429543b32787129802d9021 Mon Sep 17 00:00:00 2001 From: Peter Song Date: Thu, 26 Sep 2024 18:41:06 +0800 Subject: [PATCH 4/8] Create using_best_practice.py example for using best practice --- .../design_strategy/using_best_practice.py | 266 ++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 examples/python/tests/design_strategy/using_best_practice.py diff --git a/examples/python/tests/design_strategy/using_best_practice.py b/examples/python/tests/design_strategy/using_best_practice.py new file mode 100644 index 000000000000..29063b8f6a90 --- /dev/null +++ b/examples/python/tests/design_strategy/using_best_practice.py @@ -0,0 +1,266 @@ +""" +An example of `python + pytest + selenium` +which implemented "**Action Bot**, **Loadable Component** and **Page Object**". +""" + +import pytest +from selenium import webdriver +from selenium.common import ( + ElementNotInteractableException, + NoSuchElementException, + StaleElementReferenceException, +) +from selenium.webdriver import ActionChains +from selenium.webdriver.common.by import By +from selenium.webdriver.remote.webelement import WebElement +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait + + +@pytest.fixture(scope="function") +def chrome_driver(): + with webdriver.Chrome() as driver: + driver.set_window_size(1024, 768) + driver.implicitly_wait(0.5) + yield driver + + +class ActionBot: + def __init__(self, driver) -> None: + self.driver = driver + self.wait = WebDriverWait( + driver, + timeout=10, + poll_frequency=2, + ignored_exceptions=[ + NoSuchElementException, + StaleElementReferenceException, + ElementNotInteractableException, + ], + ) + + def element(self, locator: tuple) -> WebElement: + self.wait.until(lambda driver: driver.find_element(*locator)) + return self.driver.find_element(*locator) + + def elements(self, locator: tuple) -> list[WebElement]: + return self.driver.find_elements(*locator) + + def hover(self, locator: tuple) -> None: + element = self.element(locator) + ActionChains(self.driver).move_to_element(element).perform() + + def click(self, locator: tuple) -> None: + element = self.element(locator) + element.click() + + def type(self, locator: tuple, value: str) -> None: + element = self.element(locator) + element.clear() + element.send_keys(value) + + def text(self, locator: tuple) -> str: + element = self.element(locator) + return element.text + + +class LoadableComponent: + def load(self): + raise NotImplementedError("Subclasses must implement this method") + + def is_loaded(self): + raise NotImplementedError("Subclasses must implement this method") + + def get(self): + if not self.is_loaded(): + self.load() + if not self.is_loaded(): + raise Exception("Page not loaded properly.") + return self + + +class TodoPage(LoadableComponent): + url = "https://todomvc.com/examples/react/dist/" + + new_todo_by = (By.CSS_SELECTOR, "input.new-todo") + count_todo_left_by = (By.CSS_SELECTOR, "span.todo-count") + todo_items_by = (By.CSS_SELECTOR, "ul.todo-list>li") + + view_all_by = (By.LINK_TEXT, "All") + view_active_by = (By.LINK_TEXT, "Active") + view_completed_by = (By.LINK_TEXT, "Completed") + + toggle_all_by = (By.CSS_SELECTOR, "input.toggle-all") + clear_completed_by = (By.CSS_SELECTOR, "button.clear-completed") + + @staticmethod + def build_todo_by(s: str) -> tuple: + p = f"//li[.//label[contains(text(), '{s}')]]" + return By.XPATH, p + + @staticmethod + def build_todo_item_label_by(s: str) -> tuple: + p = f"//label[contains(text(), '{s}')]" + return By.XPATH, p + + @staticmethod + def build_todo_item_toggle_by(s: str) -> tuple: + by, using = TodoPage.build_todo_item_label_by(s) + p = f"{using}/../input[@class='toggle']" + return by, p + + @staticmethod + def build_todo_item_delete_by(s: str) -> tuple: + by, using = TodoPage.build_todo_item_label_by(s) + p = f"{using}/../button[@class='destroy']" + return by, p + + def build_count_todo_left(self, count: int) -> str: + if count == 1: + return "1 item left!" + else: + return f"{count} items left!" + + def __init__(self, driver): + self.driver = driver + self.bot = ActionBot(driver) + + def load(self): + self.driver.get(self.url) + + def is_loaded(self): + try: + WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located(self.new_todo_by)) + return True + except: + return False + + # business domain below + def count_todo_items_left(self) -> str: + return self.bot.text(self.count_todo_left_by) + + def todo_count(self) -> int: + return len(self.bot.elements(self.todo_items_by)) + + def new_todo(self, s: str): + self.bot.type(self.new_todo_by, s + "\n") + + def toggle_todo(self, s: str): + self.bot.click(self.build_todo_item_toggle_by(s)) + + def hover_todo(self, s: str) -> None: + self.bot.hover(self.build_todo_by(s)) + + def delete_todo(self, s: str): + self.hover_todo(s) + self.bot.click(self.build_todo_item_delete_by(s)) + + def clear_completed_todo(self): + self.bot.click(self.clear_completed_by) + + def toggle_all_todo(self): + self.bot.click(self.toggle_all_by) + + def view_all_todo(self): + self.bot.click(self.view_all_by) + + def view_active_todo(self): + self.bot.click(self.view_active_by) + + def view_completed_todo(self): + self.bot.click(self.view_completed_by) + + +@pytest.fixture +def page(chrome_driver) -> TodoPage: + driver = chrome_driver + return TodoPage(driver).get() + + +class TestTodoPage: + def test_new_todo(self, page: TodoPage): + assert page.todo_count() == 0 + page.new_todo("aaa") + assert page.count_todo_items_left() == page.build_count_todo_left(1) + + def test_todo_toggle(self, page: TodoPage): + s = "aaa" + page.new_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(1) + + page.toggle_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(0) + + page.toggle_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(1) + + def test_todo_delete(self, page: TodoPage): + s1 = "aaa" + s2 = "bbb" + page.new_todo(s1) + page.new_todo(s2) + assert page.count_todo_items_left() == page.build_count_todo_left(2) + + page.delete_todo(s1) + assert page.count_todo_items_left() == page.build_count_todo_left(1) + + page.delete_todo(s2) + assert page.todo_count() == 0 + + def test_new_100_todo(self, page: TodoPage): + for i in range(100): + s = f"ToDo{i}" + page.new_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(100) + + def test_toggle_all_todo(self, page: TodoPage): + for i in range(10): + s = f"ToDo{i}" + page.new_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(10) + assert page.todo_count() == 10 + + page.toggle_all_todo() + assert page.count_todo_items_left() == page.build_count_todo_left(0) + assert page.todo_count() == 10 + + page.toggle_all_todo() + assert page.count_todo_items_left() == page.build_count_todo_left(10) + assert page.todo_count() == 10 + + def test_clear_completed_todo(self, page: TodoPage): + for i in range(10): + s = f"ToDo{i}" + page.new_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(10) + assert page.todo_count() == 10 + + for i in range(5): + s = f"ToDo{i}" + page.toggle_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(5) + assert page.todo_count() == 10 + + page.clear_completed_todo() + assert page.count_todo_items_left() == page.build_count_todo_left(5) + assert page.todo_count() == 5 + + def test_view_todo(self, page: TodoPage): + for i in range(10): + s = f"ToDo{i}" + page.new_todo(s) + for i in range(4): + s = f"ToDo{i}" + page.toggle_todo(s) + + page.view_all_todo() + assert page.count_todo_items_left() == page.build_count_todo_left(6) + assert page.todo_count() == 10 + + page.view_active_todo() + assert page.count_todo_items_left() == page.build_count_todo_left(6) + assert page.todo_count() == 6 + + page.view_completed_todo() + assert page.count_todo_items_left() == page.build_count_todo_left(6) + assert page.todo_count() == 4 From 06cb3e8ab0139951e91dfedccea0a78dafe6cbc1 Mon Sep 17 00:00:00 2001 From: Peter Song Date: Thu, 26 Sep 2024 18:44:40 +0800 Subject: [PATCH 5/8] Update design_strategies.zh-cn.md --- .../test_practices/design_strategies.zh-cn.md | 285 ++++++++++++++++++ 1 file changed, 285 insertions(+) diff --git a/website_and_docs/content/documentation/test_practices/design_strategies.zh-cn.md b/website_and_docs/content/documentation/test_practices/design_strategies.zh-cn.md index a4c82b251a5c..eaa1c084b314 100644 --- a/website_and_docs/content/documentation/test_practices/design_strategies.zh-cn.md +++ b/website_and_docs/content/documentation/test_practices/design_strategies.zh-cn.md @@ -390,3 +390,288 @@ public class ActionBot { Once these abstractions have been built and duplication in your tests identified, it's possible to layer PageObjects on top of bots. + +## Example + +一个用例 使用 `python + pytest + selenium` 实现了设计策略 "**Action Bot**, **Loadable Component** +和 **Page Object**". + +A `pytest` fixture `chrome_driver`. + +```python +import pytest +from selenium import webdriver +from selenium.common import ( + ElementNotInteractableException, + NoSuchElementException, + StaleElementReferenceException, +) +from selenium.webdriver import ActionChains +from selenium.webdriver.common.by import By +from selenium.webdriver.remote.webelement import WebElement +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait + + +@pytest.fixture(scope="function") +def chrome_driver(): + with webdriver.Chrome() as driver: + driver.set_window_size(1024, 768) + driver.implicitly_wait(0.5) + yield driver +``` + +"**Action Bot**" implementation. + +```python +class ActionBot: + def __init__(self, driver) -> None: + self.driver = driver + self.wait = WebDriverWait( + driver, + timeout=10, + poll_frequency=2, + ignored_exceptions=[ + NoSuchElementException, + StaleElementReferenceException, + ElementNotInteractableException, + ], + ) + + def element(self, locator: tuple) -> WebElement: + self.wait.until(lambda driver: driver.find_element(*locator)) + return self.driver.find_element(*locator) + + def elements(self, locator: tuple) -> list[WebElement]: + return self.driver.find_elements(*locator) + + def hover(self, locator: tuple) -> None: + element = self.element(locator) + ActionChains(self.driver).move_to_element(element).perform() + + def click(self, locator: tuple) -> None: + element = self.element(locator) + element.click() + + def type(self, locator: tuple, value: str) -> None: + element = self.element(locator) + element.clear() + element.send_keys(value) + + def text(self, locator: tuple) -> str: + element = self.element(locator) + return element.text +``` + +"**Loadable Component** definition. + +```python +class LoadableComponent: + def load(self): + raise NotImplementedError("Subclasses must implement this method") + + def is_loaded(self): + raise NotImplementedError("Subclasses must implement this method") + + def get(self): + if not self.is_loaded(): + self.load() + if not self.is_loaded(): + raise Exception("Page not loaded properly.") + return self +``` + +"**Loadable Component** and **Page Object**" implementation. + +```python +class TodoPage(LoadableComponent): + url = "https://todomvc.com/examples/react/dist/" + + new_todo_by = (By.CSS_SELECTOR, "input.new-todo") + count_todo_left_by = (By.CSS_SELECTOR, "span.todo-count") + todo_items_by = (By.CSS_SELECTOR, "ul.todo-list>li") + + view_all_by = (By.LINK_TEXT, "All") + view_active_by = (By.LINK_TEXT, "Active") + view_completed_by = (By.LINK_TEXT, "Completed") + + toggle_all_by = (By.CSS_SELECTOR, "input.toggle-all") + clear_completed_by = (By.CSS_SELECTOR, "button.clear-completed") + + @staticmethod + def build_todo_by(s: str) -> tuple: + p = f"//li[.//label[contains(text(), '{s}')]]" + return By.XPATH, p + + @staticmethod + def build_todo_item_label_by(s: str) -> tuple: + p = f"//label[contains(text(), '{s}')]" + return By.XPATH, p + + @staticmethod + def build_todo_item_toggle_by(s: str) -> tuple: + by, using = TodoPage.build_todo_item_label_by(s) + p = f"{using}/../input[@class='toggle']" + return by, p + + @staticmethod + def build_todo_item_delete_by(s: str) -> tuple: + by, using = TodoPage.build_todo_item_label_by(s) + p = f"{using}/../button[@class='destroy']" + return by, p + + def build_count_todo_left(self, count: int) -> str: + if count == 1: + return "1 item left!" + else: + return f"{count} items left!" + + def __init__(self, driver): + self.driver = driver + self.bot = ActionBot(driver) + + def load(self): + self.driver.get(self.url) + + def is_loaded(self): + try: + WebDriverWait(self.driver, 10).until( + EC.visibility_of_element_located(self.new_todo_by) + ) + return True + except: + return False + + # business domain below + def count_todo_items_left(self) -> str: + return self.bot.text(self.count_todo_left_by) + + def todo_count(self) -> int: + return len(self.bot.elements(self.todo_items_by)) + + def new_todo(self, s: str): + self.bot.type(self.new_todo_by, s + "\n") + + def toggle_todo(self, s: str): + self.bot.click(self.build_todo_item_toggle_by(s)) + + def hover_todo(self, s: str) -> None: + self.bot.hover(self.build_todo_by(s)) + + def delete_todo(self, s: str): + self.hover_todo(s) + self.bot.click(self.build_todo_item_delete_by(s)) + + def clear_completed_todo(self): + self.bot.click(self.clear_completed_by) + + def toggle_all_todo(self): + self.bot.click(self.toggle_all_by) + + def view_all_todo(self): + self.bot.click(self.view_all_by) + + def view_active_todo(self): + self.bot.click(self.view_active_by) + + def view_completed_todo(self): + self.bot.click(self.view_completed_by) +``` + +Test cases implementation with `pytest`. + +```python +@pytest.fixture +def page(chrome_driver) -> TodoPage: + driver = chrome_driver + return TodoPage(driver).get() + + +class TestTodoPage: + def test_new_todo(self, page: TodoPage): + assert page.todo_count() == 0 + page.new_todo("aaa") + assert page.count_todo_items_left() == page.build_count_todo_left(1) + + def test_todo_toggle(self, page: TodoPage): + s = "aaa" + page.new_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(1) + + page.toggle_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(0) + + page.toggle_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(1) + + def test_todo_delete(self, page: TodoPage): + s1 = "aaa" + s2 = "bbb" + page.new_todo(s1) + page.new_todo(s2) + assert page.count_todo_items_left() == page.build_count_todo_left(2) + + page.delete_todo(s1) + assert page.count_todo_items_left() == page.build_count_todo_left(1) + + page.delete_todo(s2) + assert page.todo_count() == 0 + + def test_new_100_todo(self, page: TodoPage): + for i in range(100): + s = f"ToDo{i}" + page.new_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(100) + + def test_toggle_all_todo(self, page: TodoPage): + for i in range(10): + s = f"ToDo{i}" + page.new_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(10) + assert page.todo_count() == 10 + + page.toggle_all_todo() + assert page.count_todo_items_left() == page.build_count_todo_left(0) + assert page.todo_count() == 10 + + page.toggle_all_todo() + assert page.count_todo_items_left() == page.build_count_todo_left(10) + assert page.todo_count() == 10 + + def test_clear_completed_todo(self, page: TodoPage): + for i in range(10): + s = f"ToDo{i}" + page.new_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(10) + assert page.todo_count() == 10 + + for i in range(5): + s = f"ToDo{i}" + page.toggle_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(5) + assert page.todo_count() == 10 + + page.clear_completed_todo() + assert page.count_todo_items_left() == page.build_count_todo_left(5) + assert page.todo_count() == 5 + + def test_view_todo(self, page: TodoPage): + for i in range(10): + s = f"ToDo{i}" + page.new_todo(s) + for i in range(4): + s = f"ToDo{i}" + page.toggle_todo(s) + + page.view_all_todo() + assert page.count_todo_items_left() == page.build_count_todo_left(6) + assert page.todo_count() == 10 + + page.view_active_todo() + assert page.count_todo_items_left() == page.build_count_todo_left(6) + assert page.todo_count() == 6 + + page.view_completed_todo() + assert page.count_todo_items_left() == page.build_count_todo_left(6) + assert page.todo_count() == 4 +``` \ No newline at end of file From 71a5f39629ead50818b94976aa34769f72bf0e8e Mon Sep 17 00:00:00 2001 From: Peter Song Date: Thu, 26 Sep 2024 18:47:41 +0800 Subject: [PATCH 6/8] Update design_strategies.ja.md --- .../test_practices/design_strategies.ja.md | 286 +++++++++++++++++- 1 file changed, 285 insertions(+), 1 deletion(-) diff --git a/website_and_docs/content/documentation/test_practices/design_strategies.ja.md b/website_and_docs/content/documentation/test_practices/design_strategies.ja.md index 5ef10220466c..861dab630904 100644 --- a/website_and_docs/content/documentation/test_practices/design_strategies.ja.md +++ b/website_and_docs/content/documentation/test_practices/design_strategies.ja.md @@ -367,4 +367,288 @@ public class ActionBot { } ``` -これらの抽象化が構築され、テストでの重複が特定されると、ボットの上にPageObjectsを階層化することができます。 \ No newline at end of file +これらの抽象化が構築され、テストでの重複が特定されると、ボットの上にPageObjectsを階層化することができます。 + +## Example + +**Action Bot**、**Loadable Component**、および **Page Object** を実装した `python + pytest + selenium` の例です。 + +A `pytest` fixture `chrome_driver`. + +```python +import pytest +from selenium import webdriver +from selenium.common import ( + ElementNotInteractableException, + NoSuchElementException, + StaleElementReferenceException, +) +from selenium.webdriver import ActionChains +from selenium.webdriver.common.by import By +from selenium.webdriver.remote.webelement import WebElement +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait + + +@pytest.fixture(scope="function") +def chrome_driver(): + with webdriver.Chrome() as driver: + driver.set_window_size(1024, 768) + driver.implicitly_wait(0.5) + yield driver +``` + +"**Action Bot**" implementation. + +```python +class ActionBot: + def __init__(self, driver) -> None: + self.driver = driver + self.wait = WebDriverWait( + driver, + timeout=10, + poll_frequency=2, + ignored_exceptions=[ + NoSuchElementException, + StaleElementReferenceException, + ElementNotInteractableException, + ], + ) + + def element(self, locator: tuple) -> WebElement: + self.wait.until(lambda driver: driver.find_element(*locator)) + return self.driver.find_element(*locator) + + def elements(self, locator: tuple) -> list[WebElement]: + return self.driver.find_elements(*locator) + + def hover(self, locator: tuple) -> None: + element = self.element(locator) + ActionChains(self.driver).move_to_element(element).perform() + + def click(self, locator: tuple) -> None: + element = self.element(locator) + element.click() + + def type(self, locator: tuple, value: str) -> None: + element = self.element(locator) + element.clear() + element.send_keys(value) + + def text(self, locator: tuple) -> str: + element = self.element(locator) + return element.text +``` + +"**Loadable Component** definition. + +```python +class LoadableComponent: + def load(self): + raise NotImplementedError("Subclasses must implement this method") + + def is_loaded(self): + raise NotImplementedError("Subclasses must implement this method") + + def get(self): + if not self.is_loaded(): + self.load() + if not self.is_loaded(): + raise Exception("Page not loaded properly.") + return self +``` + +"**Loadable Component** and **Page Object**" implementation. + +```python +class TodoPage(LoadableComponent): + url = "https://todomvc.com/examples/react/dist/" + + new_todo_by = (By.CSS_SELECTOR, "input.new-todo") + count_todo_left_by = (By.CSS_SELECTOR, "span.todo-count") + todo_items_by = (By.CSS_SELECTOR, "ul.todo-list>li") + + view_all_by = (By.LINK_TEXT, "All") + view_active_by = (By.LINK_TEXT, "Active") + view_completed_by = (By.LINK_TEXT, "Completed") + + toggle_all_by = (By.CSS_SELECTOR, "input.toggle-all") + clear_completed_by = (By.CSS_SELECTOR, "button.clear-completed") + + @staticmethod + def build_todo_by(s: str) -> tuple: + p = f"//li[.//label[contains(text(), '{s}')]]" + return By.XPATH, p + + @staticmethod + def build_todo_item_label_by(s: str) -> tuple: + p = f"//label[contains(text(), '{s}')]" + return By.XPATH, p + + @staticmethod + def build_todo_item_toggle_by(s: str) -> tuple: + by, using = TodoPage.build_todo_item_label_by(s) + p = f"{using}/../input[@class='toggle']" + return by, p + + @staticmethod + def build_todo_item_delete_by(s: str) -> tuple: + by, using = TodoPage.build_todo_item_label_by(s) + p = f"{using}/../button[@class='destroy']" + return by, p + + def build_count_todo_left(self, count: int) -> str: + if count == 1: + return "1 item left!" + else: + return f"{count} items left!" + + def __init__(self, driver): + self.driver = driver + self.bot = ActionBot(driver) + + def load(self): + self.driver.get(self.url) + + def is_loaded(self): + try: + WebDriverWait(self.driver, 10).until( + EC.visibility_of_element_located(self.new_todo_by) + ) + return True + except: + return False + + # business domain below + def count_todo_items_left(self) -> str: + return self.bot.text(self.count_todo_left_by) + + def todo_count(self) -> int: + return len(self.bot.elements(self.todo_items_by)) + + def new_todo(self, s: str): + self.bot.type(self.new_todo_by, s + "\n") + + def toggle_todo(self, s: str): + self.bot.click(self.build_todo_item_toggle_by(s)) + + def hover_todo(self, s: str) -> None: + self.bot.hover(self.build_todo_by(s)) + + def delete_todo(self, s: str): + self.hover_todo(s) + self.bot.click(self.build_todo_item_delete_by(s)) + + def clear_completed_todo(self): + self.bot.click(self.clear_completed_by) + + def toggle_all_todo(self): + self.bot.click(self.toggle_all_by) + + def view_all_todo(self): + self.bot.click(self.view_all_by) + + def view_active_todo(self): + self.bot.click(self.view_active_by) + + def view_completed_todo(self): + self.bot.click(self.view_completed_by) +``` + +Test cases implementation with `pytest`. + +```python +@pytest.fixture +def page(chrome_driver) -> TodoPage: + driver = chrome_driver + return TodoPage(driver).get() + + +class TestTodoPage: + def test_new_todo(self, page: TodoPage): + assert page.todo_count() == 0 + page.new_todo("aaa") + assert page.count_todo_items_left() == page.build_count_todo_left(1) + + def test_todo_toggle(self, page: TodoPage): + s = "aaa" + page.new_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(1) + + page.toggle_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(0) + + page.toggle_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(1) + + def test_todo_delete(self, page: TodoPage): + s1 = "aaa" + s2 = "bbb" + page.new_todo(s1) + page.new_todo(s2) + assert page.count_todo_items_left() == page.build_count_todo_left(2) + + page.delete_todo(s1) + assert page.count_todo_items_left() == page.build_count_todo_left(1) + + page.delete_todo(s2) + assert page.todo_count() == 0 + + def test_new_100_todo(self, page: TodoPage): + for i in range(100): + s = f"ToDo{i}" + page.new_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(100) + + def test_toggle_all_todo(self, page: TodoPage): + for i in range(10): + s = f"ToDo{i}" + page.new_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(10) + assert page.todo_count() == 10 + + page.toggle_all_todo() + assert page.count_todo_items_left() == page.build_count_todo_left(0) + assert page.todo_count() == 10 + + page.toggle_all_todo() + assert page.count_todo_items_left() == page.build_count_todo_left(10) + assert page.todo_count() == 10 + + def test_clear_completed_todo(self, page: TodoPage): + for i in range(10): + s = f"ToDo{i}" + page.new_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(10) + assert page.todo_count() == 10 + + for i in range(5): + s = f"ToDo{i}" + page.toggle_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(5) + assert page.todo_count() == 10 + + page.clear_completed_todo() + assert page.count_todo_items_left() == page.build_count_todo_left(5) + assert page.todo_count() == 5 + + def test_view_todo(self, page: TodoPage): + for i in range(10): + s = f"ToDo{i}" + page.new_todo(s) + for i in range(4): + s = f"ToDo{i}" + page.toggle_todo(s) + + page.view_all_todo() + assert page.count_todo_items_left() == page.build_count_todo_left(6) + assert page.todo_count() == 10 + + page.view_active_todo() + assert page.count_todo_items_left() == page.build_count_todo_left(6) + assert page.todo_count() == 6 + + page.view_completed_todo() + assert page.count_todo_items_left() == page.build_count_todo_left(6) + assert page.todo_count() == 4 +``` \ No newline at end of file From 4fa310f2a2ce17910dd8c5957800f46a04975b32 Mon Sep 17 00:00:00 2001 From: Peter Song Date: Thu, 26 Sep 2024 18:48:11 +0800 Subject: [PATCH 7/8] Update design_strategies.pt-br.md --- .../test_practices/design_strategies.pt-br.md | 286 ++++++++++++++++++ 1 file changed, 286 insertions(+) diff --git a/website_and_docs/content/documentation/test_practices/design_strategies.pt-br.md b/website_and_docs/content/documentation/test_practices/design_strategies.pt-br.md index d2174f34adaf..5f7eb146f524 100644 --- a/website_and_docs/content/documentation/test_practices/design_strategies.pt-br.md +++ b/website_and_docs/content/documentation/test_practices/design_strategies.pt-br.md @@ -375,3 +375,289 @@ public class ActionBot { ``` Once these abstractions have been built and duplication in your tests identified, it's possible to layer PageObjects on top of bots. + + +## Example + +An example of `python + pytest + selenium` +which implemented "**Action Bot**, **Loadable Component** and **Page Object**". + +A `pytest` fixture `chrome_driver`. + +```python +import pytest +from selenium import webdriver +from selenium.common import ( + ElementNotInteractableException, + NoSuchElementException, + StaleElementReferenceException, +) +from selenium.webdriver import ActionChains +from selenium.webdriver.common.by import By +from selenium.webdriver.remote.webelement import WebElement +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait + + +@pytest.fixture(scope="function") +def chrome_driver(): + with webdriver.Chrome() as driver: + driver.set_window_size(1024, 768) + driver.implicitly_wait(0.5) + yield driver +``` + +"**Action Bot**" implementation. + +```python +class ActionBot: + def __init__(self, driver) -> None: + self.driver = driver + self.wait = WebDriverWait( + driver, + timeout=10, + poll_frequency=2, + ignored_exceptions=[ + NoSuchElementException, + StaleElementReferenceException, + ElementNotInteractableException, + ], + ) + + def element(self, locator: tuple) -> WebElement: + self.wait.until(lambda driver: driver.find_element(*locator)) + return self.driver.find_element(*locator) + + def elements(self, locator: tuple) -> list[WebElement]: + return self.driver.find_elements(*locator) + + def hover(self, locator: tuple) -> None: + element = self.element(locator) + ActionChains(self.driver).move_to_element(element).perform() + + def click(self, locator: tuple) -> None: + element = self.element(locator) + element.click() + + def type(self, locator: tuple, value: str) -> None: + element = self.element(locator) + element.clear() + element.send_keys(value) + + def text(self, locator: tuple) -> str: + element = self.element(locator) + return element.text +``` + +"**Loadable Component** definition. + +```python +class LoadableComponent: + def load(self): + raise NotImplementedError("Subclasses must implement this method") + + def is_loaded(self): + raise NotImplementedError("Subclasses must implement this method") + + def get(self): + if not self.is_loaded(): + self.load() + if not self.is_loaded(): + raise Exception("Page not loaded properly.") + return self +``` + +"**Loadable Component** and **Page Object**" implementation. + +```python +class TodoPage(LoadableComponent): + url = "https://todomvc.com/examples/react/dist/" + + new_todo_by = (By.CSS_SELECTOR, "input.new-todo") + count_todo_left_by = (By.CSS_SELECTOR, "span.todo-count") + todo_items_by = (By.CSS_SELECTOR, "ul.todo-list>li") + + view_all_by = (By.LINK_TEXT, "All") + view_active_by = (By.LINK_TEXT, "Active") + view_completed_by = (By.LINK_TEXT, "Completed") + + toggle_all_by = (By.CSS_SELECTOR, "input.toggle-all") + clear_completed_by = (By.CSS_SELECTOR, "button.clear-completed") + + @staticmethod + def build_todo_by(s: str) -> tuple: + p = f"//li[.//label[contains(text(), '{s}')]]" + return By.XPATH, p + + @staticmethod + def build_todo_item_label_by(s: str) -> tuple: + p = f"//label[contains(text(), '{s}')]" + return By.XPATH, p + + @staticmethod + def build_todo_item_toggle_by(s: str) -> tuple: + by, using = TodoPage.build_todo_item_label_by(s) + p = f"{using}/../input[@class='toggle']" + return by, p + + @staticmethod + def build_todo_item_delete_by(s: str) -> tuple: + by, using = TodoPage.build_todo_item_label_by(s) + p = f"{using}/../button[@class='destroy']" + return by, p + + def build_count_todo_left(self, count: int) -> str: + if count == 1: + return "1 item left!" + else: + return f"{count} items left!" + + def __init__(self, driver): + self.driver = driver + self.bot = ActionBot(driver) + + def load(self): + self.driver.get(self.url) + + def is_loaded(self): + try: + WebDriverWait(self.driver, 10).until( + EC.visibility_of_element_located(self.new_todo_by) + ) + return True + except: + return False + + # business domain below + def count_todo_items_left(self) -> str: + return self.bot.text(self.count_todo_left_by) + + def todo_count(self) -> int: + return len(self.bot.elements(self.todo_items_by)) + + def new_todo(self, s: str): + self.bot.type(self.new_todo_by, s + "\n") + + def toggle_todo(self, s: str): + self.bot.click(self.build_todo_item_toggle_by(s)) + + def hover_todo(self, s: str) -> None: + self.bot.hover(self.build_todo_by(s)) + + def delete_todo(self, s: str): + self.hover_todo(s) + self.bot.click(self.build_todo_item_delete_by(s)) + + def clear_completed_todo(self): + self.bot.click(self.clear_completed_by) + + def toggle_all_todo(self): + self.bot.click(self.toggle_all_by) + + def view_all_todo(self): + self.bot.click(self.view_all_by) + + def view_active_todo(self): + self.bot.click(self.view_active_by) + + def view_completed_todo(self): + self.bot.click(self.view_completed_by) +``` + +Test cases implementation with `pytest`. + +```python +@pytest.fixture +def page(chrome_driver) -> TodoPage: + driver = chrome_driver + return TodoPage(driver).get() + + +class TestTodoPage: + def test_new_todo(self, page: TodoPage): + assert page.todo_count() == 0 + page.new_todo("aaa") + assert page.count_todo_items_left() == page.build_count_todo_left(1) + + def test_todo_toggle(self, page: TodoPage): + s = "aaa" + page.new_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(1) + + page.toggle_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(0) + + page.toggle_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(1) + + def test_todo_delete(self, page: TodoPage): + s1 = "aaa" + s2 = "bbb" + page.new_todo(s1) + page.new_todo(s2) + assert page.count_todo_items_left() == page.build_count_todo_left(2) + + page.delete_todo(s1) + assert page.count_todo_items_left() == page.build_count_todo_left(1) + + page.delete_todo(s2) + assert page.todo_count() == 0 + + def test_new_100_todo(self, page: TodoPage): + for i in range(100): + s = f"ToDo{i}" + page.new_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(100) + + def test_toggle_all_todo(self, page: TodoPage): + for i in range(10): + s = f"ToDo{i}" + page.new_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(10) + assert page.todo_count() == 10 + + page.toggle_all_todo() + assert page.count_todo_items_left() == page.build_count_todo_left(0) + assert page.todo_count() == 10 + + page.toggle_all_todo() + assert page.count_todo_items_left() == page.build_count_todo_left(10) + assert page.todo_count() == 10 + + def test_clear_completed_todo(self, page: TodoPage): + for i in range(10): + s = f"ToDo{i}" + page.new_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(10) + assert page.todo_count() == 10 + + for i in range(5): + s = f"ToDo{i}" + page.toggle_todo(s) + assert page.count_todo_items_left() == page.build_count_todo_left(5) + assert page.todo_count() == 10 + + page.clear_completed_todo() + assert page.count_todo_items_left() == page.build_count_todo_left(5) + assert page.todo_count() == 5 + + def test_view_todo(self, page: TodoPage): + for i in range(10): + s = f"ToDo{i}" + page.new_todo(s) + for i in range(4): + s = f"ToDo{i}" + page.toggle_todo(s) + + page.view_all_todo() + assert page.count_todo_items_left() == page.build_count_todo_left(6) + assert page.todo_count() == 10 + + page.view_active_todo() + assert page.count_todo_items_left() == page.build_count_todo_left(6) + assert page.todo_count() == 6 + + page.view_completed_todo() + assert page.count_todo_items_left() == page.build_count_todo_left(6) + assert page.todo_count() == 4 +``` \ No newline at end of file From 2e94b422b2f085336d068fa53e6654823c5b537f Mon Sep 17 00:00:00 2001 From: Peter Song Date: Thu, 26 Sep 2024 18:49:59 +0800 Subject: [PATCH 8/8] small update --- .../documentation/test_practices/design_strategies.en.md | 3 +-- .../documentation/test_practices/design_strategies.pt-br.md | 3 +-- .../documentation/test_practices/design_strategies.zh-cn.md | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/website_and_docs/content/documentation/test_practices/design_strategies.en.md b/website_and_docs/content/documentation/test_practices/design_strategies.en.md index 2934d25ff94f..acdd3cf71ecc 100644 --- a/website_and_docs/content/documentation/test_practices/design_strategies.en.md +++ b/website_and_docs/content/documentation/test_practices/design_strategies.en.md @@ -378,8 +378,7 @@ Once these abstractions have been built and duplication in your tests identified ## Example -An example of `python + pytest + selenium` -which implemented "**Action Bot**, **Loadable Component** and **Page Object**". +An example of `python + pytest + selenium` which implemented "**Action Bot**, **Loadable Component** and **Page Object**". A `pytest` fixture `chrome_driver`. diff --git a/website_and_docs/content/documentation/test_practices/design_strategies.pt-br.md b/website_and_docs/content/documentation/test_practices/design_strategies.pt-br.md index 5f7eb146f524..824e81669b0f 100644 --- a/website_and_docs/content/documentation/test_practices/design_strategies.pt-br.md +++ b/website_and_docs/content/documentation/test_practices/design_strategies.pt-br.md @@ -379,8 +379,7 @@ Once these abstractions have been built and duplication in your tests identified ## Example -An example of `python + pytest + selenium` -which implemented "**Action Bot**, **Loadable Component** and **Page Object**". +An example of `python + pytest + selenium` which implemented "**Action Bot**, **Loadable Component** and **Page Object**". A `pytest` fixture `chrome_driver`. diff --git a/website_and_docs/content/documentation/test_practices/design_strategies.zh-cn.md b/website_and_docs/content/documentation/test_practices/design_strategies.zh-cn.md index eaa1c084b314..f138b73cec96 100644 --- a/website_and_docs/content/documentation/test_practices/design_strategies.zh-cn.md +++ b/website_and_docs/content/documentation/test_practices/design_strategies.zh-cn.md @@ -393,8 +393,7 @@ identified, it's possible to layer PageObjects on top of bots. ## Example -一个用例 使用 `python + pytest + selenium` 实现了设计策略 "**Action Bot**, **Loadable Component** -和 **Page Object**". +一个用例 使用 `python + pytest + selenium` 实现了设计策略 "**Action Bot**, **Loadable Component** 和 **Page Object**". A `pytest` fixture `chrome_driver`.