diff --git a/.github/workflows/playwright-tests.yml b/.github/workflows/playwright-tests.yml new file mode 100644 index 0000000..25ff2df --- /dev/null +++ b/.github/workflows/playwright-tests.yml @@ -0,0 +1,52 @@ +name: Playwright Tests + +# Trigger the workflow on every push to any branch +on: + push: + branches: + - '**' # Run on every branch for every commit + pull_request: + branches: + - '**' # Run on every pull request for any branch + +jobs: + test: + runs-on: ubuntu-latest + + steps: + # Step 1: Check out the repository code + - name: Checkout repository + uses: actions/checkout@v3 + + # Step 2: Set up JDK 17 (adjust if you're using a different version) + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'adopt' + + # Step 3: Install Maven 3.9.9 + - name: Setup Maven Action + uses: s4u/setup-maven-action@v1.7.0 + with: + checkout-fetch-depth: 0 + java-version: 17 + java-distribution: temurin + maven-version: 3.9.9 + + # Step 4: Verify Maven installation + - name: Verify Maven version + run: mvn --version + + # Step 5: Cache Maven dependencies + - name: Cache Maven dependencies + uses: actions/cache@v3 + with: + path: ~/.m2 + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven + + # Step 4: Run Maven to execute Playwright tests + - name: Run Playwright Tests + run: mvn verify diff --git a/.gitignore b/.gitignore index 2f43530..a527cf1 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,7 @@ buildNumber.properties .project # JDT-specific (Eclipse Java Development Tools) .classpath +.idea +.allure +allure-results +.idea \ No newline at end of file diff --git a/README.md b/README.md index d3ab88b..63786e6 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,131 @@ -# playwright-in-java-sample-code -Sample code for the Serenity Dojo Playwright In Java course. +# Mastering Modern Test Automation With Playwright In Java + +This repository contains the sample code for the **[Mastering Modern Test Automation With Playwright In Java](https://www.udemy.com/course/mastering-modern-test-automation-with-playwright-in-java/?referralCode=06560D474D519B88409D)** course. It is designed to complement your learning experience, providing hands-on examples and exercises for each module of the course. + +## About the Course + +Modern web testing demands modern solutions, and Playwright has emerged as the go-to testing framework for dynamic web applications. By combining the power of Playwright with Java's enterprise-grade ecosystem, this course will equip you with the skills needed to build reliable, maintainable test automation frameworks. + +**Key Highlights:** +- Superior auto-wait and asynchronous testing capabilities +- Multi-browser support for Chromium, Firefox, and WebKit +- Network interception and mocking capabilities +- Rich debugging and codegen tools +- Seamless integration with Java tools like JUnit and Cucumber + +You’ll learn: +- Playwright fundamentals, architecture, and best practices +- Writing robust tests for modern web applications +- Using advanced features like API mocking, parallel execution, and CI/CD integration +- Behavior-Driven Development (BDD) using Cucumber +- Generating professional test reports with Allure +- Leveraging AI tools to accelerate test writing + +### Access the Course Material +Enroll in the course and access all the learning resources here: [Mastering Modern Test Automation With Playwright In Java](https://www.udemy.com/course/mastering-modern-test-automation-with-playwright-in-java/?referralCode=06560D474D519B88409D) + +--- + +## Git Branches and Corresponding Udemy Course Modules + +The branch structure of this repository follows the convention `sample-code/`. Each branch corresponds to a module in the course and contains the sample code for that specific topic. Some of the module branches include: + +The table below maps each Git branch in this repository to its corresponding Udemy course module. Click the links to access the relevant lecture directly. + +| **Module** | **Git Branch** | **Udemy Lecture Link** | +|--------------------|--------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------| +| Start Here | `sample-code/start-here` | [Start Here](https://www.udemy.com/course/mastering-modern-test-automation-with-playwright-in-java/learn/lecture/46063725#overview) | +| Module 3 | `sample-code/module-3-my-first-playwright-test` | [My First Playwright Test](https://www.udemy.com/course/mastering-modern-test-automation-with-playwright-in-java/learn/lecture/46077299#overview) | +| Module 4 | `sample-code/module-4-interacting-with-elements` | [Interacting With Field Elements](https://www.udemy.com/course/mastering-modern-test-automation-with-playwright-in-java/learn/lecture/46649323#overview) | +| Module 5 | `sample-code/module-5-refactoring` | [Refactoring](https://www.udemy.com/course/mastering-modern-test-automation-with-playwright-in-java/learn/lecture/46077309#overview) | +| Module 6 | `sample-code/module-6-browser-options` | [Browser Options](https://www.udemy.com/course/mastering-modern-test-automation-with-playwright-in-java/learn/lecture/46110387#overview) | +| Module 7 | `sample-code/module-7-browser-contexts` | [Browser Contexts](https://www.udemy.com/course/mastering-modern-test-automation-with-playwright-in-java/learn/lecture/46145035#overview) | +| Module 8 | `sample-code/module-8-locators` | [Locators](https://www.udemy.com/course/mastering-modern-test-automation-with-playwright-in-java/learn/lecture/46178143#overview) | +| Module 9 | `sample-code/module-9-forms` | [Forms](https://www.udemy.com/course/mastering-modern-test-automation-with-playwright-in-java/learn/lecture/46649323#overview) | +| Module 10 | `sample-code/module-10-assertions` | [Assertions](https://www.udemy.com/course/mastering-modern-test-automation-with-playwright-in-java/learn/lecture/46280267#overview) | +| Module 11 | `sample-code/module-11-waits` | [Waits](https://www.udemy.com/course/mastering-modern-test-automation-with-playwright-in-java/learn/lecture/46680609#overview) | +| Module 12 | `sample-code/module-12-mocking-api-calls` | [Mocking API Calls](https://www.udemy.com/course/mastering-modern-test-automation-with-playwright-in-java/learn/lecture/46728327#overview) | +| Module 13 | `sample-code/module-13-page-objects` | [Page Objects](https://www.udemy.com/course/mastering-modern-test-automation-with-playwright-in-java/learn/lecture/46723143#overview) | +| Module 14 | `sample-code/module-14-organizing-your-tests` | [Organizing Your Tests](https://www.udemy.com/course/mastering-modern-test-automation-with-playwright-in-java/learn/lecture/46833089#overview) | +| Module 15 | `sample-code/module-15-parallel-execution` | [Parallel Execution](https://www.udemy.com/course/mastering-modern-test-automation-with-playwright-in-java/learn/lecture/46835495#overview) | +| Module 16 | `sample-code/module-16-allure-reporting` | [Allure Reporting](https://www.udemy.com/course/mastering-modern-test-automation-with-playwright-in-java/learn/lecture/46849301#overview) | +| Module 17 | `sample-code/module-17-cucumber` | [Cucumber](https://www.udemy.com/course/mastering-modern-test-automation-with-playwright-in-java/learn/lecture/46906341#overview) | + +--- + +## How to Use This Repository + +The exercises in this repository are designed to build on each other in a sequential order, providing you with a progressive learning experience. Here's how you can make the most of the exercises: + +### Step 1: Starting the Exercises +Begin your journey into Playwright and Java test automation by setting up your workspace. You have two options to start: + +1. **Use the `sample-code/start-here` branch:** + This branch contains a basic starting point with all the necessary configuration files (like a Maven pom.xml file) to help you get started quickly. +2. **Start with an empty Maven project:** + If you prefer to set up everything from scratch, you can create your own Maven project and follow along with the course to configure it step by step. This approach is great for reinforcing your understanding of the setup process. + +### Step 2: Follow Along With the Coding Exercises +Each course module introduces new concepts and techniques, with corresponding exercises for hands-on practice. Follow these steps as you progress: + +1. **Follow the Exercises in Order:** + The modules are designed to build upon each other. Completing them in order ensures you gain a solid understanding of each concept before moving to the next. +2. **Reference the Module-Specific Sample Code:** + Each module's sample code is available in the corresponding branch, named sample-code/. For example: + - sample-code/module-3-my-first-playwright-test + - sample-code/module-6-browser-options + You can check out these branches to view the sample solution for each module if you get stuck or want to see how the exercises are implemented. + +3. **Work on the Exercises in Your Own Branch:** + Create your own branch to experiment with the exercises. For example: +```bash +git checkout -b module-3-exercises +``` +This approach allows you to practice and experiment without affecting the main codebase or other branches. + +Feel free to submit pull requests or raise issues if you find bugs or areas for improvement. + +### Step 3: Using Sample Solutions to Unblock Yourself +If you encounter challenges while working through an exercise: + +1. Check the Sample Solution: + Switch to the branch for the current module to review the sample code and compare it with your own: +```bash +git checkout sample-code/ +``` + +2. **Learn From the Solution:** + Pay attention to how the concepts are applied and implemented. Take note of any differences between your code and the solution, and try to understand why those differences exist. + +### **Step 4: Starting Fresh at Any Point** +If you want to start over or reset your work for a specific module: + +1. **Checkout the Previous Module's Sample Code:** + Each module builds on the previous one, and the starting point for a module is the final state of the previous module. For example: + If you are working on Module 5 and want to start fresh, check out the code from sample-code/module-4-interacting-with-elements. + Use the following command: +```bash +git checkout sample-code/ +``` + +2. **Create a New Branch From the Previous Module:** + After checking out the previous module's sample code, create a new branch to start your work: +```bash +git checkout -b module-5-exercises +``` + +3. **Continue From the Reset State:** + Use the previous module's code as the starting point for the new module, and continue with the exercises. + + +### Step 5: Submitting Your Work +If you are following along as part of the [Serenity Dojo coaching program](http://serenitydojo.academy), you will be able to get feedback about your work from one of the Serenity Dojo coaches. Here is how you do that: + +1. Push Your Changes to Your Repository: + Push your completed exercises to your forked repository to save your work: +```bash +git push origin +``` + +2. Share Your Branch: + If requested, share the branch link with your instructor or team for review. diff --git a/pom.xml b/pom.xml index b57a6f5..ceffb0c 100644 --- a/pom.xml +++ b/pom.xml @@ -12,21 +12,138 @@ 17 17 UTF-8 + 2.29.0 + 1.9.21 + + + + io.qameta.allure + allure-bom + ${allure.version} + pom + import + + + + com.microsoft.playwright playwright - 1.47.0 + 1.48.0 test org.junit.jupiter junit-jupiter - 5.11.1 + 5.11.3 + test + + + org.junit.platform + junit-platform-suite + 1.11.3 + test + + + io.cucumber + cucumber-java + 7.20.1 + test + + + io.cucumber + cucumber-junit + 7.20.1 + test + + + io.cucumber + cucumber-junit-platform-engine + 7.20.1 + test + + + org.assertj + assertj-core + 3.26.3 + + + com.deque.html.axe-core + playwright + 4.10.0 + + + io.qameta.allure + allure-junit5 + test + + + io.qameta.allure + allure-junit-platform + test + + + io.qameta.allure + allure-cucumber7-jvm + test + + + net.datafaker + datafaker + 2.4.2 test + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + + -parameters + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.3 + + + -javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar" + + + + + org.aspectj + aspectjweaver + ${aspectj.version} + + + + + io.qameta.allure + allure-maven + 2.15.0 + + ${allure.version} + ${project.build.directory}/allure-results + + + + verify + + report + + + + + + \ No newline at end of file diff --git a/src/test/java/com/serenitydojo/playwright/ASimplePlaywrightTest.java b/src/test/java/com/serenitydojo/playwright/ASimplePlaywrightTest.java deleted file mode 100644 index 2cca58e..0000000 --- a/src/test/java/com/serenitydojo/playwright/ASimplePlaywrightTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.serenitydojo.playwright; - -import com.microsoft.playwright.Browser; -import com.microsoft.playwright.Page; -import com.microsoft.playwright.Playwright; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class ASimplePlaywrightTest { - - @Test - void shouldShowThePageTitle() { - Playwright playwright = Playwright.create(); - Browser browser = playwright.chromium().launch(); - Page page = browser.newPage(); - - page.navigate("https://practicesoftwaretesting.com"); - String title = page.title(); - - Assertions.assertTrue(title.contains("Practice Software Testing")); - - browser.close(); - playwright.close(); - } -} diff --git a/src/test/java/com/serenitydojo/playwright/toolshop/catalog/AddToCartTest.java b/src/test/java/com/serenitydojo/playwright/toolshop/catalog/AddToCartTest.java new file mode 100644 index 0000000..f9f5f36 --- /dev/null +++ b/src/test/java/com/serenitydojo/playwright/toolshop/catalog/AddToCartTest.java @@ -0,0 +1,93 @@ +package com.serenitydojo.playwright.toolshop.catalog; + +import com.serenitydojo.playwright.toolshop.catalog.pageobjects.*; +import com.serenitydojo.playwright.toolshop.fixtures.PlaywrightTestCase; +import io.qameta.allure.Feature; +import io.qameta.allure.Story; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +@DisplayName("Shopping Cart") +@Feature("Shopping Cart") +public class AddToCartTest extends PlaywrightTestCase { + + SearchComponent searchComponent; + ProductList productList; + ProductDetails productDetails; + NavBar navBar; + CheckoutCart checkoutCart; + + @BeforeEach + void openHomePage() { + page.navigate("https://practicesoftwaretesting.com"); + } + + @BeforeEach + void setUp() { + searchComponent = new SearchComponent(page); + productList = new ProductList(page); + productDetails = new ProductDetails(page); + navBar = new NavBar(page); + checkoutCart = new CheckoutCart(page); + } + + @Test + @Story("Check out") + @DisplayName("Checking out a single item") + void whenCheckingOutASingleItem() { + searchComponent.searchBy("pliers"); + productList.viewProductDetails("Combination Pliers"); + + productDetails.increaseQuanityBy(2); + productDetails.addToCart(); + + navBar.openCart(); + + List lineItems = checkoutCart.getLineItems(); + + Assertions.assertThat(lineItems) + .hasSize(1) + .first() + .satisfies(item -> { + Assertions.assertThat(item.title()).contains("Combination Pliers"); + Assertions.assertThat(item.quantity()).isEqualTo(3); + Assertions.assertThat(item.total()).isEqualTo(item.quantity() * item.price()); + }); + } + + @Test + @Story("Check out") + @DisplayName("Checking out multiple items") + void whenCheckingOutMultipleItems() { + navBar.openHomePage(); + + productList.viewProductDetails("Bolt Cutters"); + productDetails.increaseQuanityBy(2); + productDetails.addToCart(); + + navBar.openHomePage(); + productList.viewProductDetails("Slip Joint Pliers"); + productDetails.addToCart(); + + navBar.openCart(); + + List lineItems = checkoutCart.getLineItems(); + + Assertions.assertThat(lineItems).hasSize(2); + List productNames = lineItems.stream().map(CartLineItem::title).toList(); + Assertions.assertThat(productNames).contains("Bolt Cutters", "Slip Joint Pliers"); + + Assertions.assertThat(lineItems) + .allSatisfy(item -> { + Assertions.assertThat(item.quantity()).isGreaterThanOrEqualTo(1); + Assertions.assertThat(item.price()).isGreaterThan(0.0); + Assertions.assertThat(item.total()).isGreaterThan(0.0); + Assertions.assertThat(item.total()).isEqualTo(item.quantity() * item.price()); + }); + + } +} \ No newline at end of file diff --git a/src/test/java/com/serenitydojo/playwright/toolshop/catalog/SearchForProductsTest.java b/src/test/java/com/serenitydojo/playwright/toolshop/catalog/SearchForProductsTest.java new file mode 100644 index 0000000..1cc5d63 --- /dev/null +++ b/src/test/java/com/serenitydojo/playwright/toolshop/catalog/SearchForProductsTest.java @@ -0,0 +1,71 @@ +package com.serenitydojo.playwright.toolshop.catalog; + +import com.serenitydojo.playwright.toolshop.catalog.pageobjects.ProductList; +import com.serenitydojo.playwright.toolshop.catalog.pageobjects.SearchComponent; +import com.serenitydojo.playwright.toolshop.fixtures.PlaywrightTestCase; +import io.qameta.allure.Feature; +import io.qameta.allure.Story; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@DisplayName("Searching for products") +@Feature("Searching for products") +public class SearchForProductsTest extends PlaywrightTestCase { + + @BeforeEach + void openHomePage() { + page.navigate("https://practicesoftwaretesting.com"); + } + + @Nested + @DisplayName("Searching by keyword") + @Story("Searching by keyword") + class SearchingByKeyword { + + @Test + @DisplayName("When there are matching results") + void whenSearchingByKeyword() { + SearchComponent searchComponent = new SearchComponent(page); + ProductList productList = new ProductList(page); + + searchComponent.searchBy("tape"); + + var matchingProducts = productList.getProductNames(); + + Assertions.assertThat(matchingProducts).contains("Tape Measure 7.5m", "Measuring Tape", "Tape Measure 5m"); + } + + @Test + @DisplayName("When there are no matching results") + void whenThereIsNoMatchingProduct() { + SearchComponent searchComponent = new SearchComponent(page); + ProductList productList = new ProductList(page); + searchComponent.searchBy("unknown"); + + var matchingProducts = productList.getProductNames(); + + Assertions.assertThat(matchingProducts).isEmpty(); + Assertions.assertThat(productList.getSearchCompletedMessage()).contains("There are no products found."); + } + } + + @Test + @Story("Clearing the previous search results") + @DisplayName("When the user clears a previous search results") + void clearingTheSearchResults() { + SearchComponent searchComponent = new SearchComponent(page); + ProductList productList = new ProductList(page); + searchComponent.searchBy("saw"); + + var matchingFilteredProducts = productList.getProductNames(); + Assertions.assertThat(matchingFilteredProducts).hasSize(2); + + searchComponent.clearSearch(); + + var matchingProducts = productList.getProductNames(); + Assertions.assertThat(matchingProducts).hasSize(9); + } +} \ No newline at end of file diff --git a/src/test/java/com/serenitydojo/playwright/toolshop/catalog/pageobjects/CartLineItem.java b/src/test/java/com/serenitydojo/playwright/toolshop/catalog/pageobjects/CartLineItem.java new file mode 100644 index 0000000..62f0e97 --- /dev/null +++ b/src/test/java/com/serenitydojo/playwright/toolshop/catalog/pageobjects/CartLineItem.java @@ -0,0 +1,3 @@ +package com.serenitydojo.playwright.toolshop.catalog.pageobjects; + +public record CartLineItem(String title, int quantity, double price, double total) {} diff --git a/src/test/java/com/serenitydojo/playwright/toolshop/catalog/pageobjects/CheckoutCart.java b/src/test/java/com/serenitydojo/playwright/toolshop/catalog/pageobjects/CheckoutCart.java new file mode 100644 index 0000000..f2df7c7 --- /dev/null +++ b/src/test/java/com/serenitydojo/playwright/toolshop/catalog/pageobjects/CheckoutCart.java @@ -0,0 +1,39 @@ +package com.serenitydojo.playwright.toolshop.catalog.pageobjects; + +import com.microsoft.playwright.Page; +import io.qameta.allure.Step; + +import java.util.List; + +public class CheckoutCart { + private final Page page; + + public CheckoutCart(Page page) { + this.page = page; + } + + public List getLineItems() { + page.locator("app-cart tbody tr").first().waitFor(); + return page.locator("app-cart tbody tr") + .all() + .stream() + .map( + row -> { + String title = trimmed(row.getByTestId("product-title").innerText()); + int quantity = Integer.parseInt(row.getByTestId("product-quantity").inputValue()); + double price = Double.parseDouble(price(row.getByTestId("product-price").innerText())); + double linePrice = Double.parseDouble(price(row.getByTestId("line-price").innerText())); + return new CartLineItem(title, quantity, price, linePrice); + } + ).toList(); + } + + private String trimmed(String value) { + return value.strip().replaceAll("\u00A0", ""); + } + + private String price(String value) { + return value.replace("$", ""); + } + +} \ No newline at end of file diff --git a/src/test/java/com/serenitydojo/playwright/toolshop/catalog/pageobjects/NavBar.java b/src/test/java/com/serenitydojo/playwright/toolshop/catalog/pageobjects/NavBar.java new file mode 100644 index 0000000..e8bad1e --- /dev/null +++ b/src/test/java/com/serenitydojo/playwright/toolshop/catalog/pageobjects/NavBar.java @@ -0,0 +1,27 @@ +package com.serenitydojo.playwright.toolshop.catalog.pageobjects; + +import com.microsoft.playwright.Page; +import io.qameta.allure.Step; + +public class NavBar { + private final Page page; + + public NavBar(Page page) { + this.page = page; + } + + @Step("Open cart") + public void openCart() { + page.getByTestId("nav-cart").click(); + } + + @Step("Open the home page") + public void openHomePage() { + page.navigate("https://practicesoftwaretesting.com"); + } + + @Step("Open the Contact page") + public void toTheContactPage() { + page.navigate("https://practicesoftwaretesting.com/contact"); + } +} diff --git a/src/test/java/com/serenitydojo/playwright/toolshop/catalog/pageobjects/ProductDetails.java b/src/test/java/com/serenitydojo/playwright/toolshop/catalog/pageobjects/ProductDetails.java new file mode 100644 index 0000000..0b9fcb1 --- /dev/null +++ b/src/test/java/com/serenitydojo/playwright/toolshop/catalog/pageobjects/ProductDetails.java @@ -0,0 +1,31 @@ +package com.serenitydojo.playwright.toolshop.catalog.pageobjects; + +import com.microsoft.playwright.Page; +import com.microsoft.playwright.options.AriaRole; +import io.qameta.allure.Step; + +public class ProductDetails { + private final Page page; + + public ProductDetails(Page page) { + this.page = page; + } + + @Step("Increase product quantity") + public void increaseQuanityBy(int increment) { + for (int i = 1; i <= increment; i++) { + page.getByTestId("increase-quantity").click(); + } + } + + @Step("Add to cart") + public void addToCart() { + page.waitForResponse( + response -> response.url().contains("/carts") && response.request().method().equals("POST"), + () -> { + page.getByText("Add to cart").click(); + page.getByRole(AriaRole.ALERT).click(); + } + ); + } +} diff --git a/src/test/java/com/serenitydojo/playwright/toolshop/catalog/pageobjects/ProductList.java b/src/test/java/com/serenitydojo/playwright/toolshop/catalog/pageobjects/ProductList.java new file mode 100644 index 0000000..4570aae --- /dev/null +++ b/src/test/java/com/serenitydojo/playwright/toolshop/catalog/pageobjects/ProductList.java @@ -0,0 +1,39 @@ +package com.serenitydojo.playwright.toolshop.catalog.pageobjects; + +import com.microsoft.playwright.Page; +import com.serenitydojo.playwright.toolshop.fixtures.ProductSummary; +import io.qameta.allure.Step; + +import java.util.List; + +public class ProductList { + private final Page page; + + public ProductList(Page page) { + this.page = page; + } + + + public List getProductNames() { + return page.getByTestId("product-name").allInnerTexts(); + } + + public List getProductSummaries() { + return page.locator(".card").all() + .stream() + .map(productCard -> { + String productName = productCard.getByTestId("product-name").textContent().strip(); + String productPrice = productCard.getByTestId("product-price").textContent(); + return new ProductSummary(productName, productPrice); + }).toList(); + } + + @Step("View product details") + public void viewProductDetails(String productName) { + page.locator(".card").getByText(productName).click(); + } + + public String getSearchCompletedMessage() { + return page.getByTestId("search_completed").textContent(); + } +} diff --git a/src/test/java/com/serenitydojo/playwright/toolshop/catalog/pageobjects/SearchComponent.java b/src/test/java/com/serenitydojo/playwright/toolshop/catalog/pageobjects/SearchComponent.java new file mode 100644 index 0000000..8670319 --- /dev/null +++ b/src/test/java/com/serenitydojo/playwright/toolshop/catalog/pageobjects/SearchComponent.java @@ -0,0 +1,37 @@ +package com.serenitydojo.playwright.toolshop.catalog.pageobjects; + +import com.microsoft.playwright.Page; +import com.microsoft.playwright.options.AriaRole; + +public class SearchComponent { + private final Page page; + + public SearchComponent(Page page) { + this.page = page; + } + + public void searchBy(String keyword) { + page.waitForResponse("**/products/search?**", () -> { + page.getByPlaceholder("Search").fill(keyword); + page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Search")).click(); + }); + } + + public void clearSearch() { + page.waitForResponse("**/products**", () -> { + page.getByTestId("search-reset").click(); + }); + } + + public void filterBy(String filterName) { + page.waitForResponse("**/products?**by_category=**", () -> { + page.getByLabel(filterName).click(); + }); + } + + public void sortBy(String sortFilter) { + page.waitForResponse("**/products?page=0&sort=**", () -> { + page.getByTestId("sort").selectOption(sortFilter); + }); + } +} \ No newline at end of file diff --git a/src/test/java/com/serenitydojo/playwright/toolshop/contact/ContactForm.java b/src/test/java/com/serenitydojo/playwright/toolshop/contact/ContactForm.java new file mode 100644 index 0000000..cd438f6 --- /dev/null +++ b/src/test/java/com/serenitydojo/playwright/toolshop/contact/ContactForm.java @@ -0,0 +1,63 @@ +package com.serenitydojo.playwright.toolshop.contact; + +import com.microsoft.playwright.Locator; +import com.microsoft.playwright.Page; +import com.microsoft.playwright.options.AriaRole; + +import java.nio.file.Path; + +public class ContactForm { + private final Page page; + private Locator firstNameField; + private Locator lastNameField; + private Locator emailNameField; + private Locator messageField; + private Locator subjectField; + private Locator sendButton; + + public ContactForm(Page page) { + this.page = page; + this.firstNameField = page.getByLabel("First name"); + this.lastNameField = page.getByLabel("Last name"); + this.emailNameField = page.getByLabel("Email"); + this.messageField = page.getByLabel("Message"); + this.subjectField = page.getByLabel("Subject"); + this.sendButton = page.getByText("Send"); + } + + public void setFirstName(String firstName) { + firstNameField.fill(firstName); + } + + public void setLastName(String lastName) { + lastNameField.fill(lastName); + } + + public void setEmail(String email) { + emailNameField.fill(email); + } + + public void setMessage(String message) { + messageField.fill(message); + } + + public void selectSubject(String subject) { + subjectField.selectOption(subject); + } + + public void setAttachment(Path fileToUpload) { + page.setInputFiles("#attachment", fileToUpload); + } + + public void submitForm() { + sendButton.click(); + } + + public String getAlertMessage() { + return page.getByRole(AriaRole.ALERT).textContent(); + } + + public void clearField(String fieldName) { + page.getByLabel(fieldName).clear(); + } +} diff --git a/src/test/java/com/serenitydojo/playwright/toolshop/contact/ContactFormTest.java b/src/test/java/com/serenitydojo/playwright/toolshop/contact/ContactFormTest.java new file mode 100644 index 0000000..f698e89 --- /dev/null +++ b/src/test/java/com/serenitydojo/playwright/toolshop/contact/ContactFormTest.java @@ -0,0 +1,113 @@ +package com.serenitydojo.playwright.toolshop.contact; + +import com.microsoft.playwright.assertions.LocatorAssertions; +import com.microsoft.playwright.options.AriaRole; +import com.serenitydojo.playwright.toolshop.catalog.pageobjects.NavBar; +import com.serenitydojo.playwright.toolshop.fixtures.PlaywrightTestCase; +import io.qameta.allure.Allure; +import io.qameta.allure.Feature; +import io.qameta.allure.Story; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat; + +@DisplayName("Contact form") +@Feature("Contact form") +@Execution(ExecutionMode.SAME_THREAD) +public class ContactFormTest extends PlaywrightTestCase { + + ContactForm contactForm; + NavBar navigate; + + @BeforeEach + void openContactPage() { + contactForm = new ContactForm(page); + navigate = new NavBar(page); + navigate.toTheContactPage(); + } + + @Story("Submitting a request") + @DisplayName("Customers can use the contact form to contact us") + @Test + void completeForm() throws URISyntaxException { + contactForm.setFirstName("Sarah-Jane"); + contactForm.setLastName("Smith"); + contactForm.setEmail("sarah@example.com"); + contactForm.setMessage("A very long message to the warranty service about a warranty on a product!"); + contactForm.selectSubject("Warranty"); + + Path fileToUpload = Paths.get(ClassLoader.getSystemResource("data/sample-data.txt").toURI()); + contactForm.setAttachment(fileToUpload); + + contactForm.submitForm(); + + Assertions.assertThat(contactForm.getAlertMessage()) + .contains("Thanks for your message! We will contact you shortly."); + } + + @Story("Submitting a request") + @DisplayName("First name, last name, email and message are mandatory") + @ParameterizedTest(name = "{arguments} is a mandatory field") + @ValueSource(strings = {"First name", "Last name", "Email", "Message"}) + void mandatoryFields(String fieldName) { + // Fill in the field values + contactForm.setFirstName("Sarah-Jane"); + contactForm.setLastName("Smith"); + contactForm.setEmail("sarah@example.com"); + contactForm.setMessage("A very long message to the warranty service about a warranty on a product!"); + contactForm.selectSubject("Warranty"); + + // Clear one of the fields + contactForm.clearField(fieldName); + page.waitForTimeout(250); + contactForm.submitForm(); + + // Check the error message for that field + var errorMessage = page.getByRole(AriaRole.ALERT).getByText(fieldName + " is required"); + + assertThat(errorMessage).isVisible(); + } + + @Story("Submitting a request") + @DisplayName("The message must be at least 50 characters long") + @Test + void messageTooShort() { + + contactForm.setFirstName("Sarah-Jane"); + contactForm.setLastName("Smith"); + contactForm.setEmail("sarah@example.com"); + contactForm.setMessage("A short long message."); + contactForm.selectSubject("Warranty"); + + contactForm.submitForm(); + + assertThat(page.getByRole(AriaRole.ALERT)).hasText("Message must be minimal 50 characters"); + } + + @Story("Submitting a request") + @DisplayName("The email address must be correctly formatted") + @ParameterizedTest(name = "'{arguments}' should be rejected") + @ValueSource(strings = {"not-an-email", "not-an.email.com", "notanemail"}) + void invalidEmailField(String invalidEmail) { + contactForm.setFirstName("Sarah-Jane"); + contactForm.setLastName("Smith"); + contactForm.setEmail(invalidEmail); + contactForm.setMessage("A very long message to the warranty service about a warranty on a product!"); + contactForm.selectSubject("Warranty"); + + contactForm.submitForm(); + + assertThat(page.getByRole(AriaRole.ALERT)).hasText("Email format is invalid"); + } +} diff --git a/src/test/java/com/serenitydojo/playwright/toolshop/cucumber/CucumberTests.java b/src/test/java/com/serenitydojo/playwright/toolshop/cucumber/CucumberTests.java new file mode 100644 index 0000000..0924335 --- /dev/null +++ b/src/test/java/com/serenitydojo/playwright/toolshop/cucumber/CucumberTests.java @@ -0,0 +1,18 @@ +package com.serenitydojo.playwright.toolshop.cucumber; + +import org.junit.platform.suite.api.ConfigurationParameter; +import org.junit.platform.suite.api.IncludeEngines; +import org.junit.platform.suite.api.SelectClasspathResource; +import org.junit.platform.suite.api.Suite; + +@Suite +@IncludeEngines("cucumber") +@SelectClasspathResource("/features") +@ConfigurationParameter( + key="cucumber.plugin", + value = "io.qameta.allure.cucumber7jvm.AllureCucumber7Jvm," + + "pretty," + + "html:target/cucumber-reports/cucumber.html" +) +public class CucumberTests { +} diff --git a/src/test/java/com/serenitydojo/playwright/toolshop/cucumber/stepdefinitions/PlaywrightCucumberFixtures.java b/src/test/java/com/serenitydojo/playwright/toolshop/cucumber/stepdefinitions/PlaywrightCucumberFixtures.java new file mode 100644 index 0000000..d6eb534 --- /dev/null +++ b/src/test/java/com/serenitydojo/playwright/toolshop/cucumber/stepdefinitions/PlaywrightCucumberFixtures.java @@ -0,0 +1,54 @@ +package com.serenitydojo.playwright.toolshop.cucumber.stepdefinitions; + +import com.microsoft.playwright.*; +import io.cucumber.java.After; +import io.cucumber.java.AfterAll; +import io.cucumber.java.Before; + +import java.util.Arrays; + +public class PlaywrightCucumberFixtures { + private static final ThreadLocal playwright + = ThreadLocal.withInitial(() -> { + Playwright playwright = Playwright.create(); + playwright.selectors().setTestIdAttribute("data-test"); + return playwright; + } + ); + + private static final ThreadLocal browser = ThreadLocal.withInitial(() -> + playwright.get().chromium().launch( + new BrowserType.LaunchOptions() + .setHeadless(true) + .setArgs(Arrays.asList("--no-sandbox", "--disable-extensions", "--disable-gpu")) + ) + ); + + private static final ThreadLocal browserContext = new ThreadLocal<>(); + + private static final ThreadLocal page = new ThreadLocal<>(); + + @Before(order = 100) + public void setUpBrowserContext() { + browserContext.set(browser.get().newContext()); + page.set(browserContext.get().newPage()); + } + + @After + public void closeContext() { + browserContext.get().close(); + } + + @AfterAll + public static void tearDown() { + browser.get().close(); + browser.remove(); + + playwright.get().close(); + playwright.remove(); + } + + public static Page getPage() { + return page.get(); + } +} diff --git a/src/test/java/com/serenitydojo/playwright/toolshop/cucumber/stepdefinitions/ProductCatalogStepDefinitions.java b/src/test/java/com/serenitydojo/playwright/toolshop/cucumber/stepdefinitions/ProductCatalogStepDefinitions.java new file mode 100644 index 0000000..e688ca4 --- /dev/null +++ b/src/test/java/com/serenitydojo/playwright/toolshop/cucumber/stepdefinitions/ProductCatalogStepDefinitions.java @@ -0,0 +1,88 @@ +package com.serenitydojo.playwright.toolshop.cucumber.stepdefinitions; + +import com.serenitydojo.playwright.toolshop.catalog.pageobjects.NavBar; +import com.serenitydojo.playwright.toolshop.catalog.pageobjects.ProductList; +import com.serenitydojo.playwright.toolshop.catalog.pageobjects.SearchComponent; +import com.serenitydojo.playwright.toolshop.fixtures.ProductSummary; +import io.cucumber.datatable.DataTable; +import io.cucumber.java.Before; +import io.cucumber.java.DataTableType; +import io.cucumber.java.en.And; +import io.cucumber.java.en.Given; +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; +import io.cucumber.java.eo.Se; +import io.cucumber.messages.types.Product; +import org.assertj.core.api.Assertions; + +import java.util.List; +import java.util.Map; + +public class ProductCatalogStepDefinitions { + + NavBar navBar; + SearchComponent searchComponent; + ProductList productList; + + @Before + public void setupPageObjects() { + navBar = new NavBar(PlaywrightCucumberFixtures.getPage()); + searchComponent = new SearchComponent(PlaywrightCucumberFixtures.getPage()); + productList = new ProductList(PlaywrightCucumberFixtures.getPage()); + } + + @Given("Sally is on the home page") + public void sally_is_on_the_home_page() { + navBar.openHomePage(); + } + + @When("she searches for {string}") + public void she_searches_for(String searchTerm) { + searchComponent.searchBy(searchTerm); + } + + @Then("the {string} product should be displayed") + public void the_product_should_be_displayed(String productName) { + var matchingProducts = productList.getProductNames(); + Assertions.assertThat(matchingProducts).contains(productName); + } + + @DataTableType + public ProductSummary productSummaryRow(Map productData) { + return new ProductSummary(productData.get("Product"),productData.get("Price")); + } + + @Then("the following products should be displayed:") + public void theFollowingProductsShouldBeDisplayed(List expectedProductSummaries) { + List matchingProducts = productList.getProductSummaries(); + Assertions.assertThat(matchingProducts).containsExactlyInAnyOrderElementsOf(expectedProductSummaries); + } + + @Then("no products should be displayed") + public void noProductsShouldBeDisplayed() { + List matchingProducts = productList.getProductSummaries(); + Assertions.assertThat(matchingProducts).isEmpty(); + } + + @And("the message {string} should be displayed") + public void theMessageShouldBeDisplayed(String messageText) { + String completionMessage = productList.getSearchCompletedMessage(); + Assertions.assertThat(completionMessage).isEqualTo(messageText); + } + + @And("she filters by {string}") + public void sheFiltersBy(String filterName) { + searchComponent.filterBy(filterName); + } + + @When("she sorts by {string}") + public void sheSortsBy(String sortFilter) { + searchComponent.sortBy(sortFilter); + } + + @Then("the first product displayed should be {string}") + public void theFirstProductDisplayedShouldBe(String firstProductName) { + List productNames = productList.getProductNames(); + Assertions.assertThat(productNames).startsWith(firstProductName); + } +} diff --git a/src/test/java/com/serenitydojo/playwright/toolshop/domain/Address.java b/src/test/java/com/serenitydojo/playwright/toolshop/domain/Address.java new file mode 100644 index 0000000..968a507 --- /dev/null +++ b/src/test/java/com/serenitydojo/playwright/toolshop/domain/Address.java @@ -0,0 +1,4 @@ +package com.serenitydojo.playwright.toolshop.domain; + +public record Address(String street, String city, String state, String country, String postal_code) { +} diff --git a/src/test/java/com/serenitydojo/playwright/toolshop/domain/User.java b/src/test/java/com/serenitydojo/playwright/toolshop/domain/User.java new file mode 100644 index 0000000..618719a --- /dev/null +++ b/src/test/java/com/serenitydojo/playwright/toolshop/domain/User.java @@ -0,0 +1,125 @@ +package com.serenitydojo.playwright.toolshop.domain; + +import net.datafaker.Faker; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +public record User( + String first_name, + String last_name, + Address address, + String phone, + String dob, + String password, + String email) { + public static User randomUser() { + Faker fake = new Faker(); + + int year = fake.number().numberBetween(1970, 2000); + int month = fake.number().numberBetween(1, 12); + int day = fake.number().numberBetween(1, 28); + LocalDate date = LocalDate.of(year, month, day); + String formattedDate = date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + + return new User( + fake.name().firstName(), + fake.name().lastName(), + new Address( + fake.address().streetName(), + fake.address().city(), + fake.address().state(), + fake.address().country(), + fake.address().postcode() + ), + fake.phoneNumber().phoneNumber(), + formattedDate, + "Az123!&xyz", + fake.internet().emailAddress() + ); + } + + public User withPassword(String password) { + return new User( + first_name, + last_name, + address, + phone, + dob, + password, + email); + } + + public User withFirstName(String first_name) { + return new User(first_name,last_name,address,phone,dob,password,email); + } + + public static User randomUserWithNullField(String fieldName) { + + User user = randomUser(); + + switch (fieldName.toLowerCase()) { + + case "firstname": + return new User( + null, + user.last_name(), + user.address(), + user.phone(), + user.dob(), + user.password(), + user.email() + ); + + case "lastname": + return new User( + user.first_name(), + null, + user.address(), + user.phone(), + user.dob(), + user.password(), + user.email() + ); + + case "phone": + return new User( + user.first_name(), + user.last_name(), + user.address(), + null, + user.dob(), + user.password(), + user.email() + ); + + case "email": + return new User( + user.first_name(), + user.last_name(), + user.address(), + user.phone(), + user.dob(), + user.password(), + null + ); + + case "password": + return new User( + user.first_name(), + user.last_name(), + user.address(), + user.phone(), + user.dob(), + null, + user.email() + ); + + default: + throw new IllegalArgumentException( + "Unknown field: " + fieldName + ); + } + } + +} diff --git a/src/test/java/com/serenitydojo/playwright/toolshop/fixtures/PlaywrightTestCase.java b/src/test/java/com/serenitydojo/playwright/toolshop/fixtures/PlaywrightTestCase.java new file mode 100644 index 0000000..011b3bc --- /dev/null +++ b/src/test/java/com/serenitydojo/playwright/toolshop/fixtures/PlaywrightTestCase.java @@ -0,0 +1,52 @@ +package com.serenitydojo.playwright.toolshop.fixtures; + +import com.microsoft.playwright.*; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +import java.util.Arrays; + +public abstract class PlaywrightTestCase { + + protected static ThreadLocal playwright + = ThreadLocal.withInitial(() -> { + Playwright playwright = Playwright.create(); + playwright.selectors().setTestIdAttribute("data-test"); + return playwright; + } + ); + + protected static ThreadLocal browser = ThreadLocal.withInitial(() -> + playwright.get().chromium().launch( + new BrowserType.LaunchOptions() + .setHeadless(false) + .setArgs(Arrays.asList("--no-sandbox", "--disable-extensions", "--disable-gpu")) + ) + ); + + protected BrowserContext browserContext; + + protected Page page; + + @BeforeEach + void setUpBrowserContext() { + browserContext = browser.get().newContext(); + page = browserContext.newPage(); + } + + @AfterEach + void closeContext() { + browserContext.close(); + } + + @AfterAll + static void tearDown() { + browser.get().close(); + browser.remove(); + + playwright.get().close(); + playwright.remove(); + } + +} diff --git a/src/test/java/com/serenitydojo/playwright/toolshop/fixtures/ProductSummary.java b/src/test/java/com/serenitydojo/playwright/toolshop/fixtures/ProductSummary.java new file mode 100644 index 0000000..3688cc1 --- /dev/null +++ b/src/test/java/com/serenitydojo/playwright/toolshop/fixtures/ProductSummary.java @@ -0,0 +1,3 @@ +package com.serenitydojo.playwright.toolshop.fixtures; + +public record ProductSummary(String name, String price) {} diff --git a/src/test/java/com/serenitydojo/playwright/toolshop/login/LoginPage.java b/src/test/java/com/serenitydojo/playwright/toolshop/login/LoginPage.java new file mode 100644 index 0000000..6a67535 --- /dev/null +++ b/src/test/java/com/serenitydojo/playwright/toolshop/login/LoginPage.java @@ -0,0 +1,33 @@ +package com.serenitydojo.playwright.toolshop.login; + +import com.microsoft.playwright.Page; +import com.microsoft.playwright.options.AriaRole; +import com.serenitydojo.playwright.toolshop.domain.User; + +public class LoginPage { + private final Page page; + + public LoginPage(Page page) { + this.page = page; + } + + public void open() { + page.navigate("https://practicesoftwaretesting.com/auth/login"); + } + + public void loginAs(User user) { + page.getByPlaceholder("Your email").fill(user.email()); + page.getByPlaceholder("Your password").fill(user.password()); + page.getByRole(AriaRole.BUTTON, + new Page.GetByRoleOptions().setName("Login")).click(); + + } + + public String title() { + return page.getByTestId("page-title").textContent(); + } + + public String loginErrorMessage() { + return page.getByTestId("login-error").textContent(); + } +} \ No newline at end of file diff --git a/src/test/java/com/serenitydojo/playwright/toolshop/login/LoginWithRegisteredUserTest.java b/src/test/java/com/serenitydojo/playwright/toolshop/login/LoginWithRegisteredUserTest.java new file mode 100644 index 0000000..3074c2a --- /dev/null +++ b/src/test/java/com/serenitydojo/playwright/toolshop/login/LoginWithRegisteredUserTest.java @@ -0,0 +1,42 @@ +package com.serenitydojo.playwright.toolshop.login; + +import com.serenitydojo.playwright.toolshop.domain.User; +import com.serenitydojo.playwright.toolshop.fixtures.PlaywrightTestCase; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class LoginWithRegisteredUserTest extends PlaywrightTestCase { + + @Test + @DisplayName("Should be able to login with a registered user") + void should_login_with_registered_user() { + // Register a user via the API + User user = User.randomUser(); + UserAPIClient userAPIClient = new UserAPIClient(page); + userAPIClient.registerUser(user); + + // Login via the login page + LoginPage loginPage = new LoginPage(page); + loginPage.open(); + loginPage.loginAs(user); + + // Check that we are on the right account page + assertThat(loginPage.title()).isEqualTo("My account"); + } + + @Test + @DisplayName("Should reject a user if they provide a wrong password") + void should_reject_user_with_invalid_password() { + User user = User.randomUser(); + UserAPIClient userAPIClient = new UserAPIClient(page); + userAPIClient.registerUser(user); + + LoginPage loginPage = new LoginPage(page); + loginPage.open(); + loginPage.loginAs(user.withPassword("wrong-password")); + + assertThat(loginPage.loginErrorMessage()).isEqualTo("Invalid email or password"); + } +} \ No newline at end of file diff --git a/src/test/java/com/serenitydojo/playwright/toolshop/login/RegisterUserAPITest.java b/src/test/java/com/serenitydojo/playwright/toolshop/login/RegisterUserAPITest.java new file mode 100644 index 0000000..cad4499 --- /dev/null +++ b/src/test/java/com/serenitydojo/playwright/toolshop/login/RegisterUserAPITest.java @@ -0,0 +1,121 @@ +package com.serenitydojo.playwright.toolshop.login; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.microsoft.playwright.APIRequest; +import com.microsoft.playwright.APIRequestContext; +import com.microsoft.playwright.APIResponse; +import com.microsoft.playwright.Playwright; +import com.microsoft.playwright.junit.UsePlaywright; +import com.microsoft.playwright.options.RequestOptions; +import com.serenitydojo.playwright.toolshop.domain.User; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@UsePlaywright +public class RegisterUserAPITest { + + private APIRequestContext request; + private Gson gson = new Gson(); + + @BeforeEach + void setup(Playwright playwright) { + request = playwright.request().newContext( + new APIRequest.NewContextOptions().setBaseURL("https://api.practicesoftwaretesting.com") + ); + } + + @Test + void first_name_is_mandatory() { + User userWithNoName = User.randomUser().withFirstName(null); + + var response = request.post("/users/register", + RequestOptions.create() + .setHeader("Content-Type", "application/json") + .setData(userWithNoName) + ); + + assertSoftly(softly -> { + softly.assertThat(response.status()).isEqualTo(422); + JsonObject responseObject = gson.fromJson(response.text(), JsonObject.class); + softly.assertThat(responseObject.has("first_name")).isTrue(); + String errorMessage = responseObject.get("first_name").getAsString(); + softly.assertThat(errorMessage).isEqualTo("The first name field is required."); + }); + } + + + @AfterEach + void tearDown() { + if (request != null) { + request.dispose(); + } + } + + @Test + void should_register_user() { + User validUser = User.randomUser(); + + var response = request.post("/users/register", + RequestOptions.create() + .setHeader("Content-Type", "application/json") + .setData(validUser) + ); + + String responseBody = response.text(); + User createdUser = gson.fromJson(responseBody, User.class); + + JsonObject responseObject = gson.fromJson(responseBody, JsonObject.class); + + assertSoftly(softly -> { + softly.assertThat(response.status()) + .as("Registration should return 201 created status code") + .isEqualTo(201); + + softly.assertThat(createdUser) + .as("Created user should match the specified user without the password") + .isEqualTo(validUser.withPassword(null)); + + assertThat(responseObject.has("password")) + .as("No password should be returned") + .isFalse(); + + softly.assertThat(responseObject.get("id").getAsString()) + .as("Registered user should have an id") + .isNotEmpty(); + + softly.assertThat(response.headers().get("content-type")).contains("application/json"); + }); + } + + + @Test + void should_not_register_user() { + User invalidUser = User.randomUserWithNullField("firstName"); + + var response = request.post("/users/register", + RequestOptions.create() + .setHeader("Content-Type", "application/json") + .setData(invalidUser) + ); + + String responseBody = response.text(); +// User createdUser = gson.fromJson(responseBody, User.class); + assertSoftly(softly -> { + softly.assertThat(responseBody).isEqualTo("{\"first_name\":[\"The first name field is required.\"]}") + .as("First name field is required"); + softly.assertThat(response.status()).isEqualTo(422); + }); + + + JsonObject responseObject = gson.fromJson(responseBody, JsonObject.class); + System.out.println(responseObject); + } + +} diff --git a/src/test/java/com/serenitydojo/playwright/toolshop/login/UserAPIClient.java b/src/test/java/com/serenitydojo/playwright/toolshop/login/UserAPIClient.java new file mode 100644 index 0000000..d279459 --- /dev/null +++ b/src/test/java/com/serenitydojo/playwright/toolshop/login/UserAPIClient.java @@ -0,0 +1,27 @@ +package com.serenitydojo.playwright.toolshop.login; + +import com.microsoft.playwright.Page; +import com.microsoft.playwright.options.RequestOptions; +import com.serenitydojo.playwright.toolshop.domain.User; + +public class UserAPIClient { + private final Page page; + + private static final String REGISTER_USER = "https://api.practicesoftwaretesting.com/users/register"; + + public UserAPIClient(Page page) { + this.page = page; + } + + public void registerUser(User user) { + var response = page.request().post( + REGISTER_USER, + RequestOptions.create() + .setData(user) + .setHeader("Content-Type", "application/json") + .setHeader("Accept", "application/json")); + if (response.status() != 201) { + throw new IllegalStateException("Could not create user: " + response.text()); + } + } +} diff --git a/src/test/resources/allure.properties b/src/test/resources/allure.properties new file mode 100644 index 0000000..6c1e0bb --- /dev/null +++ b/src/test/resources/allure.properties @@ -0,0 +1 @@ +allure.results.directory=target/allure-results \ No newline at end of file diff --git a/src/test/resources/data/sample-data.txt b/src/test/resources/data/sample-data.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/test/resources/features/catalog/product_catalog.feature b/src/test/resources/features/catalog/product_catalog.feature new file mode 100644 index 0000000..25bfec7 --- /dev/null +++ b/src/test/resources/features/catalog/product_catalog.feature @@ -0,0 +1,50 @@ +Feature: Product Catalog + + As a customer, + I want to easily search, filter, and sort products in the catalog + So that I can find what I need quickly. + + Sally is an online shopper + + Rule: Customers should be able to search for products by name + Example: The one where Sally searches for an Adjustable Wrench + Given Sally is on the home page + When she searches for "Adjustable Wrench" + Then the "Adjustable Wrench" product should be displayed + + Example: The one where Sally searches for a more general term + Given Sally is on the home page + When she searches for "saw" + Then the following products should be displayed: + | Product | Price | + | Wood Saw | $12.18 | + | Circular Saw | $80.19 | + + Example: The one where Sally searches for a product that doesn't exist + Given Sally is on the home page + When she searches for "Product-Does-Not-Exist" + Then no products should be displayed + And the message "There are no products found." should be displayed + + + Rule: Customers should be able to narrow downs their search by category + Example: The one where Sally only wants to see Hand Saws + Given Sally is on the home page + When she searches for "saw" + And she filters by "Hand Saw" + Then the following products should be displayed: + | Product | Price | + | Wood Saw | $12.18 | + + + Rule: Customers should be able to sort products by various criteria + Scenario Outline: Sally sorts by different criteria + Given Sally is on the home page + When she sorts by "" + Then the first product displayed should be "" + Examples: + | Sort | First Product | + | Name (A - Z) | Adjustable Wrench | + | Name (Z - A) | Wood Saw | + | Price (High - Low) | Drawer Tool Cabinet | + | Price (Low - High) | Washers | diff --git a/src/test/resources/junit-platform.properties b/src/test/resources/junit-platform.properties new file mode 100644 index 0000000..20eb13c --- /dev/null +++ b/src/test/resources/junit-platform.properties @@ -0,0 +1,13 @@ +junit.jupiter.execution.parallel.enabled=true + +junit.jupiter.execution.parallel.mode.default=concurrent +junit.jupiter.execution.parallel.mode.classes.default=concurrent +junit.jupiter.execution.parallel.console.mode=verbose + +junit.jupiter.execution.parallel.config.strategy=dynamic +junit.jupiter.execution.parallel.config.dynamic.factor=4 + +cucumber.execution.parallel.enabled=true +cucumber.execution.parallel.config.strategy=fixed +cucumber.execution.parallel.config.fixed.parallelism=4 +cucumber.execution.parallel.config.fixed.max-pool-size=4 diff --git a/test-results/com.serenitydojo.playwright.ASimplePlaywrightTest.shouldShowSearchTermsInTheTitle-chromium/trace.zip b/test-results/com.serenitydojo.playwright.ASimplePlaywrightTest.shouldShowSearchTermsInTheTitle-chromium/trace.zip new file mode 100644 index 0000000..a8d5997 Binary files /dev/null and b/test-results/com.serenitydojo.playwright.ASimplePlaywrightTest.shouldShowSearchTermsInTheTitle-chromium/trace.zip differ diff --git a/test-results/com.serenitydojo.playwright.ASimplePlaywrightTest.shouldShowThePageTitle-chromium/trace.zip b/test-results/com.serenitydojo.playwright.ASimplePlaywrightTest.shouldShowThePageTitle-chromium/trace.zip new file mode 100644 index 0000000..e8c7aa3 Binary files /dev/null and b/test-results/com.serenitydojo.playwright.ASimplePlaywrightTest.shouldShowThePageTitle-chromium/trace.zip differ