diff --git a/examples/java/src/test/java/dev/selenium/design_strategies/ActionBot.java b/examples/java/src/test/java/dev/selenium/design_strategies/ActionBot.java new file mode 100644 index 00000000000..30b0d8d2880 --- /dev/null +++ b/examples/java/src/test/java/dev/selenium/design_strategies/ActionBot.java @@ -0,0 +1,30 @@ +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; + +public class ActionBot { + private final WebDriver driver; + + public ActionBot(WebDriver driver) { + this.driver = driver; + } + + public void click(By locator) { + driver.findElement(locator).click(); + } + + public void submit(By locator) { + driver.findElement(locator).submit(); + } + + /** + * Type something into an input field. WebDriver doesn't normally clear these + * before typing, so this method does that first. It also sends a return key + * to move the focus out of the element. + */ + public void type(By locator, String text) { + WebElement element = driver.findElement(locator); + element.clear(); + element.sendKeys(text + "\n"); + } +} \ No newline at end of file diff --git a/examples/java/src/test/java/dev/selenium/design_strategies/EditIssue.java b/examples/java/src/test/java/dev/selenium/design_strategies/EditIssue.java new file mode 100644 index 00000000000..0e601a01756 --- /dev/null +++ b/examples/java/src/test/java/dev/selenium/design_strategies/EditIssue.java @@ -0,0 +1,69 @@ +package com.example.webdriver; + +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; + +public class EditIssue { + + private final WebDriver driver; + + public EditIssue(WebDriver driver) { + this.driver = driver; + } + + public void setTitle(String title) { + WebElement field = driver.findElement(By.id("issue_title")); + clearAndType(field, title); + } + + public void setBody(String body) { + WebElement field = driver.findElement(By.id("issue_body")); + clearAndType(field, body); + } + + public void setHowToReproduce(String howToReproduce) { + WebElement field = driver.findElement(By.id("issue_form_repro-command")); + clearAndType(field, howToReproduce); + } + + public void setLogOutput(String logOutput) { + WebElement field = driver.findElement(By.id("issue_form_logs")); + clearAndType(field, logOutput); + } + + public void setOperatingSystem(String operatingSystem) { + WebElement field = driver.findElement(By.id("issue_form_operating-system")); + clearAndType(field, operatingSystem); + } + + public void setSeleniumVersion(String seleniumVersion) { + WebElement field = driver.findElement(By.id("issue_form_selenium-version")); + clearAndType(field, seleniumVersion); + } + + public void setBrowserVersion(String browserVersion) { + WebElement field = driver.findElement(By.id("issue_form_browser-versions")); + clearAndType(field, browserVersion); + } + + public void setDriverVersion(String driverVersion) { + WebElement field = driver.findElement(By.id("issue_form_browser-driver-versions")); + clearAndType(field, driverVersion); + } + + public void setUsingGrid(String usingGrid) { + WebElement field = driver.findElement(By.id("issue_form_selenium-grid-version")); + clearAndType(field, usingGrid); + } + + // public IssueList submit() { + // driver.findElement(By.cssSelector("button[type='submit']")).click(); + // return new IssueList(driver); + // } + + private void clearAndType(WebElement field, String text) { + field.clear(); + field.sendKeys(text); + } +} diff --git a/examples/java/src/test/java/dev/selenium/design_strategies/EditIssueBetter.java b/examples/java/src/test/java/dev/selenium/design_strategies/EditIssueBetter.java new file mode 100644 index 00000000000..2b0600baeed --- /dev/null +++ b/examples/java/src/test/java/dev/selenium/design_strategies/EditIssueBetter.java @@ -0,0 +1,96 @@ +package com.example.webdriver; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; +import org.openqa.selenium.support.PageFactory; + +import org.junit.jupiter.api.Assertions; + +public class EditIssueBetter extends EditIssue { + + private final WebDriver driver; + + // By default the PageFactory will locate elements with the same name or id + // as the field. Since the issue_title element has an id attribute of "issue_title" + // we don't need any additional annotations. + private WebElement issue_title; + + // But we'd prefer a different name in our code than "issue_body", so we use the + // FindBy annotation to tell the PageFactory how to locate the element. + @FindBy(id = "issue_body") private WebElement body; + + public EditIssueBetter(WebDriver driver) { + super(driver); + + // This call sets the WebElement fields. + PageFactory.initElements(driver, this); + } + + @Override + protected void load() { + driver.get("https://github.com/SeleniumHQ/selenium/issues/new?assignees=&labels=I-defect%2Cneeds-triaging&projects=&template=bug-report.yml&title=%5B%F0%9F%90%9B+Bug%5D%3A+"); + } + + @Override + protected void isLoaded() throws Error { + String url = driver.getCurrentUrl(); + Assertions.assertTrue("Not on the issue entry page: " + url, url.endsWith("/new")); + } + + public void setHowToReproduce(String howToReproduce) { + WebElement field = driver.findElement(By.id("issue_form_repro-command")); + clearAndType(field, howToReproduce); + } + + public void setLogOutput(String logOutput) { + WebElement field = driver.findElement(By.id("issue_form_logs")); + clearAndType(field, logOutput); + } + + public void setOperatingSystem(String operatingSystem) { + WebElement field = driver.findElement(By.id("issue_form_operating-system")); + clearAndType(field, operatingSystem); + } + + public void setSeleniumVersion(String seleniumVersion) { + WebElement field = driver.findElement(By.id("issue_form_selenium-version")); + clearAndType(field, seleniumVersion); + } + + public void setBrowserVersion(String browserVersion) { + WebElement field = driver.findElement(By.id("issue_form_browser-versions")); + clearAndType(field, browserVersion); + } + + public void setDriverVersion(String driverVersion) { + WebElement field = driver.findElement(By.id("issue_form_browser-driver-versions")); + clearAndType(field, driverVersion); + } + + public void setUsingGrid(String usingGrid) { + WebElement field = driver.findElement(By.id("issue_form_selenium-grid-version")); + clearAndType(field, usingGrid); + } + + // public IssueList submit() { + // driver.findElement(By.cssSelector("button[type='submit']")).click(); + // return new IssueList(driver); + // } + + private void clearAndType(WebElement field, String text) { + field.clear(); + field.sendKeys(text); + } +} + +// EditIssueBetter page = new EditIssueBetter(driver).get(); + +// Further Updates + + // @Override + // protected void load() { + // securedPage.get(); + + // driver.get("https://github.com/SeleniumHQ/selenium/issues/new?assignees=&labels=I-defect%2Cneeds-triaging&projects=&template=bug-report.yml&title=%5B%F0%9F%90%9B+Bug%5D%3A+"); + // } diff --git a/examples/java/src/test/java/dev/selenium/design_strategies/LoadableComponent.java b/examples/java/src/test/java/dev/selenium/design_strategies/LoadableComponent.java new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/java/src/test/java/dev/selenium/design_strategies/PageObjectTests.java b/examples/java/src/test/java/dev/selenium/design_strategies/PageObjectTests.java new file mode 100644 index 00000000000..73e082c744c --- /dev/null +++ b/examples/java/src/test/java/dev/selenium/design_strategies/PageObjectTests.java @@ -0,0 +1,30 @@ +// import org.junit.jupiter.api.Test; + +// public class FooTest { +// private EditIssue editIssue; + +// @Before +// public void prepareComponents() { +// WebDriver driver = new FirefoxDriver(); + +// ProjectPage project = new ProjectPage(driver, "selenium"); +// SecuredPage securedPage = new SecuredPage(driver, project, "example", "top secret"); +// editIssue = new EditIssue(driver, securedPage); +// } + +// @Test +// public void demonstrateNestedLoadableComponents() { +// editIssue.get(); + +// editIssue.title.sendKeys('Title'); +// editIssue.body.sendKeys('What Happened'); +// editIssue.setHowToReproduce('How to Reproduce'); +// editIssue.setLogOutput('Log Output'); +// editIssue.setOperatingSystem('Operating System'); +// editIssue.setSeleniumVersion('Selenium Version'); +// editIssue.setBrowserVersion('Browser Version'); +// editIssue.setDriverVersion('Driver Version'); +// editIssue.setUsingGrid('I Am Using Grid'); +// } + +// } diff --git a/examples/java/src/test/java/dev/selenium/design_strategies/ProjectPage.java b/examples/java/src/test/java/dev/selenium/design_strategies/ProjectPage.java new file mode 100644 index 00000000000..c56f9c68478 --- /dev/null +++ b/examples/java/src/test/java/dev/selenium/design_strategies/ProjectPage.java @@ -0,0 +1,28 @@ +package com.example.webdriver; + +import org.openqa.selenium.WebDriver; + +import org.junit.jupiter.api.Assertions; + +public class ProjectPage { + + private final WebDriver driver; + private final String projectName; + + public ProjectPage(WebDriver driver, String projectName) { + this.driver = driver; + this.projectName = projectName; + } + + + protected void load() { + driver.get("http://" + projectName + ".googlecode.com/"); + } + + + protected void isLoaded() throws Error { + String url = driver.getCurrentUrl(); + + Assertions.assertTrue(url.contains(projectName)); + } +} \ No newline at end of file diff --git a/examples/java/src/test/java/dev/selenium/design_strategies/SecuredPage.java b/examples/java/src/test/java/dev/selenium/design_strategies/SecuredPage.java new file mode 100644 index 00000000000..f79386cb289 --- /dev/null +++ b/examples/java/src/test/java/dev/selenium/design_strategies/SecuredPage.java @@ -0,0 +1,52 @@ +package com.example.webdriver; + +import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; + + + +public class SecuredPage{ + + private final WebDriver driver; + // private final LoadableComponent parent; + private final String username; + private final String password; + + public SecuredPage(WebDriver driver, String username, String password) { + this.driver = driver; + this.parent = parent; + this.username = username; + this.password = password; + } + + + protected void load() { + parent.get(); + + String originalUrl = driver.getCurrentUrl(); + + // Sign in + driver.get("https://www.google.com/accounts/ServiceLogin?service=code"); + driver.findElement(By.name("Email")).sendKeys(username); + WebElement passwordField = driver.findElement(By.name("Passwd")); + passwordField.sendKeys(password); + passwordField.submit(); + + // Now return to the original URL + driver.get(originalUrl); + } + + + protected void isLoaded() throws Error { + // If you're signed in, you have the option of picking a different login. + // Let's check for the presence of that. + + try { + WebElement div = driver.findElement(By.id("multilogin-dropdown")); + } catch (NoSuchElementException e) { + fail("Cannot locate user name link"); + } + } +} \ No newline at end of file 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 517b7f0e20a..4a0947c85c6 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 @@ -37,103 +37,75 @@ As an example of a UI that we'd like to model, take a look at the [new issue](https://github.com/SeleniumHQ/selenium/issues/new?assignees=&labels=I-defect%2Cneeds-triaging&projects=&template=bug-report.yml&title=%5B%F0%9F%90%9B+Bug%5D%3A+) page. From the point of view of a test author, this offers the service of being able to file a new issue. A basic Page Object would look like: -```java -package com.example.webdriver; - -import org.openqa.selenium.By; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; - -public class EditIssue { - - private final WebDriver driver; - - public EditIssue(WebDriver driver) { - this.driver = driver; - } - - public void setTitle(String title) { - WebElement field = driver.findElement(By.id("issue_title"))); - clearAndType(field, title); - } - - public void setBody(String body) { - WebElement field = driver.findElement(By.id("issue_body")); - clearAndType(field, body); - } - - public void setHowToReproduce(String howToReproduce) { - WebElement field = driver.findElement(By.id("issue_form_repro-command")); - clearAndType(field, howToReproduce); - } - - public void setLogOutput(String logOutput) { - WebElement field = driver.findElement(By.id("issue_form_logs")); - clearAndType(field, logOutput); - } - - public void setOperatingSystem(String operatingSystem) { - WebElement field = driver.findElement(By.id("issue_form_operating-system")); - clearAndType(field, operatingSystem); - } - - public void setSeleniumVersion(String seleniumVersion) { - WebElement field = driver.findElement(By.id("issue_form_selenium-version")); - clearAndType(field, logOutput); - } - - public void setBrowserVersion(String browserVersion) { - WebElement field = driver.findElement(By.id("issue_form_browser-versions")); - clearAndType(field, browserVersion); - } - - public void setDriverVersion(String driverVersion) { - WebElement field = driver.findElement(By.id("issue_form_browser-driver-versions")); - clearAndType(field, driverVersion); - } - - public void setUsingGrid(String usingGrid) { - WebElement field = driver.findElement(By.id("issue_form_selenium-grid-version")); - clearAndType(field, usingGrid); - } - - public IssueList submit() { - driver.findElement(By.cssSelector("button[type='submit']")).click(); - return new IssueList(driver); - } - - private void clearAndType(WebElement field, String text) { - field.clear(); - field.sendKeys(text); - } -} -``` +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/EditIssue.java#L1-L69" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} In order to turn this into a LoadableComponent, all we need to do is to set that as the base type: -```java -public class EditIssue extends LoadableComponent { - // rest of class ignored for now -} -``` +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/EditIssueBetter.java#L10" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} This signature looks a little unusual, but all it means is that this class represents a LoadableComponent that loads the EditIssue page. By extending this base class, we need to implement two new methods: -```java - @Override - protected void load() { - driver.get("https://github.com/SeleniumHQ/selenium/issues/new?assignees=&labels=I-defect%2Cneeds-triaging&projects=&template=bug-report.yml&title=%5B%F0%9F%90%9B+Bug%5D%3A+"); - } - - @Override - protected void isLoaded() throws Error { - String url = driver.getCurrentUrl(); - assertTrue("Not on the issue entry page: " + url, url.endsWith("/new")); - } -``` +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/EditIssueBetter.java#L30-L39" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} The `load` method is used to navigate to the page, whilst the `isLoaded` method is used to determine whether we are on the right page. Although the method looks like it should return @@ -143,102 +115,51 @@ it's possible to give users of the class clear information that can be used to d With a little rework, our PageObject looks like: -```java -package com.example.webdriver; -import org.openqa.selenium.By; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.FindBy; -import org.openqa.selenium.support.PageFactory; - -import static junit.framework.Assert.assertTrue; - -public class EditIssue extends LoadableComponent { - - private final WebDriver driver; - - // By default the PageFactory will locate elements with the same name or id - // as the field. Since the issue_title element has an id attribute of "issue_title" - // we don't need any additional annotations. - private WebElement issue_title; - - // But we'd prefer a different name in our code than "issue_body", so we use the - // FindBy annotation to tell the PageFactory how to locate the element. - @FindBy(id = "issue_body") private WebElement body; - - public EditIssue(WebDriver driver) { - this.driver = driver; - - // This call sets the WebElement fields. - PageFactory.initElements(driver, this); - } - - @Override - protected void load() { - driver.get("https://github.com/SeleniumHQ/selenium/issues/new?assignees=&labels=I-defect%2Cneeds-triaging&projects=&template=bug-report.yml&title=%5B%F0%9F%90%9B+Bug%5D%3A+"); - } - - @Override - protected void isLoaded() throws Error { - String url = driver.getCurrentUrl(); - assertTrue("Not on the issue entry page: " + url, url.endsWith("/new")); - } - - public void setHowToReproduce(String howToReproduce) { - WebElement field = driver.findElement(By.id("issue_form_repro-command")); - clearAndType(field, howToReproduce); - } - - public void setLogOutput(String logOutput) { - WebElement field = driver.findElement(By.id("issue_form_logs")); - clearAndType(field, logOutput); - } - - public void setOperatingSystem(String operatingSystem) { - WebElement field = driver.findElement(By.id("issue_form_operating-system")); - clearAndType(field, operatingSystem); - } - - public void setSeleniumVersion(String seleniumVersion) { - WebElement field = driver.findElement(By.id("issue_form_selenium-version")); - clearAndType(field, logOutput); - } - - public void setBrowserVersion(String browserVersion) { - WebElement field = driver.findElement(By.id("issue_form_browser-versions")); - clearAndType(field, browserVersion); - } - - public void setDriverVersion(String driverVersion) { - WebElement field = driver.findElement(By.id("issue_form_browser-driver-versions")); - clearAndType(field, driverVersion); - } - - public void setUsingGrid(String usingGrid) { - WebElement field = driver.findElement(By.id("issue_form_selenium-grid-version")); - clearAndType(field, usingGrid); - } - - public IssueList submit() { - driver.findElement(By.cssSelector("button[type='submit']")).click(); - return new IssueList(driver); - } - - private void clearAndType(WebElement field, String text) { - field.clear(); - field.sendKeys(text); - } -} - -``` +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/EditIssueBetter.java#L1-L85" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} That doesn't seem to have bought us much, right? One thing it has done is encapsulate the information about how to navigate to the page into the page itself, meaning that this information's not scattered through the code base. It also means that we can do this in our tests: -```java -EditIssue page = new EditIssue(driver).get(); -``` +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/EditIssueBetter.java#L87" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} This call will cause the driver to navigate to the page if that's necessary. @@ -260,141 +181,81 @@ What would this look like in code? For a start, each logical component would have its own class. The "load" method in each of them would "get" the parent. The end result, in addition to the EditIssue class above is: -ProjectPage.java: - -```java -package com.example.webdriver; - -import org.openqa.selenium.WebDriver; - -import static org.junit.Assert.assertTrue; - -public class ProjectPage extends LoadableComponent { - - private final WebDriver driver; - private final String projectName; - - public ProjectPage(WebDriver driver, String projectName) { - this.driver = driver; - this.projectName = projectName; - } - - @Override - protected void load() { - driver.get("http://" + projectName + ".googlecode.com/"); - } +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} - @Override - protected void isLoaded() throws Error { - String url = driver.getCurrentUrl(); +ProjectPage.java: - assertTrue(url.contains(projectName)); - } -} -``` +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/ProjectPage.java#L1-L38" >}} and SecuredPage.java: -```java -package com.example.webdriver; - -import org.openqa.selenium.By; -import org.openqa.selenium.NoSuchElementException; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; - -import static org.junit.Assert.fail; - -public class SecuredPage extends LoadableComponent { - - private final WebDriver driver; - private final LoadableComponent parent; - private final String username; - private final String password; - - public SecuredPage(WebDriver driver, LoadableComponent parent, String username, String password) { - this.driver = driver; - this.parent = parent; - this.username = username; - this.password = password; - } - - @Override - protected void load() { - parent.get(); - - String originalUrl = driver.getCurrentUrl(); - - // Sign in - driver.get("https://www.google.com/accounts/ServiceLogin?service=code"); - driver.findElement(By.name("Email")).sendKeys(username); - WebElement passwordField = driver.findElement(By.name("Passwd")); - passwordField.sendKeys(password); - passwordField.submit(); - - // Now return to the original URL - driver.get(originalUrl); - } - - @Override - protected void isLoaded() throws Error { - // If you're signed in, you have the option of picking a different login. - // Let's check for the presence of that. - - try { - WebElement div = driver.findElement(By.id("multilogin-dropdown")); - } catch (NoSuchElementException e) { - fail("Cannot locate user name link"); - } - } -} -``` +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/SecuredPage.java#L1-L52" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} The "load" method in EditIssue now looks like: -```java - @Override - protected void load() { - securedPage.get(); - - driver.get("https://github.com/SeleniumHQ/selenium/issues/new?assignees=&labels=I-defect%2Cneeds-triaging&projects=&template=bug-report.yml&title=%5B%F0%9F%90%9B+Bug%5D%3A+"); - } -``` +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/EditIssueBetter.java#L91-L96" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} This shows that the components are all "nested" within each other. A call to `get()` in EditIssue will cause all its dependencies to load too. The example usage: -```java -public class FooTest { - private EditIssue editIssue; - - @Before - public void prepareComponents() { - WebDriver driver = new FirefoxDriver(); - - ProjectPage project = new ProjectPage(driver, "selenium"); - SecuredPage securedPage = new SecuredPage(driver, project, "example", "top secret"); - editIssue = new EditIssue(driver, securedPage); - } - - @Test - public void demonstrateNestedLoadableComponents() { - editIssue.get(); - - editIssue.title.sendKeys('Title'); - editIssue.body.sendKeys('What Happened'); - editIssue.setHowToReproduce('How to Reproduce'); - editIssue.setLogOutput('Log Output'); - editIssue.setOperatingSystem('Operating System'); - editIssue.setSeleniumVersion('Selenium Version'); - editIssue.setBrowserVersion('Browser Version'); - editIssue.setDriverVersion('Driver Version'); - editIssue.setUsingGrid('I Am Using Grid'); - } - -} -``` +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/PageObjectTests.java#L1-L30" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} If you're using a library such as [Guiceberry](https://github.com/zorzella/guiceberry) in your tests, the preamble of setting up the PageObjects can be omitted leading to nice, clear, readable tests. @@ -412,34 +273,26 @@ A "bot" is an action-oriented abstraction over the raw Selenium APIs. This means that if you find that commands aren't doing the Right Thing for your app, it's easy to change them. As an example: -```java -public class ActionBot { - private final WebDriver driver; - - public ActionBot(WebDriver driver) { - this.driver = driver; - } - - public void click(By locator) { - driver.findElement(locator).click(); - } - - public void submit(By locator) { - driver.findElement(locator).submit(); - } - - /** - * Type something into an input field. WebDriver doesn't normally clear these - * before typing, so this method does that first. It also sends a return key - * to move the focus out of the element. - */ - public void type(By locator, String text) { - WebElement element = driver.findElement(locator); - element.clear(); - element.sendKeys(text + "\n"); - } -} -``` +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/ActionBot.java#L5-L30" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} Once these abstractions have been built and duplication in your tests identified, it's possible to layer PageObjects on top of bots. @@ -447,11 +300,6 @@ Once these abstractions have been built and duplication in your tests identified {{< tabpane text=true >}} {{< tab header="Python" >}} - -An example of `python + pytest + selenium` which implemented "**Action Bot**, **Loadable Component** and **Page Object**". - -A `pytest` fixture `chrome_driver`. - {{< gh-codeblock path="/examples/python/tests/design_strategy/using_best_practice.py#L6-L26" >}} {{< /tab >}} {{< tab header="Java" >}} 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 c351cc2a5cf..832bf6065df 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 @@ -34,102 +34,74 @@ LoadableComponentは、PageObjectsの作成の負担を軽減することを目 テスト作成者の観点から、これは新しい問題を提出できるサービスを提供します。 基本的なページオブジェクトは次のようになります。 -```java -package com.example.webdriver; - -import org.openqa.selenium.By; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; - -public class EditIssue { - - private final WebDriver driver; - - public EditIssue(WebDriver driver) { - this.driver = driver; - } - - public void setTitle(String title) { - WebElement field = driver.findElement(By.id("issue_title"))); - clearAndType(field, title); - } - - public void setBody(String body) { - WebElement field = driver.findElement(By.id("issue_body")); - clearAndType(field, body); - } - - public void setHowToReproduce(String howToReproduce) { - WebElement field = driver.findElement(By.id("issue_form_repro-command")); - clearAndType(field, howToReproduce); - } - - public void setLogOutput(String logOutput) { - WebElement field = driver.findElement(By.id("issue_form_logs")); - clearAndType(field, logOutput); - } - - public void setOperatingSystem(String operatingSystem) { - WebElement field = driver.findElement(By.id("issue_form_operating-system")); - clearAndType(field, operatingSystem); - } - - public void setSeleniumVersion(String seleniumVersion) { - WebElement field = driver.findElement(By.id("issue_form_selenium-version")); - clearAndType(field, logOutput); - } - - public void setBrowserVersion(String browserVersion) { - WebElement field = driver.findElement(By.id("issue_form_browser-versions")); - clearAndType(field, browserVersion); - } - - public void setDriverVersion(String driverVersion) { - WebElement field = driver.findElement(By.id("issue_form_browser-driver-versions")); - clearAndType(field, driverVersion); - } - - public void setUsingGrid(String usingGrid) { - WebElement field = driver.findElement(By.id("issue_form_selenium-grid-version")); - clearAndType(field, usingGrid); - } - - public IssueList submit() { - driver.findElement(By.cssSelector("button[type='submit']")).click(); - return new IssueList(driver); - } - - private void clearAndType(WebElement field, String text) { - field.clear(); - field.sendKeys(text); - } -} -``` +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/EditIssue.java#L1-L69" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} これをLoadableComponentに変換するには、これを基本型として設定するだけです。 -```java -public class EditIssue extends LoadableComponent { - // rest of class ignored for now -} -``` +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/EditIssueBetter.java#L10" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} この署名は少し変わっているように見えますが、それは、このクラスがEditIssueページをロードするLoadableComponentを表すことを意味します。 このベースクラスを拡張することにより、2つの新しいメソッドを実装する必要があります。 -```java - @Override - protected void load() { - driver.get("https://github.com/SeleniumHQ/selenium/issues/new?assignees=&labels=I-defect%2Cneeds-triaging&projects=&template=bug-report.yml&title=%5B%F0%9F%90%9B+Bug%5D%3A+"); - } - - @Override - protected void isLoaded() throws Error { - String url = driver.getCurrentUrl(); - assertTrue("Not on the issue entry page: " + url, url.endsWith("/new")); - } -``` +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/EditIssueBetter.java#L30-L39" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} `load` メソッドはページに移動するために使用され、 `isLoaded` メソッドは正しいページにいるかどうかを判断するために使用されます。 このメソッドはブール値を返す必要があるように見えますが、代わりにJUnitのAssertクラスを使用して一連のアサーションを実行します。 @@ -138,103 +110,52 @@ public class EditIssue extends LoadableComponent { 少し手直しすると、PageObjectは次のようになります。 -```java -package com.example.webdriver; -import org.openqa.selenium.By; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.FindBy; -import org.openqa.selenium.support.PageFactory; - -import static junit.framework.Assert.assertTrue; - -public class EditIssue extends LoadableComponent { - - private final WebDriver driver; - - // By default the PageFactory will locate elements with the same name or id - // as the field. Since the issue_title element has an id attribute of "issue_title" - // we don't need any additional annotations. - private WebElement issue_title; - - // But we'd prefer a different name in our code than "issue_body", so we use the - // FindBy annotation to tell the PageFactory how to locate the element. - @FindBy(id = "issue_body") private WebElement body; - - public EditIssue(WebDriver driver) { - this.driver = driver; - - // This call sets the WebElement fields. - PageFactory.initElements(driver, this); - } - - @Override - protected void load() { - driver.get("https://github.com/SeleniumHQ/selenium/issues/new?assignees=&labels=I-defect%2Cneeds-triaging&projects=&template=bug-report.yml&title=%5B%F0%9F%90%9B+Bug%5D%3A+"); - } - - @Override - protected void isLoaded() throws Error { - String url = driver.getCurrentUrl(); - assertTrue("Not on the issue entry page: " + url, url.endsWith("/new")); - } - - public void setHowToReproduce(String howToReproduce) { - WebElement field = driver.findElement(By.id("issue_form_repro-command")); - clearAndType(field, howToReproduce); - } - - public void setLogOutput(String logOutput) { - WebElement field = driver.findElement(By.id("issue_form_logs")); - clearAndType(field, logOutput); - } - - public void setOperatingSystem(String operatingSystem) { - WebElement field = driver.findElement(By.id("issue_form_operating-system")); - clearAndType(field, operatingSystem); - } - - public void setSeleniumVersion(String seleniumVersion) { - WebElement field = driver.findElement(By.id("issue_form_selenium-version")); - clearAndType(field, logOutput); - } - - public void setBrowserVersion(String browserVersion) { - WebElement field = driver.findElement(By.id("issue_form_browser-versions")); - clearAndType(field, browserVersion); - } - - public void setDriverVersion(String driverVersion) { - WebElement field = driver.findElement(By.id("issue_form_browser-driver-versions")); - clearAndType(field, driverVersion); - } - - public void setUsingGrid(String usingGrid) { - WebElement field = driver.findElement(By.id("issue_form_selenium-grid-version")); - clearAndType(field, usingGrid); - } - - public IssueList submit() { - driver.findElement(By.cssSelector("button[type='submit']")).click(); - return new IssueList(driver); - } - - private void clearAndType(WebElement field, String text) { - field.clear(); - field.sendKeys(text); - } -} - -``` +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/EditIssueBetter.java#L1-L85" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} それは私たちをあまり信じられなかったようですよね? これまでに行ったことの1つは、ページに移動する方法に関する情報をページ自体にカプセル化することです。 つまり、この情報はコードベース全体に散らばっていません。 これは、テストで下記を実行できることも意味します。 -```java -EditIssue page = new EditIssue(driver).get(); -``` +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/EditIssueBetter.java#L87" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} この呼び出しにより、ドライバーは必要に応じてページに移動します。 @@ -256,140 +177,81 @@ LoadableComponentsは、他のLoadableComponentsと組み合わせて使用す それぞれの "load" メソッドは、親クラスを "get" します。 上記のEditIssueクラスに加えて、最終結果は次のようになります。 -ProjectPage.java: - -```java -package com.example.webdriver; - -import org.openqa.selenium.WebDriver; - -import static org.junit.Assert.assertTrue; - -public class ProjectPage extends LoadableComponent { - - private final WebDriver driver; - private final String projectName; - - public ProjectPage(WebDriver driver, String projectName) { - this.driver = driver; - this.projectName = projectName; - } - - @Override - protected void load() { - driver.get("http://" + projectName + ".googlecode.com/"); - } +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} - @Override - protected void isLoaded() throws Error { - String url = driver.getCurrentUrl(); +ProjectPage.java: - assertTrue(url.contains(projectName)); - } -} -``` +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/ProjectPage.java#L1-L38" >}} and SecuredPage.java: -```java -package com.example.webdriver; - -import org.openqa.selenium.By; -import org.openqa.selenium.NoSuchElementException; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; - -import static org.junit.Assert.fail; - -public class SecuredPage extends LoadableComponent { - - private final WebDriver driver; - private final LoadableComponent parent; - private final String username; - private final String password; - - public SecuredPage(WebDriver driver, LoadableComponent parent, String username, String password) { - this.driver = driver; - this.parent = parent; - this.username = username; - this.password = password; - } - - @Override - protected void load() { - parent.get(); - - String originalUrl = driver.getCurrentUrl(); - - // Sign in - driver.get("https://www.google.com/accounts/ServiceLogin?service=code"); - driver.findElement(By.name("Email")).sendKeys(username); - WebElement passwordField = driver.findElement(By.name("Passwd")); - passwordField.sendKeys(password); - passwordField.submit(); - - // Now return to the original URL - driver.get(originalUrl); - } - - @Override - protected void isLoaded() throws Error { - // If you're signed in, you have the option of picking a different login. - // Let's check for the presence of that. - - try { - WebElement div = driver.findElement(By.id("multilogin-dropdown")); - } catch (NoSuchElementException e) { - fail("Cannot locate user name link"); - } - } -} -``` +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/SecuredPage.java#L1-L52" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} EditIssueの "load" メソッドは次のようになります。 -```java - @Override - protected void load() { - securedPage.get(); - - driver.get("https://github.com/SeleniumHQ/selenium/issues/new?assignees=&labels=I-defect%2Cneeds-triaging&projects=&template=bug-report.yml&title=%5B%F0%9F%90%9B+Bug%5D%3A+"); - } -``` +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/EditIssueBetter.java#L91-L96" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} これは、コンポーネントがすべて相互に "ネストされている" ことを示しています。 EditIssueで `get()` を呼び出すと、そのすべての依存関係も読み込まれます。 使用例: -```java -public class FooTest { - private EditIssue editIssue; - - @Before - public void prepareComponents() { - WebDriver driver = new FirefoxDriver(); - - ProjectPage project = new ProjectPage(driver, "selenium"); - SecuredPage securedPage = new SecuredPage(driver, project, "example", "top secret"); - editIssue = new EditIssue(driver, securedPage); - } - - @Test - public void demonstrateNestedLoadableComponents() { - editIssue.get(); - - editIssue.title.sendKeys('Title'); - editIssue.body.sendKeys('What Happened'); - editIssue.setHowToReproduce('How to Reproduce'); - editIssue.setLogOutput('Log Output'); - editIssue.setOperatingSystem('Operating System'); - editIssue.setSeleniumVersion('Selenium Version'); - editIssue.setBrowserVersion('Browser Version'); - editIssue.setDriverVersion('Driver Version'); - editIssue.setUsingGrid('I Am Using Grid'); - } -} -``` +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/PageObjectTests.java#L1-L30" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} テストで [Guiceberry](https://github.com/zorzella/guiceberry) などのライブラリを使用している場合は、PageObjectsの設定の前文を省略して、わかりやすく読みやすいテストを作成できます。 @@ -404,34 +266,26 @@ PageObjectsは、テストでの重複を減らすための便利な方法です つまり、コマンドがアプリに対して正しいことをしていないことがわかった場合、コマンドを簡単に変更できます。 例として: -```java -public class ActionBot { - private final WebDriver driver; - - public ActionBot(WebDriver driver) { - this.driver = driver; - } - - public void click(By locator) { - driver.findElement(locator).click(); - } - - public void submit(By locator) { - driver.findElement(locator).submit(); - } - - /** - * Type something into an input field. WebDriver doesn't normally clear these - * before typing, so this method does that first. It also sends a return key - * to move the focus out of the element. - */ - public void type(By locator, String text) { - WebElement element = driver.findElement(locator); - element.clear(); - element.sendKeys(text + "\n"); - } -} -``` +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/ActionBot.java#L5-L30" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} これらの抽象化が構築され、テストでの重複が特定されると、ボットの上にPageObjectsを階層化することができます。 @@ -439,11 +293,6 @@ public class ActionBot { {{< tabpane text=true >}} {{< tab header="Python" >}} - -**Action Bot**、**Loadable Component**、および **Page Object** を実装した `python + pytest + selenium` の例です。 - -A `pytest` fixture `chrome_driver`. - {{< gh-codeblock path="/examples/python/tests/design_strategy/using_best_practice.py#L6-L26" >}} {{< /tab >}} {{< tab header="Java" >}} 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 d45d25e4111..12bb49e9c54 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 @@ -40,103 +40,75 @@ the [new issue](https://github.com/SeleniumHQ/selenium/issues/new?assignees=&lab the point of view of a test author, this offers the service of being able to file a new issue. A basic Page Object would look like: -```java -package com.example.webdriver; - -import org.openqa.selenium.By; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; - -public class EditIssue { - - private final WebDriver driver; - - public EditIssue(WebDriver driver) { - this.driver = driver; - } - - public void setTitle(String title) { - WebElement field = driver.findElement(By.id("issue_title"))); - clearAndType(field, title); - } - - public void setBody(String body) { - WebElement field = driver.findElement(By.id("issue_body")); - clearAndType(field, body); - } - - public void setHowToReproduce(String howToReproduce) { - WebElement field = driver.findElement(By.id("issue_form_repro-command")); - clearAndType(field, howToReproduce); - } - - public void setLogOutput(String logOutput) { - WebElement field = driver.findElement(By.id("issue_form_logs")); - clearAndType(field, logOutput); - } - - public void setOperatingSystem(String operatingSystem) { - WebElement field = driver.findElement(By.id("issue_form_operating-system")); - clearAndType(field, operatingSystem); - } - - public void setSeleniumVersion(String seleniumVersion) { - WebElement field = driver.findElement(By.id("issue_form_selenium-version")); - clearAndType(field, logOutput); - } - - public void setBrowserVersion(String browserVersion) { - WebElement field = driver.findElement(By.id("issue_form_browser-versions")); - clearAndType(field, browserVersion); - } - - public void setDriverVersion(String driverVersion) { - WebElement field = driver.findElement(By.id("issue_form_browser-driver-versions")); - clearAndType(field, driverVersion); - } - - public void setUsingGrid(String usingGrid) { - WebElement field = driver.findElement(By.id("issue_form_selenium-grid-version")); - clearAndType(field, usingGrid); - } - - public IssueList submit() { - driver.findElement(By.cssSelector("button[type='submit']")).click(); - return new IssueList(driver); - } - - private void clearAndType(WebElement field, String text) { - field.clear(); - field.sendKeys(text); - } -} -``` +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/EditIssue.java#L1-L69" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} In order to turn this into a LoadableComponent, all we need to do is to set that as the base type: -```java -public class EditIssue extends LoadableComponent { - // rest of class ignored for now -} -``` +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/EditIssueBetter.java#L10" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} This signature looks a little unusual, but it all means is that this class represents a LoadableComponent that loads the EditIssue page. By extending this base class, we need to implement two new methods: -```java - @Override - protected void load() { - driver.get("https://github.com/SeleniumHQ/selenium/issues/new?assignees=&labels=I-defect%2Cneeds-triaging&projects=&template=bug-report.yml&title=%5B%F0%9F%90%9B+Bug%5D%3A+"); - } - - @Override - protected void isLoaded() throws Error { - String url = driver.getCurrentUrl(); - assertTrue("Not on the issue entry page: " + url, url.endsWith("/new")); - } -``` +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/EditIssueBetter.java#L30-L39" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} The `load` method is used to navigate to the page, whilst the `isLoaded` method is used to determine whether we are on the right page. Although the @@ -148,103 +120,52 @@ used to debug tests. With a little rework, our PageObject looks like: -```java -package com.example.webdriver; -import org.openqa.selenium.By; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.FindBy; -import org.openqa.selenium.support.PageFactory; - -import static junit.framework.Assert.assertTrue; - -public class EditIssue extends LoadableComponent { - - private final WebDriver driver; - - // By default the PageFactory will locate elements with the same name or id - // as the field. Since the issue_title element has an id attribute of "issue_title" - // we don't need any additional annotations. - private WebElement issue_title; - - // But we'd prefer a different name in our code than "issue_body", so we use the - // FindBy annotation to tell the PageFactory how to locate the element. - @FindBy(id = "issue_body") private WebElement body; - - public EditIssue(WebDriver driver) { - this.driver = driver; - - // This call sets the WebElement fields. - PageFactory.initElements(driver, this); - } - - @Override - protected void load() { - driver.get("https://github.com/SeleniumHQ/selenium/issues/new?assignees=&labels=I-defect%2Cneeds-triaging&projects=&template=bug-report.yml&title=%5B%F0%9F%90%9B+Bug%5D%3A+"); - } - - @Override - protected void isLoaded() throws Error { - String url = driver.getCurrentUrl(); - assertTrue("Not on the issue entry page: " + url, url.endsWith("/new")); - } - - public void setHowToReproduce(String howToReproduce) { - WebElement field = driver.findElement(By.id("issue_form_repro-command")); - clearAndType(field, howToReproduce); - } - - public void setLogOutput(String logOutput) { - WebElement field = driver.findElement(By.id("issue_form_logs")); - clearAndType(field, logOutput); - } - - public void setOperatingSystem(String operatingSystem) { - WebElement field = driver.findElement(By.id("issue_form_operating-system")); - clearAndType(field, operatingSystem); - } - - public void setSeleniumVersion(String seleniumVersion) { - WebElement field = driver.findElement(By.id("issue_form_selenium-version")); - clearAndType(field, logOutput); - } - - public void setBrowserVersion(String browserVersion) { - WebElement field = driver.findElement(By.id("issue_form_browser-versions")); - clearAndType(field, browserVersion); - } - - public void setDriverVersion(String driverVersion) { - WebElement field = driver.findElement(By.id("issue_form_browser-driver-versions")); - clearAndType(field, driverVersion); - } - - public void setUsingGrid(String usingGrid) { - WebElement field = driver.findElement(By.id("issue_form_selenium-grid-version")); - clearAndType(field, usingGrid); - } - - public IssueList submit() { - driver.findElement(By.cssSelector("button[type='submit']")).click(); - return new IssueList(driver); - } - - private void clearAndType(WebElement field, String text) { - field.clear(); - field.sendKeys(text); - } -} - -``` +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/EditIssueBetter.java#L1-L85" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} That doesn't seem to have bought us much, right? One thing it has done is encapsulate the information about how to navigate to the page into the page itself, meaning that this information's not scattered through the code base. It also means that we can do this in our tests: -```java -EditIssue page = new EditIssue(driver).get(); -``` +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/EditIssueBetter.java#L87" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} This call will cause the driver to navigate to the page if that's necessary. @@ -266,138 +187,79 @@ What would this look like in code? For a start, each logical component would have its own class. The "load" method in each of them would "get" the parent. The end result, in addition to the EditIssue class above is: -ProjectPage.java: - -```java -package com.example.webdriver; - -import org.openqa.selenium.WebDriver; - -import static org.junit.Assert.assertTrue; - -public class ProjectPage extends LoadableComponent { - - private final WebDriver driver; - private final String projectName; - - public ProjectPage(WebDriver driver, String projectName) { - this.driver = driver; - this.projectName = projectName; - } - - @Override - protected void load() { - driver.get("http://" + projectName + ".googlecode.com/"); - } +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} - @Override - protected void isLoaded() throws Error { - String url = driver.getCurrentUrl(); +ProjectPage.java: - assertTrue(url.contains(projectName)); - } -} -``` +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/ProjectPage.java#L1-L38" >}} and SecuredPage.java: -```java -package com.example.webdriver; - -import org.openqa.selenium.By; -import org.openqa.selenium.NoSuchElementException; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; - -import static org.junit.Assert.fail; - -public class SecuredPage extends LoadableComponent { - - private final WebDriver driver; - private final LoadableComponent parent; - private final String username; - private final String password; - - public SecuredPage(WebDriver driver, LoadableComponent parent, String username, String password) { - this.driver = driver; - this.parent = parent; - this.username = username; - this.password = password; - } - - @Override - protected void load() { - parent.get(); - - String originalUrl = driver.getCurrentUrl(); - - // Sign in - driver.get("https://www.google.com/accounts/ServiceLogin?service=code"); - driver.findElement(By.name("Email")).sendKeys(username); - WebElement passwordField = driver.findElement(By.name("Passwd")); - passwordField.sendKeys(password); - passwordField.submit(); - - // Now return to the original URL - driver.get(originalUrl); - } - - @Override - protected void isLoaded() throws Error { - // If you're signed in, you have the option of picking a different login. - // Let's check for the presence of that. - - try { - WebElement div = driver.findElement(By.id("multilogin-dropdown")); - } catch (NoSuchElementException e) { - fail("Cannot locate user name link"); - } - } -} -``` +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/SecuredPage.java#L1-L52" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} The "load" method in EditIssue now looks like: -```java - @Override - protected void load() { - securedPage.get(); - - driver.get("https://github.com/SeleniumHQ/selenium/issues/new?assignees=&labels=I-defect%2Cneeds-triaging&projects=&template=bug-report.yml&title=%5B%F0%9F%90%9B+Bug%5D%3A+"); - } -``` +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/EditIssueBetter.java#L91-L96" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} This shows that the components are all "nested" within each other. A call to `get()` in EditIssue will cause all its dependencies to load too. The example usage: -```java -public class FooTest { - private EditIssue editIssue; - - @Before - public void prepareComponents() { - WebDriver driver = new FirefoxDriver(); - - ProjectPage project = new ProjectPage(driver, "selenium"); - SecuredPage securedPage = new SecuredPage(driver, project, "example", "top secret"); - editIssue = new EditIssue(driver, securedPage); - } - - @Test - public void demonstrateNestedLoadableComponents() { - editIssue.get(); - - editIssue.title.sendKeys('Title'); - editIssue.body.sendKeys('What Happened'); - editIssue.setHowToReproduce('How to Reproduce'); - editIssue.setLogOutput('Log Output'); - editIssue.setOperatingSystem('Operating System'); - editIssue.setSeleniumVersion('Selenium Version'); - editIssue.setBrowserVersion('Browser Version'); - editIssue.setDriverVersion('Driver Version'); - editIssue.setUsingGrid('I Am Using Grid'); - } -} -``` +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/PageObjectTests.java#L1-L30" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} If you're using a library such as [Guiceberry](https://github.com/zorzella/guiceberry) in your tests, the preamble of setting up the PageObjects can be omitted leading to nice, clear, readable tests. @@ -411,34 +273,26 @@ Although PageObjects are a useful way of reducing duplication in your tests, it' A "bot" is an action-oriented abstraction over the raw Selenium APIs. This means that if you find that commands aren't doing the Right Thing for your app, it's easy to change them. As an example: -```java -public class ActionBot { - private final WebDriver driver; - - public ActionBot(WebDriver driver) { - this.driver = driver; - } - - public void click(By locator) { - driver.findElement(locator).click(); - } - - public void submit(By locator) { - driver.findElement(locator).submit(); - } - - /** - * Type something into an input field. WebDriver doesn't normally clear these - * before typing, so this method does that first. It also sends a return key - * to move the focus out of the element. - */ - public void type(By locator, String text) { - WebElement element = driver.findElement(locator); - element.clear(); - element.sendKeys(text + "\n"); - } -} -``` +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/ActionBot.java#L5-L30" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} Once these abstractions have been built and duplication in your tests identified, it's possible to layer PageObjects on top of bots. @@ -447,10 +301,6 @@ Once these abstractions have been built and duplication in your tests identified {{< tabpane text=true >}} {{< tab header="Python" >}} -An example of `python + pytest + selenium` which implemented "**Action Bot**, **Loadable Component** and **Page Object**". - -A `pytest` fixture `chrome_driver`. - {{< gh-codeblock path="/examples/python/tests/design_strategy/using_best_practice.py#L6-L26" >}} {{< /tab >}} {{< tab header="Java" >}} 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 0d379b4b4a3..9399d0e9926 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 @@ -49,103 +49,75 @@ the [new issue](https://github.com/SeleniumHQ/selenium/issues/new?assignees=&lab From the point of view of a test author, this offers the service of being able to file a new issue. A basic Page Object would look like: -```java -package com.example.webdriver; - -import org.openqa.selenium.By; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; - -public class EditIssue { - - private final WebDriver driver; - - public EditIssue(WebDriver driver) { - this.driver = driver; - } - - public void setTitle(String title) { - WebElement field = driver.findElement(By.id("issue_title"))); - clearAndType(field, title); - } - - public void setBody(String body) { - WebElement field = driver.findElement(By.id("issue_body")); - clearAndType(field, body); - } - - public void setHowToReproduce(String howToReproduce) { - WebElement field = driver.findElement(By.id("issue_form_repro-command")); - clearAndType(field, howToReproduce); - } - - public void setLogOutput(String logOutput) { - WebElement field = driver.findElement(By.id("issue_form_logs")); - clearAndType(field, logOutput); - } - - public void setOperatingSystem(String operatingSystem) { - WebElement field = driver.findElement(By.id("issue_form_operating-system")); - clearAndType(field, operatingSystem); - } - - public void setSeleniumVersion(String seleniumVersion) { - WebElement field = driver.findElement(By.id("issue_form_selenium-version")); - clearAndType(field, logOutput); - } - - public void setBrowserVersion(String browserVersion) { - WebElement field = driver.findElement(By.id("issue_form_browser-versions")); - clearAndType(field, browserVersion); - } - - public void setDriverVersion(String driverVersion) { - WebElement field = driver.findElement(By.id("issue_form_browser-driver-versions")); - clearAndType(field, driverVersion); - } - - public void setUsingGrid(String usingGrid) { - WebElement field = driver.findElement(By.id("issue_form_selenium-grid-version")); - clearAndType(field, usingGrid); - } - - public IssueList submit() { - driver.findElement(By.cssSelector("button[type='submit']")).click(); - return new IssueList(driver); - } - - private void clearAndType(WebElement field, String text) { - field.clear(); - field.sendKeys(text); - } -} -``` +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/EditIssue.java#L1-L69" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} In order to turn this into a LoadableComponent, all we need to do is to set that as the base type: -```java -public class EditIssue extends LoadableComponent { - // rest of class ignored for now -} -``` +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/EditIssueBetter.java#L10" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} This signature looks a little unusual, but it all means is that this class represents a LoadableComponent that loads the EditIssue page. By extending this base class, we need to implement two new methods: -```java - @Override - protected void load() { - driver.get("https://github.com/SeleniumHQ/selenium/issues/new?assignees=&labels=I-defect%2Cneeds-triaging&projects=&template=bug-report.yml&title=%5B%F0%9F%90%9B+Bug%5D%3A+"); - } - - @Override - protected void isLoaded() throws Error { - String url = driver.getCurrentUrl(); - assertTrue("Not on the issue entry page: " + url, url.endsWith("/new")); - } -``` +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/EditIssueBetter.java#L30-L39" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} The `load` method is used to navigate to the page, whilst the `isLoaded` method is used to determine whether we are on the right page. Although the @@ -157,103 +129,52 @@ can be used to debug tests. With a little rework, our PageObject looks like: -```java -package com.example.webdriver; -import org.openqa.selenium.By; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.FindBy; -import org.openqa.selenium.support.PageFactory; - -import static junit.framework.Assert.assertTrue; - -public class EditIssue extends LoadableComponent { - - private final WebDriver driver; - - // By default the PageFactory will locate elements with the same name or id - // as the field. Since the issue_title element has an id attribute of "issue_title" - // we don't need any additional annotations. - private WebElement issue_title; - - // But we'd prefer a different name in our code than "issue_body", so we use the - // FindBy annotation to tell the PageFactory how to locate the element. - @FindBy(id = "issue_body") private WebElement body; - - public EditIssue(WebDriver driver) { - this.driver = driver; - - // This call sets the WebElement fields. - PageFactory.initElements(driver, this); - } - - @Override - protected void load() { - driver.get("https://github.com/SeleniumHQ/selenium/issues/new?assignees=&labels=I-defect%2Cneeds-triaging&projects=&template=bug-report.yml&title=%5B%F0%9F%90%9B+Bug%5D%3A+"); - } - - @Override - protected void isLoaded() throws Error { - String url = driver.getCurrentUrl(); - assertTrue("Not on the issue entry page: " + url, url.endsWith("/new")); - } - - public void setHowToReproduce(String howToReproduce) { - WebElement field = driver.findElement(By.id("issue_form_repro-command")); - clearAndType(field, howToReproduce); - } - - public void setLogOutput(String logOutput) { - WebElement field = driver.findElement(By.id("issue_form_logs")); - clearAndType(field, logOutput); - } - - public void setOperatingSystem(String operatingSystem) { - WebElement field = driver.findElement(By.id("issue_form_operating-system")); - clearAndType(field, operatingSystem); - } - - public void setSeleniumVersion(String seleniumVersion) { - WebElement field = driver.findElement(By.id("issue_form_selenium-version")); - clearAndType(field, logOutput); - } - - public void setBrowserVersion(String browserVersion) { - WebElement field = driver.findElement(By.id("issue_form_browser-versions")); - clearAndType(field, browserVersion); - } - - public void setDriverVersion(String driverVersion) { - WebElement field = driver.findElement(By.id("issue_form_browser-driver-versions")); - clearAndType(field, driverVersion); - } - - public void setUsingGrid(String usingGrid) { - WebElement field = driver.findElement(By.id("issue_form_selenium-grid-version")); - clearAndType(field, usingGrid); - } - - public IssueList submit() { - driver.findElement(By.cssSelector("button[type='submit']")).click(); - return new IssueList(driver); - } - - private void clearAndType(WebElement field, String text) { - field.clear(); - field.sendKeys(text); - } -} - -``` +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/EditIssueBetter.java#L1-L85" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} That doesn't seem to have bought us much, right? One thing it has done is encapsulate the information about how to navigate to the page into the page itself, meaning that this information's not scattered through the code base. It also means that we can do this in our tests: -```java -EditIssue page = new EditIssue(driver).get(); -``` +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/EditIssueBetter.java#L87" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} This call will cause the driver to navigate to the page if that's necessary. @@ -275,139 +196,80 @@ What would this look like in code? For a start, each logical component would have its own class. The "load" method in each of them would "get" the parent. The end result, in addition to the EditIssue class above is: -ProjectPage.java: - -```java -package com.example.webdriver; - -import org.openqa.selenium.WebDriver; - -import static org.junit.Assert.assertTrue; - -public class ProjectPage extends LoadableComponent { - - private final WebDriver driver; - private final String projectName; - - public ProjectPage(WebDriver driver, String projectName) { - this.driver = driver; - this.projectName = projectName; - } - - @Override - protected void load() { - driver.get("http://" + projectName + ".googlecode.com/"); - } +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} - @Override - protected void isLoaded() throws Error { - String url = driver.getCurrentUrl(); +ProjectPage.java: - assertTrue(url.contains(projectName)); - } -} -``` +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/ProjectPage.java#L1-L38" >}} and SecuredPage.java: -```java -package com.example.webdriver; - -import org.openqa.selenium.By; -import org.openqa.selenium.NoSuchElementException; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; - -import static org.junit.Assert.fail; - -public class SecuredPage extends LoadableComponent { - - private final WebDriver driver; - private final LoadableComponent parent; - private final String username; - private final String password; - - public SecuredPage(WebDriver driver, LoadableComponent parent, String username, String password) { - this.driver = driver; - this.parent = parent; - this.username = username; - this.password = password; - } - - @Override - protected void load() { - parent.get(); - - String originalUrl = driver.getCurrentUrl(); - - // Sign in - driver.get("https://www.google.com/accounts/ServiceLogin?service=code"); - driver.findElement(By.name("Email")).sendKeys(username); - WebElement passwordField = driver.findElement(By.name("Passwd")); - passwordField.sendKeys(password); - passwordField.submit(); - - // Now return to the original URL - driver.get(originalUrl); - } - - @Override - protected void isLoaded() throws Error { - // If you're signed in, you have the option of picking a different login. - // Let's check for the presence of that. - - try { - WebElement div = driver.findElement(By.id("multilogin-dropdown")); - } catch (NoSuchElementException e) { - fail("Cannot locate user name link"); - } - } -} -``` +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/SecuredPage.java#L1-L52" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} The "load" method in EditIssue now looks like: -```java - @Override - protected void load() { - securedPage.get(); - - driver.get("https://github.com/SeleniumHQ/selenium/issues/new?assignees=&labels=I-defect%2Cneeds-triaging&projects=&template=bug-report.yml&title=%5B%F0%9F%90%9B+Bug%5D%3A+"); - } -``` +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/EditIssueBetter.java#L91-L96" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} This shows that the components are all "nested" within each other. A call to `get()` in EditIssue will cause all its dependencies to load too. The example usage: -```java -public class FooTest { - private EditIssue editIssue; - - @Before - public void prepareComponents() { - WebDriver driver = new FirefoxDriver(); - - ProjectPage project = new ProjectPage(driver, "selenium"); - SecuredPage securedPage = new SecuredPage(driver, project, "example", "top secret"); - editIssue = new EditIssue(driver, securedPage); - } - - @Test - public void demonstrateNestedLoadableComponents() { - editIssue.get(); - - editIssue.title.sendKeys('Title'); - editIssue.body.sendKeys('What Happened'); - editIssue.setHowToReproduce('How to Reproduce'); - editIssue.setLogOutput('Log Output'); - editIssue.setOperatingSystem('Operating System'); - editIssue.setSeleniumVersion('Selenium Version'); - editIssue.setBrowserVersion('Browser Version'); - editIssue.setDriverVersion('Driver Version'); - editIssue.setUsingGrid('I Am Using Grid'); - } -} -``` +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/PageObjectTests.java#L1-L30" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} If you're using a library such as [Guiceberry](https://github.com/zorzella/guiceberry) in your tests, the preamble of setting up the PageObjects can be omitted leading to nice, clear, readable tests. @@ -425,34 +287,26 @@ A "bot" is an action-oriented abstraction over the raw Selenium APIs. This means that if you find that commands aren't doing the Right Thing for your app, it's easy to change them. As an example: -```java -public class ActionBot { - private final WebDriver driver; - - public ActionBot(WebDriver driver) { - this.driver = driver; - } - - public void click(By locator) { - driver.findElement(locator).click(); - } - - public void submit(By locator) { - driver.findElement(locator).submit(); - } - - /** - * Type something into an input field. WebDriver doesn't normally clear these - * before typing, so this method does that first. It also sends a return key - * to move the focus out of the element. - */ - public void type(By locator, String text) { - WebElement element = driver.findElement(locator); - element.clear(); - element.sendKeys(text + "\n"); - } -} -``` +{{< tabpane text=true >}} +{{< tab header="Python" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Java" >}} +{{< gh-codeblock path="/examples/java/src/test/java/dev/selenium/design_strategies/ActionBot.java#L5-L30" >}} +{{< /tab >}} +{{< tab header="CSharp" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Ruby" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="JavaScript" >}} +{{< badge-code >}} +{{< /tab >}} +{{< tab header="Kotlin" >}} +{{< badge-code >}} +{{< /tab >}} +{{< /tabpane >}} Once these abstractions have been built and duplication in your tests identified, it's possible to layer PageObjects on top of bots. @@ -462,10 +316,6 @@ identified, it's possible to layer PageObjects on top of bots. {{< tabpane text=true >}} {{< tab header="Python" >}} -一个用例 使用 `python + pytest + selenium` 实现了设计策略 "**Action Bot**, **Loadable Component** 和 **Page Object**". - -A `pytest` fixture `chrome_driver`. - {{< gh-codeblock path="/examples/python/tests/design_strategy/using_best_practice.py#L6-L26" >}} {{< /tab >}} {{< tab header="Java" >}}