diff --git a/.github/workflows/playwright-tests.yml b/.github/workflows/playwright-tests.yml
index e702f70f..c49aa218 100644
--- a/.github/workflows/playwright-tests.yml
+++ b/.github/workflows/playwright-tests.yml
@@ -24,10 +24,10 @@ jobs:
node-version: '20'
# Step 2: Set up JDK 17 (adjust if you're using a different version)
- - name: Set up JDK 17
+ - name: Set up JDK 21
uses: actions/setup-java@v3
with:
- java-version: '17'
+ java-version: '21'
distribution: 'adopt'
# Step 3: Install Maven 3.9.9
@@ -35,9 +35,9 @@ jobs:
uses: s4u/setup-maven-action@v1.7.0
with:
checkout-fetch-depth: 0
- java-version: 17
+ java-version: 21
java-distribution: temurin
- maven-version: 3.9.9
+ maven-version: 3.9.16
# Step 4: Verify Maven installation
- name: Verify Maven version
@@ -57,4 +57,20 @@ jobs:
# Step 6: Run Maven to execute Playwright tests
- name: Run Playwright Tests
- run: mvn verify
\ No newline at end of file
+ run: mvn verify
+
+ # Step 7: Archive any trace files
+ - name: Archive trace files
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: traces
+ path: target/traces/*.zip
+
+ # Step 8: Deploy Allure report to GitHub Pages
+ - name: Deploy Allure Report to GitHub Pages
+ if: always() # Ensure this step runs even if tests fail
+ uses: peaceiris/actions-gh-pages@v3
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: target/site/allure-maven-plugin
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 497ee081..42585d46 100644
--- a/pom.xml
+++ b/pom.xml
@@ -9,24 +9,257 @@
1.0-SNAPSHOT
- 21
- 21
+ 21
UTF-8
+
+ 2.35.2
+ 2.15.2
+ 1.60.0
+ 1.9.24
+ 5.14.2
+ 7.34.4
+ 5.3.11
com.microsoft.playwright
playwright
- 1.60.0
+ ${playwright.version}
+ test
+
+
+ io.qameta.allure
+ allure-cucumber7-jvm
test
org.junit.jupiter
junit-jupiter
- 5.11.3
test
+
+
+ org.junit.platform
+ junit-platform-suite
+ test
+
+
+ io.cucumber
+ cucumber-java
+ test
+
+
+ io.cucumber
+ cucumber-junit-platform-engine
+ test
+
+
+ io.cucumber
+ cucumber-junit
+ ${cucumber.version}
+ test
+
+
+ org.assertj
+ assertj-core
+ 3.27.7
+ test
+
+
+ io.qameta.allure
+ allure-jupiter
+ test
+
+
+
+ net.datafaker
+ datafaker
+ 2.7.0
+ test
+
+
+ org.aspectj
+ aspectjweaver
+ ${aspectj.version}
+ test
+
+
+ org.apache.commons
+ commons-lang3
+ 3.18.0
+
+
+
+ net.serenity-bdd
+ serenity-core
+
+
+
+ net.serenity-bdd
+ serenity-junit5
+ test
+
+
+
+ net.serenity-bdd
+ serenity-cucumber
+ test
+
+
+
+ net.serenity-bdd
+ serenity-ensure
+
+
+
+ net.serenity-bdd
+ serenity-playwright
+ test
+
+
+
+
+
+
+ org.junit
+ junit-bom
+ ${junit.version}
+ pom
+ import
+
+
+
+ io.cucumber
+ cucumber-bom
+ ${cucumber.version}
+ pom
+ import
+
+
+
+ net.serenity-bdd
+ serenity-bom
+ ${serenity.version}
+ pom
+ import
+
+
+
+
+ io.qameta.allure
+ allure-bom
+ ${allure.version}
+ pom
+ import
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.14.0
+
+
+ ${maven.compiler.release}
+
+
+ -parameters
+
+
+ true
+
+
+ /Users/erhanbatu/Library/Java/JavaVirtualMachines/temurin-21.0.11/Contents/Home/bin/javac
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.5.6
+
+ true
+
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+ 3.5.6
+
+
+
+
+ integration-test
+ verify
+
+
+
+
+
+
+ /Users/erhanbatu/Library/Java/JavaVirtualMachines/temurin-21.0.11/Contents/Home/bin/java
+
+
+
+
+
+
+
+ **/*Test.java
+ **/*Tests.java
+
+
+
+
+
+
+
+
+
+
+
+ net.serenity-bdd.maven.plugins
+ serenity-maven-plugin
+ ${serenity.version}
+
+
+
+ serenity-reports
+ post-integration-test
+
+ aggregate
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/react-todomvc/Dockerfile b/react-todomvc/Dockerfile
new file mode 100644
index 00000000..88e98c46
--- /dev/null
+++ b/react-todomvc/Dockerfile
@@ -0,0 +1,7 @@
+FROM node:16
+WORKDIR /app
+COPY package*.json ./
+RUN npm install
+COPY . .
+EXPOSE 7002
+CMD ["npm","run","serve"]
\ 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
index c50e7ea0..5fc6f5f9 100644
--- a/src/test/java/com/serenitydojo/playwright/ASimplePlaywrightTest.java
+++ b/src/test/java/com/serenitydojo/playwright/ASimplePlaywrightTest.java
@@ -1,15 +1,67 @@
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;
+import com.microsoft.playwright.*;
+import com.microsoft.playwright.junit.UsePlaywright;
+import org.junit.jupiter.api.*;
+import java.util.Arrays;
+
+//This annotation allows you to do test without creating playwright environment and browser
+//@UsePlaywright
public class ASimplePlaywrightTest {
+ private static Playwright playwright;
+ private static Browser browser;
+ private static BrowserContext browserContext;
+ Page page;
+
+ @BeforeAll
+ public static void setUpBrowser(){
+ playwright = Playwright.create();
+ browser = playwright.chromium().launch(
+ new BrowserType.LaunchOptions()
+ .setHeadless(false)
+ .setArgs(Arrays.asList("--no-sandbox", "--disable-extensions","--disable-gpu"))
+ );
+ //this allows paralel execution
+ browserContext = browser.newContext();
+
+ }
+
+ @BeforeEach
+ public void setUp(){
+ page = browser.newPage();
+ }
+
+ @AfterAll
+ public static void tearDown(){
+ browser.close();
+ playwright.close();
+ }
+
@Test
void shouldShowThePageTitle() {
// TODO: Write your first playwright test here
+
+ page.navigate("https://practicesoftwaretesting.com/");
+ String title = page.title();
+ Assertions.assertTrue(title.contains("Practice Software Testing"));
+
+ }
+
+ @Test
+ void shouldSearchByKeyword(){
+
+ page.navigate("https://practicesoftwaretesting.com/");
+ page.locator("[placeholder=Search]").fill("Pliers");
+ //this is basically sort of xpath
+ page.locator("button:has-text('Search')").click();
+
+ //this will look for the element with card
+ int matchingSearchResults = page.locator(".card").count();
+ System.out.println(matchingSearchResults);
+
+ Assertions.assertTrue(matchingSearchResults>0);
+
}
}
diff --git a/src/test/java/com/serenitydojo/playwright/AddingItemsToTheCartTest.java b/src/test/java/com/serenitydojo/playwright/AddingItemsToTheCartTest.java
new file mode 100644
index 00000000..4293f096
--- /dev/null
+++ b/src/test/java/com/serenitydojo/playwright/AddingItemsToTheCartTest.java
@@ -0,0 +1,47 @@
+package com.serenitydojo.playwright;
+
+import com.microsoft.playwright.*;
+import com.microsoft.playwright.junit.UsePlaywright;
+import com.microsoft.playwright.options.AriaRole;
+import org.junit.jupiter.api.*;
+
+import java.util.Arrays;
+import java.util.List;
+
+@UsePlaywright(HeadlessChromeOptions.class)
+public class AddingItemsToTheCartTest {
+
+ @DisplayName("Search for pliers")
+ @Test
+ void searchForPliers(Page page){
+ page.navigate("https://practicesoftwaretesting.com/");
+// page.locator("[placeholder='Search']").fill("pliers");
+ page.getByPlaceholder("Search").fill("pliers");
+ page.getByRole(AriaRole.BUTTON,
+ new Page.GetByRoleOptions().setName("Search")).click();
+
+ Locator products = page.locator(".card").filter(
+ new Locator.FilterOptions()
+ .setHas(page.getByAltText("Pliers"))
+ .setHasNot(page.getByText("Out of stock"))
+ );
+
+ List productNames = products.locator("h5").allTextContents();
+ System.out.println(productNames);
+
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+}
diff --git a/src/test/java/com/serenitydojo/playwright/AnAnnotatedPlaywrightTest.java b/src/test/java/com/serenitydojo/playwright/AnAnnotatedPlaywrightTest.java
new file mode 100644
index 00000000..a7622d38
--- /dev/null
+++ b/src/test/java/com/serenitydojo/playwright/AnAnnotatedPlaywrightTest.java
@@ -0,0 +1,57 @@
+package com.serenitydojo.playwright;
+
+import com.microsoft.playwright.Browser;
+import com.microsoft.playwright.BrowserType;
+import com.microsoft.playwright.Page;
+import com.microsoft.playwright.Playwright;
+import com.microsoft.playwright.junit.Options;
+import com.microsoft.playwright.junit.OptionsFactory;
+import com.microsoft.playwright.junit.UsePlaywright;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+
+@UsePlaywright(AnAnnotatedPlaywrightTest.MyOptions.class)
+public class AnAnnotatedPlaywrightTest {
+
+ public static class MyOptions implements OptionsFactory{
+
+ @Override
+ public Options getOptions() {
+ return new Options()
+ .setHeadless(false)
+ .setLaunchOptions(
+ new BrowserType.LaunchOptions()
+ .setArgs(Arrays.asList("--no-sandbox", "--disable-extensions","--disable-gpu"))
+ );
+ }
+ }
+ @Test
+ void shouldShowThePageTitle(Page page) {
+ // TODO: Write your first playwright test here
+
+ page.navigate("https://practicesoftwaretesting.com/");
+ String title = page.title();
+ Assertions.assertTrue(title.contains("Practice Software Testing"));
+
+ }
+
+ @Test
+ void shouldSearchByKeyword(Page page){
+
+ page.navigate("https://practicesoftwaretesting.com/");
+ page.locator("[placeholder=Search]").fill("Pliers");
+ //this is basically sort of xpath
+ page.locator("button:has-text('Search')").click();
+
+ //this will look for the element with card
+ int matchingSearchResults = page.locator(".card").count();
+ System.out.println(matchingSearchResults);
+
+ Assertions.assertTrue(matchingSearchResults>0);
+
+ }
+}
diff --git a/src/test/java/com/serenitydojo/playwright/HeadlessChromeOptions.java b/src/test/java/com/serenitydojo/playwright/HeadlessChromeOptions.java
new file mode 100644
index 00000000..44ac2a39
--- /dev/null
+++ b/src/test/java/com/serenitydojo/playwright/HeadlessChromeOptions.java
@@ -0,0 +1,23 @@
+package com.serenitydojo.playwright;
+
+import com.microsoft.playwright.BrowserType;
+import com.microsoft.playwright.junit.Options;
+import com.microsoft.playwright.junit.OptionsFactory;
+
+import java.util.Arrays;
+
+public class HeadlessChromeOptions implements OptionsFactory {
+
+
+ @Override
+ public Options getOptions() {
+ return new Options().setLaunchOptions(
+ new BrowserType.LaunchOptions().setHeadless(false)
+ .setArgs(Arrays.asList("--no-sandbox", "--disable-extensions", "--disable-gpu"))
+ )
+ .setTestIdAttribute("data-test");
+ }
+
+
+
+}
diff --git a/src/test/java/com/serenitydojo/playwright/PlaywrightAssertionsTest.java b/src/test/java/com/serenitydojo/playwright/PlaywrightAssertionsTest.java
new file mode 100644
index 00000000..56e44a12
--- /dev/null
+++ b/src/test/java/com/serenitydojo/playwright/PlaywrightAssertionsTest.java
@@ -0,0 +1,112 @@
+package com.serenitydojo.playwright;
+
+import com.microsoft.playwright.Page;
+import com.microsoft.playwright.junit.UsePlaywright;
+import com.microsoft.playwright.options.LoadState;
+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;
+
+import java.util.Comparator;
+import java.util.List;
+
+import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
+
+@UsePlaywright(HeadlessChromeOptions.class)
+public class PlaywrightAssertionsTest {
+
+ @DisplayName("Making assertions about the contents of a field")
+ @Nested
+ class LocationgElementsUsingCSS {
+
+ @BeforeEach
+ void openContactPage(Page page) {
+ page.navigate("https://practicesoftwaretesting.com/contact");
+ }
+
+ @DisplayName("Checking the value of a field")
+ @Test
+ void fieldValues(Page page){
+
+ var firstNameField = page.getByLabel("First Name");
+
+ firstNameField.fill("Erhan");
+
+ assertThat(firstNameField).hasValue("Erhan");
+
+ assertThat(firstNameField).not().isDisabled();
+ assertThat(firstNameField).isVisible();
+ assertThat(firstNameField).isEditable();
+
+ }
+
+
+
+ }
+
+
+ @DisplayName("Making assertions about data values")
+ @Nested
+ class MakingAssertionsAboutDataValues {
+
+ @BeforeEach
+ void openHomePage(Page page) {
+ page.navigate("https://practicesoftwaretesting.com");
+ page.waitForCondition(() -> page.getByTestId("product-name").count() > 0);
+ }
+
+ @Test
+ void allProductPricesShouldBeCorrectValues(Page page){
+
+ List prices = page.getByTestId("product-price")
+ .allTextContents()
+ .stream()
+ .map(price -> Double.parseDouble(price.replace("$","")))
+ .toList();
+
+ Assertions.assertThat(prices)
+ .isNotEmpty()
+ .allMatch(price -> price > 0)
+ .doesNotContain(0.0)
+ .allMatch(price -> price < 1000)
+ .allSatisfy(price ->
+ Assertions.assertThat(price).isGreaterThan(0.0)
+ .isLessThan(1000.0));
+
+ }
+
+ @Test
+ void shouldSortInAlphabeticalOrder(Page page){
+
+ page.getByLabel("Sort").selectOption("Name (A - Z)");
+ page.waitForLoadState(LoadState.NETWORKIDLE);
+
+ List productNames = page.getByTestId("product-name").allTextContents();
+
+// Assertions.assertThat(productNames).isSortedAccordingTo(Comparator.naturalOrder());
+ Assertions.assertThat(productNames).isSortedAccordingTo(String.CASE_INSENSITIVE_ORDER);
+
+ }
+
+ @Test
+ void shouldSortInReverseAlphabeticalOrder(Page page){
+
+ page.getByLabel("Sort").selectOption("Name (Z - A)");
+ page.waitForLoadState(LoadState.NETWORKIDLE);
+
+ List productNames = page.getByTestId("product-name").allTextContents();
+
+ Assertions.assertThat(productNames).isSortedAccordingTo(Comparator.reverseOrder());
+
+ }
+
+ }
+
+
+
+
+
+
+}
diff --git a/src/test/java/com/serenitydojo/playwright/PlaywrightFormsTest.java b/src/test/java/com/serenitydojo/playwright/PlaywrightFormsTest.java
new file mode 100644
index 00000000..73f0dbf4
--- /dev/null
+++ b/src/test/java/com/serenitydojo/playwright/PlaywrightFormsTest.java
@@ -0,0 +1,161 @@
+package com.serenitydojo.playwright;
+
+import com.microsoft.playwright.Locator;
+import com.microsoft.playwright.Page;
+import com.microsoft.playwright.junit.UsePlaywright;
+import com.microsoft.playwright.options.AriaRole;
+import org.junit.jupiter.api.*;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.net.URISyntaxException;
+import java.nio.file.Path;
+import java.util.regex.Pattern;
+
+import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
+
+@UsePlaywright(HeadlessChromeOptions.class)
+public class PlaywrightFormsTest {
+
+ @DisplayName("Interacting with text fields")
+ @Nested
+ class WhenInteractingWithTextFields {
+
+ @BeforeEach
+ void openContactPage(Page page) {
+ page.navigate("https://practicesoftwaretesting.com/contact");
+ }
+
+ @DisplayName("Complete the form")
+ @Test
+ void completeForm(Page page) throws URISyntaxException {
+ var firstNameField = page.getByLabel("First name");
+ var lastNameField = page.getByLabel("Last name");
+ var emailNameField = page.getByLabel("Email");
+ var messageField = page.getByLabel("Message");
+ var subjectField = page.getByLabel("Subject");
+ var uploadField = page.getByLabel("Attachment");
+
+ firstNameField.fill("Erhan");
+ lastNameField.fill("Batu");
+ emailNameField.fill("erbatu@gmail.com");
+ messageField.fill("Hello World!");
+ subjectField.selectOption("Warranty");
+
+ Path fileToUpload = Path.of(
+ ClassLoader.getSystemResource("data/sample-data.txt").toURI()
+ );
+
+ page.setInputFiles("#attachment", fileToUpload);
+
+
+ assertThat(firstNameField).hasValue("Erhan");
+ assertThat(lastNameField).hasValue("Batu");
+ assertThat(subjectField).hasValue("warranty");
+
+ String uploadFile = uploadField.inputValue();
+ org.assertj.core.api.Assertions.assertThat(uploadFile).endsWith("sample-data.txt");
+
+
+
+ }
+
+ @DisplayName("Mandatory Fields")
+ @ParameterizedTest
+ @ValueSource(strings = {"First name", "Last name"})
+ void mandatoryFields(String fieldName, Page page){
+ var firstNameField = page.getByLabel("First name");
+ var lastNameField = page.getByLabel("Last name");
+ var emailNameField = page.getByLabel("Email");
+ var messageField = page.getByLabel("Message");
+ var subjectField = page.getByLabel("Subject");
+ var sendButton = page.getByText("Send");
+
+ firstNameField.fill("Erhan");
+ lastNameField.fill("Batu");
+ emailNameField.fill("erbatu@gmail.com");
+ messageField.fill("Hello World!");
+ subjectField.selectOption("Warranty");
+
+ page.getByLabel(fieldName).clear();
+
+ sendButton.click();
+
+ var errorMessage = page.getByRole(AriaRole.ALERT).getByText(fieldName + " is required");
+
+ assertThat(errorMessage).isVisible();
+
+
+ }
+
+ @DisplayName("Categories Tests")
+ @Test
+ void categoriesTests(Page page){
+
+ page.getByRole(AriaRole.BUTTON).getByText("Categories").click();
+ page.getByLabel("nav-categories").getByText("Power Tools").click();
+
+ assertThat(page).hasTitle(Pattern.compile(".*Power Tools.*"));
+
+ String circularSawPrice = page.locator(".card").filter(
+ new Locator.FilterOptions()
+ .setHasText("circular saw"))
+ .locator("[data-test='product-price']").innerText();
+ System.out.println(circularSawPrice);
+
+ Assertions.assertEquals(circularSawPrice, "$80.19");
+
+
+ }
+
+ @DisplayName("CheckBox Test")
+ @Test
+ void checkBoxTest(Page page){
+
+ page.getByRole(AriaRole.BUTTON).getByText("Categories").click();
+ page.getByLabel("nav-categories").getByText("Power Tools").click();
+
+ page.getByRole(AriaRole.LIST).locator(".checkbox")
+ .filter(
+ new Locator.FilterOptions().setHasText("Grinder")
+ ).locator("input[type='checkbox']").check();
+
+
+ assertThat(page.getByText("There are no products found")).isVisible();
+
+ page.getByRole(AriaRole.LIST).locator(".checkbox").filter(
+ new Locator.FilterOptions().setHasText("Sander")
+ ).locator("[name='category_id']").check();
+
+ Locator productCards = page.locator(".card-img-top");
+
+ assertThat(productCards).hasCount(3);
+
+ System.out.println(productCards.count());
+
+ }
+
+ @DisplayName("Dropdowns Test")
+ @Test
+ void dropDownTesting(Page page){
+
+ page.getByRole(AriaRole.BUTTON).getByText("Categories").click();
+ page.getByLabel("nav-categories").getByText("Power Tools").click();
+
+ page.getByRole(AriaRole.COMBOBOX).selectOption("Price (High - Low)");
+
+ }
+
+
+
+
+
+ }
+
+
+
+
+
+
+
+}
diff --git a/src/test/java/com/serenitydojo/playwright/PlaywrightLocators.java b/src/test/java/com/serenitydojo/playwright/PlaywrightLocators.java
new file mode 100644
index 00000000..c09d4eb5
--- /dev/null
+++ b/src/test/java/com/serenitydojo/playwright/PlaywrightLocators.java
@@ -0,0 +1,175 @@
+package com.serenitydojo.playwright;
+
+import com.microsoft.playwright.*;
+import com.microsoft.playwright.assertions.PlaywrightAssertions;
+import org.junit.jupiter.api.*;
+import org.junit.jupiter.api.parallel.Execution;
+import org.junit.jupiter.api.parallel.ExecutionMode;
+import org.junit.jupiter.api.Nested;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
+
+@Execution(ExecutionMode.SAME_THREAD)
+public class PlaywrightLocators {
+
+
+ protected static Playwright playwright;
+ protected static Browser browser;
+ protected static BrowserContext browserContext;
+
+ Page page;
+
+ @BeforeAll
+ static void setUpBrowser() {
+ playwright = Playwright.create();
+ browser = playwright.chromium().launch(
+ new BrowserType.LaunchOptions().setHeadless(true)
+ .setArgs(Arrays.asList("--no-sandbox", "--disable-extensions", "--disable-gpu"))
+ );
+ }
+
+ @BeforeEach
+ void setUp() {
+ browserContext = browser.newContext();
+ page = browserContext.newPage();
+ }
+
+ @AfterEach
+ void closeContext() {
+ browserContext.close();
+ }
+
+ @AfterAll
+ static void tearDown() {
+ browser.close();
+ playwright.close();
+ }
+
+ @DisplayName("Locating elements using CSS")
+ @Nested
+ class LocatingElementsUsingCSS {
+
+ @BeforeEach
+ void openContactPage() {
+ page.navigate("https://practicesoftwaretesting.com/contact");
+ }
+
+ @DisplayName("By id")
+ @Test
+ void locateTheFirstNameFieldByID() {
+ page.locator("#first_name").fill("Sarah-Jane");
+ assertThat(page.locator("#first_name")).hasValue("Sarah-Jane");
+ }
+
+ @DisplayName("By CSS class")
+ @Test
+ void locateTheSendButtonByCssClass() {
+ page.locator("#first_name").fill("Sarah-Jane");
+ page.locator(".btnSubmit").click();
+ List alertMessages = page.locator(".alert").allTextContents();
+ Assertions.assertTrue(!alertMessages.isEmpty());
+ }
+
+ @DisplayName("By attribute")
+ @Test
+ void locateTheSendButtonByAttribute() {
+ page.locator("[placeholder='Your last name *']").fill("Smith");
+ PlaywrightAssertions.assertThat(page.locator("#last_name")).hasValue("Smith");
+ }
+
+
+
+
+
+
+
+
+
+
+
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @DisplayName("Locating elements by text")
+ @Nested
+ class LocatingElementsByText {
+
+ @BeforeEach
+ void openTheCatalogPage() {
+ openPage();
+ }
+
+ @DisplayName("Locating an element by text contents")
+ @Test
+ void byText() {
+ page.getByText("Bolt Cutters").click();
+
+ assertThat(page.getByText("MightyCraft Hardware")).isVisible();
+ }
+
+ @DisplayName("Using alt text")
+ @Test
+ void byAltText() {
+ page.getByAltText("Combination Pliers").click();
+
+ assertThat(page.getByText("ForgeFlex Tools")).isVisible();
+ }
+
+ @DisplayName("Using title")
+ @Test
+ void byTitle() {
+ page.getByAltText("Combination Pliers").click();
+
+ page.getByTitle("Practice Software Testing - Toolshop").click();
+ }
+ }
+
+ @DisplayName("Locating elements by test Id")
+ @Nested
+ class LocatingElementsByTestID {
+
+ @BeforeAll
+ static void setTestId() {
+ playwright.selectors().setTestIdAttribute("data-test");
+ }
+
+ @DisplayName("Using a custom data-test field")
+ @Test
+ void byTestId() {
+ openPage();
+
+ playwright.selectors().setTestIdAttribute("data-test");
+
+ page.getByTestId("search-query").fill("Pliers");
+ page.getByTestId("search-submit").click();
+ }
+
+ }
+
+
+
+ private void openPage() {
+ page.navigate("https://practicesoftwaretesting.com");
+ }
+
+
+}
diff --git a/src/test/java/com/serenitydojo/playwright/PlaywrightWaitsTest.java b/src/test/java/com/serenitydojo/playwright/PlaywrightWaitsTest.java
new file mode 100644
index 00000000..90397442
--- /dev/null
+++ b/src/test/java/com/serenitydojo/playwright/PlaywrightWaitsTest.java
@@ -0,0 +1,172 @@
+package com.serenitydojo.playwright;
+
+
+import com.microsoft.playwright.Page;
+import com.microsoft.playwright.junit.UsePlaywright;
+import com.microsoft.playwright.options.AriaRole;
+import com.microsoft.playwright.options.WaitForSelectorState;
+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;
+
+import java.util.Comparator;
+import java.util.List;
+
+import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
+
+@UsePlaywright(HeadlessChromeOptions.class)
+public class PlaywrightWaitsTest {
+
+
+ @DisplayName("Explicit Wait")
+ @Nested
+ class ExplicitWait {
+ @BeforeEach
+ void openContactPage(Page page) {
+ page.navigate("https://practicesoftwaretesting.com");
+ page.waitForSelector("[data-test=product-name]");
+ page.waitForSelector(".card-img-top");
+ }
+
+ @Test
+ void shouldShowAllProductName(Page page){
+ List productNames = page.getByTestId("product-name").allInnerTexts();
+ Assertions.assertThat(productNames).contains("Pliers", "Bolt Cutters");
+
+ }
+
+ @Test
+ void shouldShowAllProductImages(Page page){
+ List productImageTitles = page.locator(".card-img-top").all()
+ .stream()
+ .map(img -> img.getAttribute("alt"))
+ .toList();
+
+ Assertions.assertThat(productImageTitles).contains("Bolt Cutters");
+ }
+
+ }
+
+ @DisplayName("Implicit Wait")
+ @Nested
+ class ImplicitWait{
+
+ @BeforeEach
+ void openContactPage(Page page) {
+ page.navigate("https://practicesoftwaretesting.com");
+
+ }
+
+ @Test
+ void shouldWaitForTheFilterCheckboxes(Page page){
+
+ var screwDriverFilter = page.getByLabel("Screwdriver");
+
+ screwDriverFilter.click();
+
+ assertThat(screwDriverFilter).isChecked();
+ }
+
+ @Test
+ void shouldFilterProductsByCategory(Page page){
+
+ page.getByRole(AriaRole.MENUBAR).getByText("Categories").click();
+ page.getByRole(AriaRole.MENUBAR).getByText("Power Tools").click();
+
+ page.waitForSelector(".card",
+ new Page.WaitForSelectorOptions().setState(WaitForSelectorState.VISIBLE).setTimeout(2000)
+ );
+ var filteredProducts = page.getByTestId("product-name").allInnerTexts();
+
+ Assertions.assertThat(filteredProducts).contains("Sheet Sander");
+
+ }
+
+
+
+ }
+
+
+ @Nested
+ class WaitingForElementsToAppearAndDisappear{
+
+ @BeforeEach
+ void openContactPage(Page page) {
+ page.navigate("https://practicesoftwaretesting.com");
+
+ }
+
+ @Test
+ void shouldDisplayToasterMessage(Page page){
+
+ page.getByText("Bolt Cutters").click();
+ page.getByText("Add to cart").click();
+
+ assertThat(page.getByRole(AriaRole.ALERT)).isVisible();
+ assertThat(page.getByRole(AriaRole.ALERT)).hasText("Product added to shopping cart.");
+
+ page.waitForCondition(() -> page.getByRole(AriaRole.ALERT).isHidden());
+
+ }
+
+ @Test
+ void shoudlUpdateTheCartItemCount(Page page){
+
+ page.getByText("Bolt Cutters").click();
+ page.getByText("Add to cart").click();
+
+ page.waitForCondition(() -> page.getByTestId("cart-quantity").textContent().equals("1"));
+ page.waitForSelector("[data-test=cart-quantity]:has-text('1')");
+
+ }
+
+
+
+
+ }
+
+
+ @Nested
+ class WaitingForAPICalls {
+
+ @Test
+ void sortByDescendingPrice(Page page) {
+ page.navigate("https://practicesoftwaretesting.com");
+
+ // Sort by descending price
+ // (Note: The API endpoint has evolved and is slightly different to the one in the video;
+ // it now sends the sort/page criteria in the request body rather than as URL query params)
+ page.waitForResponse(
+ response -> response.url().contains("/products")
+ && response.request().postData() != null
+ && response.request().postData().contains("\"sort\":\"price,desc\""),
+ () -> {
+ page.getByTestId("sort").selectOption("Price (High - Low)");
+ });
+
+ // Find all the prices on the page
+ var productPrices = page.getByTestId("product-price")
+ .allInnerTexts()
+ .stream()
+ .map(WaitingForAPICalls::extractPrice)
+ .toList();
+
+ // Are the prices in the correct order
+ Assertions.assertThat(productPrices)
+ .isNotEmpty()
+ .isSortedAccordingTo(Comparator.reverseOrder());
+ }
+
+ private static double extractPrice(String price) {
+ return Double.parseDouble(price.replace("$", ""));
+ }
+ }
+
+
+
+
+
+
+}
diff --git a/src/test/java/com/serenitydojo/playwright/todo/AddingAndDeletingTodoItemsTest.java b/src/test/java/com/serenitydojo/playwright/todo/AddingAndDeletingTodoItemsTest.java
new file mode 100644
index 00000000..65dac700
--- /dev/null
+++ b/src/test/java/com/serenitydojo/playwright/todo/AddingAndDeletingTodoItemsTest.java
@@ -0,0 +1,155 @@
+package com.serenitydojo.playwright.todo;
+
+import com.microsoft.playwright.Page;
+import com.microsoft.playwright.junit.UsePlaywright;
+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;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
+
+@DisplayName("Adding and deleting todo items to the list")
+@Feature("Adding and deleting todo items to the list")
+@UsePlaywright(ChromeHeadlessOptions.class)
+class AddingAndDeletingTodoItemsTest {
+
+ TodoMvcAppPage todoMvcApp;
+
+ @BeforeEach
+ void openApp(Page page) {
+ todoMvcApp = new TodoMvcAppPage(page);
+
+ todoMvcApp.open();
+ }
+
+ @Story("When the application starts")
+ @DisplayName("When the application starts")
+ @Nested
+ class WhenTheApplicationStarts {
+ @DisplayName("The list should be empty")
+ @Test
+ void the_list_should_initially_be_empty() {
+ Assertions.assertThat(todoMvcApp.todoItemsDisplayed()).isEmpty();
+ }
+
+ @DisplayName("The user should be prompted to enter a todo item")
+ @Test
+ void the_user_should_be_prompted_to_enter_a_value() {
+ assertThat(todoMvcApp.todoField()).isVisible();
+ assertThat(todoMvcApp.todoField()).hasAttribute("placeholder", "What needs to be done?");
+ }
+
+ }
+
+ @Story("When we want to add item to the list")
+ @DisplayName("When we want to add item to the list")
+ @Nested
+ class WhenAddingItems {
+
+ @DisplayName("We can add a single item")
+ @Test
+ void addingASingleItem() {
+ todoMvcApp.addTodoItem("Feed the cat");
+
+ Assertions.assertThat(todoMvcApp.todoItemsDisplayed()).containsExactly("Feed the cat");
+
+ }
+
+ @DisplayName("We can add multiple items")
+ @Test
+ void addingSeveralItem() {
+ todoMvcApp.addTodoItem("Feed the cat");
+ todoMvcApp.addTodoItem("Walk the dog");
+
+ Assertions.assertThat(todoMvcApp.todoItemsDisplayed()).containsExactly("Feed the cat", "Walk the dog");
+ }
+
+ @DisplayName("We can't add add an empty item")
+ @Test
+ void addingAnEmptyItem() {
+ todoMvcApp.addTodoItem("Feed the cat");
+ todoMvcApp.addTodoItem("");
+
+ Assertions.assertThat(todoMvcApp.todoItemsDisplayed()).containsExactly("Feed the cat");
+
+ }
+
+ @DisplayName("We can add duplicate items")
+ @Test
+ void addingDuplicateItem() {
+ todoMvcApp.addTodoItem("Feed the cat");
+ todoMvcApp.addTodoItem("Walk the dog");
+ todoMvcApp.addTodoItem("Feed the cat");
+
+ Assertions.assertThat(todoMvcApp.todoItemsDisplayed()).containsExactly("Feed the cat", "Walk the dog","Feed the cat");
+ }
+
+ @DisplayName("We can add items with non-English characters")
+ @ParameterizedTest
+ @CsvSource({
+ "Feed the cat", // English
+ "Alimentar al gato", // Spanish
+ "להאכיל את החתול", // Hebrew
+ "ให้อาหารแมว", // Thai
+ "喂猫", // Chinese
+ "إطعام القط", // Arabic
+ "кормить кошку", // Russian
+ "猫に餌をやる", // Japanese
+ "고양이 먹이기" // Korean
+ })
+ void addingNonEnglishItems(String item) {
+ todoMvcApp.addTodoItem(item);
+ Assertions.assertThat(todoMvcApp.todoItemsDisplayed()).containsExactly(item);
+ }
+ }
+
+ @Story("When we want to delete item in the list")
+ @DisplayName("When we want to delete item in the list")
+ @Nested
+ class WhenDeletingItems {
+
+ @DisplayName("We can delete an item in the middle of the list")
+ @Test
+ void deletingAnItemInTheMiddleOfTheList() {
+ todoMvcApp.addTodoItems("Feed the cat", "Walk the dog", "Buy some milk");
+
+ todoMvcApp.deleteItem("Walk the dog");
+
+ Assertions.assertThat(todoMvcApp.todoItemsDisplayed()).containsExactly("Feed the cat", "Buy some milk");
+
+ }
+
+
+ @DisplayName("We can delete an item at the end of the list")
+ @Test
+ void deletingAnItemAtTheEndOfTheList() {
+ todoMvcApp.addTodoItems("Feed the cat", "Walk the dog", "Buy some milk");
+
+ todoMvcApp.deleteItem("Buy some milk");
+
+ Assertions.assertThat(todoMvcApp.todoItemsDisplayed()).containsExactly("Feed the cat", "Walk the dog");
+
+ }
+
+
+
+ @DisplayName("We can delete an item at the start of the list")
+ @Test
+ void deletingAnItemAtTheStartOfTheList() {
+ todoMvcApp.addTodoItems("Feed the cat", "Walk the dog", "Buy some milk");
+
+ todoMvcApp.deleteItem("Feed the cat");
+
+ Assertions.assertThat(todoMvcApp.todoItemsDisplayed()).containsExactly("Walk the dog", "Buy some milk");
+
+ }
+
+ }
+
+}
diff --git a/src/test/java/com/serenitydojo/playwright/todo/ChromeHeadlessOptions.java b/src/test/java/com/serenitydojo/playwright/todo/ChromeHeadlessOptions.java
new file mode 100644
index 00000000..cdf89cd6
--- /dev/null
+++ b/src/test/java/com/serenitydojo/playwright/todo/ChromeHeadlessOptions.java
@@ -0,0 +1,19 @@
+package com.serenitydojo.playwright.todo;
+
+import com.microsoft.playwright.BrowserType;
+import com.microsoft.playwright.junit.Options;
+import com.microsoft.playwright.junit.OptionsFactory;
+
+import java.util.Arrays;
+
+public class ChromeHeadlessOptions implements OptionsFactory {
+ @Override
+ public Options getOptions() {
+ return new Options().setLaunchOptions(
+ new BrowserType.LaunchOptions()
+ .setArgs(Arrays.asList("--no-sandbox", "--disable-extensions", "--disable-gpu"))
+ )
+ .setHeadless(true)
+ .setTestIdAttribute("data-testid");
+ }
+}
diff --git a/src/test/java/com/serenitydojo/playwright/todo/CompletingTodoItemsTest.java b/src/test/java/com/serenitydojo/playwright/todo/CompletingTodoItemsTest.java
new file mode 100644
index 00000000..35126f38
--- /dev/null
+++ b/src/test/java/com/serenitydojo/playwright/todo/CompletingTodoItemsTest.java
@@ -0,0 +1,60 @@
+package com.serenitydojo.playwright.todo;
+
+import com.microsoft.playwright.Page;
+import com.microsoft.playwright.junit.UsePlaywright;
+import io.qameta.allure.Feature;
+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;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
+
+@DisplayName("Completing todo items to the list")
+@UsePlaywright(ChromeHeadlessOptions.class)
+@Feature("Completing todo items to the list")
+class CompletingTodoItemsTest {
+
+ TodoMvcAppPage todoMvcApp;
+
+ @BeforeEach
+ void openApp(Page page) {
+ todoMvcApp = new TodoMvcAppPage(page);
+
+ todoMvcApp.open();
+ }
+
+ @DisplayName("Completed items should be marked as completed")
+ @Test
+ void completedItemsShouldBeMarkedAsCompleted() {
+ todoMvcApp.addTodoItems("Feed the cat","Walk the dog","Buy some milk");
+
+ todoMvcApp.completeItem("Feed the cat");
+
+ assertThat(todoMvcApp.itemRow("Feed the cat")).hasClass("completed");
+ }
+
+ @DisplayName("Completing an item should update the number of items left count")
+ @Test
+ void shouldUpdateNumberOfItemsLeftCount() {
+ todoMvcApp.addTodoItems("Feed the cat","Walk the dog","Buy some milk");
+
+ todoMvcApp.completeItem("Feed the cat");
+
+ Assertions.assertThat(todoMvcApp.todoCount()).isEqualTo("2 items left!");
+ }
+
+ @DisplayName("Should be able to clear completed items")
+ @Test
+ void shouldBeAbleToClearCompletedItems() {
+ todoMvcApp.addTodoItems("Feed the cat", "Walk the dog", "Buy some milk");
+
+ todoMvcApp.completeItem("Walk the dog");
+ todoMvcApp.clearCompleted();
+
+ Assertions.assertThat(todoMvcApp.todoItemsDisplayed()).containsExactly("Feed the cat", "Buy some milk");
+ }
+}
diff --git a/src/test/java/com/serenitydojo/playwright/todo/FilteringTodoItemsTest.java b/src/test/java/com/serenitydojo/playwright/todo/FilteringTodoItemsTest.java
new file mode 100644
index 00000000..32abd341
--- /dev/null
+++ b/src/test/java/com/serenitydojo/playwright/todo/FilteringTodoItemsTest.java
@@ -0,0 +1,70 @@
+package com.serenitydojo.playwright.todo;
+
+
+import com.microsoft.playwright.Page;
+import com.microsoft.playwright.junit.UsePlaywright;
+import io.qameta.allure.Feature;
+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 static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
+
+@DisplayName("Filtering todo items")
+@UsePlaywright(ChromeHeadlessOptions.class)
+@Feature("Filtering todo items")
+class FilteringTodoItemsTest {
+
+ TodoMvcAppPage todoMvcApp;
+
+ @BeforeEach
+ void openApp(Page page) {
+ todoMvcApp = new TodoMvcAppPage(page);
+ todoMvcApp.open();
+ }
+
+ @DisplayName("All items should be displayed by default")
+ @Test
+ void allItemsShouldBeDisplayedByDefault() {
+ todoMvcApp.addTodoItems("Feed the cat", "Walk the dog", "Buy some milk");
+
+ Assertions.assertThat(todoMvcApp.activeFilter()).isEqualTo("All");
+ }
+
+
+ @DisplayName("Should be able to filter active items")
+ @Test
+ void shouldBeAbleToFilterByActiveItems() {
+ todoMvcApp.addTodoItems("Feed the cat", "Walk the dog", "Buy some milk");
+
+ todoMvcApp.completeItem("Walk the dog");
+ todoMvcApp.filterItemsBy("Active");
+
+ Assertions.assertThat(todoMvcApp.todoItemsDisplayed()).containsExactly("Feed the cat", "Buy some milk");
+ }
+
+ @DisplayName("Should be able to filter completed items")
+ @Test
+ void shouldBeAbleToFilterByCompletedItems() {
+ todoMvcApp.addTodoItems("Feed the cat", "Walk the dog", "Buy some milk");
+
+ todoMvcApp.completeItem("Walk the dog");
+ todoMvcApp.filterItemsBy("Completed");
+
+ Assertions.assertThat(todoMvcApp.todoItemsDisplayed()).containsExactly("Walk the dog");
+ }
+
+ @DisplayName("Should be able to revert to showing all items")
+ @Test
+ void shouldBeAbleToRevertToShowingAllItems() {
+ todoMvcApp.addTodoItems("Feed the cat", "Walk the dog", "Buy some milk");
+
+ todoMvcApp.completeItem("Walk the dog");
+ todoMvcApp.filterItemsBy("Completed");
+ todoMvcApp.filterItemsBy("All");
+
+ Assertions.assertThat(todoMvcApp.todoItemsDisplayed()).containsExactly("Feed the cat", "Walk the dog", "Buy some milk");
+ }
+
+}
diff --git a/src/test/java/com/serenitydojo/playwright/todo/TodoMvcAppPage.java b/src/test/java/com/serenitydojo/playwright/todo/TodoMvcAppPage.java
new file mode 100644
index 00000000..37bba4c1
--- /dev/null
+++ b/src/test/java/com/serenitydojo/playwright/todo/TodoMvcAppPage.java
@@ -0,0 +1,82 @@
+package com.serenitydojo.playwright.todo;
+
+import com.microsoft.playwright.Locator;
+import com.microsoft.playwright.Page;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.List;
+
+public class TodoMvcAppPage {
+
+ private final Page page;
+
+ private final Locator todoItems;
+ private final Locator todoField;
+
+ private final String baseUrl;
+
+ public TodoMvcAppPage(Page page) {
+ this.page = page;
+ baseUrl = (StringUtils.isEmpty(System.getenv("APP_HOST_URL"))) ? "http://localhost:7002" : System.getenv("APP_HOST_URL");
+ this.todoItems = page.getByTestId("todo-item");
+ this.todoField = page.getByTestId("text-input");
+ }
+
+ public void open() {
+ page.navigate(baseUrl);
+ }
+
+ public List todoItemsDisplayed() {
+ return todoItems.allTextContents();
+ }
+
+ public Locator todoField() {
+ return todoField;
+ }
+
+ public void addTodoItem(String todoItem) {
+ todoField.fill(todoItem);
+ todoField.press("Enter");
+ }
+
+ public void addTodoItems(String... items) {
+ for (String item : items) {
+ addTodoItem(item);
+ }
+ }
+
+ public void deleteItem(String itemName) {
+ Locator itemRow = itemRow(itemName);
+ Locator deleteButton = itemRow.getByTestId("todo-item-button");
+
+ itemRow.hover();
+ deleteButton.click();
+ }
+
+ public Locator itemRow(String itemName) {
+ return page.getByTestId("todo-item").filter(new Locator.FilterOptions().setHasText(itemName));
+ }
+
+ public void completeItem(String itemName) {
+ Locator itemRow = itemRow(itemName);
+ Locator completeButton = itemRow.getByTestId("todo-item-toggle");
+ completeButton.click();
+
+ }
+
+ public String todoCount() {
+ return page.locator(".todo-count").textContent();
+ }
+
+ public String activeFilter() {
+ return page.locator(".filters .selected").textContent();
+ }
+
+ public void filterItemsBy(String filter) {
+ page.locator(".filters").getByText(filter).click();
+ }
+
+ public void clearCompleted() {
+ page.getByTestId("footer").getByText("Clear completed").click();
+ }
+}
diff --git a/src/test/java/com/serenitydojo/playwright/toolshop/apiLogin/LoginPage.java b/src/test/java/com/serenitydojo/playwright/toolshop/apiLogin/LoginPage.java
new file mode 100644
index 00000000..f9fec9d2
--- /dev/null
+++ b/src/test/java/com/serenitydojo/playwright/toolshop/apiLogin/LoginPage.java
@@ -0,0 +1,37 @@
+package com.serenitydojo.playwright.toolshop.apiLogin;
+
+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();
+ }
+}
diff --git a/src/test/java/com/serenitydojo/playwright/toolshop/apiLogin/LoginWithRegisteredUserTest.java b/src/test/java/com/serenitydojo/playwright/toolshop/apiLogin/LoginWithRegisteredUserTest.java
new file mode 100644
index 00000000..6bb9b3a1
--- /dev/null
+++ b/src/test/java/com/serenitydojo/playwright/toolshop/apiLogin/LoginWithRegisteredUserTest.java
@@ -0,0 +1,43 @@
+package com.serenitydojo.playwright.toolshop.apiLogin;
+
+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(){
+
+ User user = User.randomUser();
+ UserAPIClient userAPIClient = new UserAPIClient(page);
+ userAPIClient.registerUser(user);
+
+ LoginPage loginPage = new LoginPage(page);
+ loginPage.open();
+ loginPage.loginAs(user);
+
+ assertThat(loginPage.title()).isEqualTo("My account");
+
+ }
+
+ @Test
+ 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");
+ }
+
+
+}
diff --git a/src/test/java/com/serenitydojo/playwright/toolshop/apiLogin/RegisterAPITest.java b/src/test/java/com/serenitydojo/playwright/toolshop/apiLogin/RegisterAPITest.java
new file mode 100644
index 00000000..f1194cb7
--- /dev/null
+++ b/src/test/java/com/serenitydojo/playwright/toolshop/apiLogin/RegisterAPITest.java
@@ -0,0 +1,127 @@
+package com.serenitydojo.playwright.toolshop.apiLogin;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.microsoft.playwright.APIRequest;
+import com.microsoft.playwright.APIRequestContext;
+import com.microsoft.playwright.Playwright;
+import com.microsoft.playwright.junit.UsePlaywright;
+import com.microsoft.playwright.options.RequestOptions;
+import com.serenitydojo.playwright.toolshop.domain.Address;
+import com.serenitydojo.playwright.toolshop.domain.User;
+import org.assertj.core.api.SoftAssertions;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.SoftAssertions.assertSoftly;
+
+@UsePlaywright
+public class RegisterAPITest {
+
+ 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")
+ );
+ }
+
+ @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();
+
+ System.out.println("Request user:");
+ System.out.println(new Gson().toJson(validUser));
+
+ System.out.println("Response status: " + response.status());
+ System.out.println("Response body:");
+ System.out.println(responseBody);
+
+
+ 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)
+ .isEqualTo(validUser.withPassword(null));
+
+ //response object doesn't have password field
+ assertThat(responseObject.has("password"))
+ .isFalse();
+
+ softly.assertThat(responseObject.get("id").getAsString())
+ .as("Register user Id")
+ .isNotEmpty();
+
+
+ }
+ );
+
+
+ }
+
+ @Test
+ void firstNameIsMandatory(){
+
+ Address address = new Address("street", "44","mly","ttt","nl","77kk");
+
+ User userWithNoName = new User(
+ null,
+ "Smith",
+ address,
+ "+316878996789",
+ "1990-01-01",
+ "78678kjhkAs#$",
+ "tyfteyt@gmail.com"
+ );
+
+ 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.");
+
+ });
+
+
+
+ }
+
+
+
+}
diff --git a/src/test/java/com/serenitydojo/playwright/toolshop/apiLogin/UserAPIClient.java b/src/test/java/com/serenitydojo/playwright/toolshop/apiLogin/UserAPIClient.java
new file mode 100644
index 00000000..8f201352
--- /dev/null
+++ b/src/test/java/com/serenitydojo/playwright/toolshop/apiLogin/UserAPIClient.java
@@ -0,0 +1,32 @@
+package com.serenitydojo.playwright.toolshop.apiLogin;
+
+import com.microsoft.playwright.Page;
+import com.microsoft.playwright.options.RequestOptions;
+import com.serenitydojo.playwright.toolshop.domain.User;
+
+import java.util.Arrays;
+
+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")
+
+ );
+ System.out.println(response.text());
+ if(response.status() != 201){
+ throw new IllegalStateException("Unexpected result: " + response.status());
+ }
+ }
+}
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 00000000..843f1885
--- /dev/null
+++ b/src/test/java/com/serenitydojo/playwright/toolshop/catalog/AddToCartTest.java
@@ -0,0 +1,105 @@
+package com.serenitydojo.playwright.toolshop.catalog;
+
+import com.serenitydojo.playwright.toolshop.catalog.pageobjects.*;
+import com.serenitydojo.playwright.toolshop.fixtures.PlaywrightTestCase;
+
+import net.serenitybdd.annotations.Feature;
+import net.serenitybdd.annotations.Steps;
+import net.serenitybdd.annotations.Story;
+import net.serenitybdd.junit5.SerenityJUnit5Extension;
+import net.serenitybdd.playwright.PlaywrightSerenity;
+import net.serenitybdd.playwright.junit5.SerenityPlaywrightExtension;
+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.extension.ExtendWith;
+
+import java.util.List;
+
+@DisplayName("Shopping Cart")
+@Feature("Shopping Cart")
+@ExtendWith(SerenityJUnit5Extension.class)
+@ExtendWith(SerenityPlaywrightExtension.class)
+public class AddToCartTest extends PlaywrightTestCase {
+ @Steps SearchComponent searchComponent;
+ @Steps ProductList productList;
+ @Steps ProductDetails productDetails;
+ @Steps NavBar navBar;
+ @Steps CheckoutCart checkoutCart;
+
+ @BeforeEach
+ void openHomePage() {
+ page.navigate("https://practicesoftwaretesting.com");
+ }
+
+ @BeforeEach
+ void setUp() {
+
+ // we use @Steps from serenity so we don't have to use initiate them here, serenity does automatically
+// searchComponent = new SearchComponent(page);
+// productList = new ProductList(page);
+// productDetails = new ProductDetails(page);
+// navBar = new NavBar(page);
+// checkoutCart = new CheckoutCart(page);
+
+ PlaywrightSerenity.registerPage(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 00000000..5f76101d
--- /dev/null
+++ b/src/test/java/com/serenitydojo/playwright/toolshop/catalog/SearchForProductsTest.java
@@ -0,0 +1,80 @@
+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 com.serenitydojo.playwright.toolshop.fixtures.ScreenshotManager;
+import net.serenitybdd.annotations.Feature;
+import net.serenitybdd.annotations.Story;
+import net.serenitybdd.junit5.SerenityJUnit5Extension;
+import net.serenitybdd.playwright.junit5.SerenityPlaywrightExtension;
+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;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+
+@ExtendWith(SerenityJUnit5Extension.class)
+@ExtendWith(SerenityPlaywrightExtension.class)
+@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);
+
+// ScreenshotManager.takeScreenshot(page, "ss1");
+ searchComponent.searchBy("tape");
+// ScreenshotManager.takeScreenshot(page, "ss2");
+
+ 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/WithTracing.java b/src/test/java/com/serenitydojo/playwright/toolshop/catalog/WithTracing.java
new file mode 100644
index 00000000..73c5ae00
--- /dev/null
+++ b/src/test/java/com/serenitydojo/playwright/toolshop/catalog/WithTracing.java
@@ -0,0 +1,33 @@
+package com.serenitydojo.playwright.toolshop.fixtures;
+
+import com.microsoft.playwright.BrowserContext;
+import com.microsoft.playwright.Tracing;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestInfo;
+
+import java.nio.file.Paths;
+
+public interface WithTracing {
+
+
+ @BeforeEach
+ default void setupTrace(BrowserContext context) {
+ context.tracing().start(
+ new Tracing.StartOptions()
+ .setScreenshots(true)
+ .setSnapshots(true)
+ .setSources(true)
+ );
+ }
+
+ @AfterEach
+ default void recordTrace(TestInfo testInfo, BrowserContext context) {
+ String traceName = testInfo.getDisplayName().replace(" ","-").toLowerCase();
+ context.tracing().stop(
+ new Tracing.StopOptions()
+ .setPath(Paths.get("target/traces/trace-" + traceName + ".zip"))
+ );
+ }
+
+}
\ 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 00000000..c8810308
--- /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) {}
\ No newline at end of file
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 00000000..fab295d3
--- /dev/null
+++ b/src/test/java/com/serenitydojo/playwright/toolshop/catalog/pageobjects/CheckoutCart.java
@@ -0,0 +1,38 @@
+package com.serenitydojo.playwright.toolshop.catalog.pageobjects;
+
+import com.microsoft.playwright.Page;
+
+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 00000000..cbd54d66
--- /dev/null
+++ b/src/test/java/com/serenitydojo/playwright/toolshop/catalog/pageobjects/NavBar.java
@@ -0,0 +1,28 @@
+package com.serenitydojo.playwright.toolshop.catalog.pageobjects;
+
+import com.microsoft.playwright.Page;
+import net.serenitybdd.annotations.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");
+ }
+}
\ No newline at end of file
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 00000000..391e2d29
--- /dev/null
+++ b/src/test/java/com/serenitydojo/playwright/toolshop/catalog/pageobjects/ProductDetails.java
@@ -0,0 +1,33 @@
+package com.serenitydojo.playwright.toolshop.catalog.pageobjects;
+
+import com.microsoft.playwright.Page;
+import com.microsoft.playwright.options.AriaRole;
+import net.serenitybdd.annotations.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();
+ }
+ );
+
+ }
+}
\ No newline at end of file
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 00000000..d3bb9bec
--- /dev/null
+++ b/src/test/java/com/serenitydojo/playwright/toolshop/catalog/pageobjects/ProductList.java
@@ -0,0 +1,42 @@
+package com.serenitydojo.playwright.toolshop.catalog.pageobjects;
+
+import com.microsoft.playwright.Page;
+import com.serenitydojo.playwright.toolshop.fixtures.ProductSummary;
+import com.serenitydojo.playwright.toolshop.fixtures.ScreenshotManager;
+import net.serenitybdd.annotations.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();
+// ScreenshotManager.takeScreenshot(page,"view product details");
+ }
+
+ public String getSearchCompletedMessage() {
+ return page.getByTestId("search_completed").textContent();
+ }
+}
\ No newline at end of file
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 00000000..fab98bee
--- /dev/null
+++ b/src/test/java/com/serenitydojo/playwright/toolshop/catalog/pageobjects/SearchComponent.java
@@ -0,0 +1,60 @@
+package com.serenitydojo.playwright.toolshop.catalog.pageobjects;
+
+import com.microsoft.playwright.Page;
+import com.microsoft.playwright.options.AriaRole;
+import net.serenitybdd.annotations.Step;
+
+public class SearchComponent {
+ private final Page page;
+
+ public SearchComponent(Page page) {
+ this.page = page;
+ }
+
+ @Step("Search for {0}")
+ public void searchBy(String keyword) {
+ page.waitForResponse(response ->
+ response.url().endsWith("/products/search")
+ && "QUERY".equalsIgnoreCase(
+ response.request().method()
+ )
+ && response.status() == 200,
+ () -> {
+ page.getByPlaceholder("Search").fill(keyword);
+ page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Search")).click();
+ });
+ }
+
+ @Step("Clear search")
+ public void clearSearch() {
+ page.waitForResponse("**/products**", () -> {
+ page.getByTestId("search-reset").click();
+ });
+ }
+
+ @Step("Filter by {0}")
+ public void filterBy(String filterName) {
+
+ page.waitForResponse(
+ response ->
+ response.url().endsWith("/products")
+ && "QUERY".equalsIgnoreCase(
+ response.request().method()
+ )
+ && response.status() == 200,
+ () -> page.getByLabel(filterName).check()
+ );
+ }
+
+ @Step("Sort by {0}")
+ public void sortBy(String sortFilter) {
+ page.waitForResponse(response ->
+ response.url().endsWith("/products")
+ && "QUERY".equalsIgnoreCase(
+ response.request().method()
+ )
+ && response.status() == 200, () -> {
+ 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 00000000..cd7f323d
--- /dev/null
+++ b/src/test/java/com/serenitydojo/playwright/toolshop/contact/ContactForm.java
@@ -0,0 +1,65 @@
+package com.serenitydojo.playwright.toolshop.contact;
+
+import com.microsoft.playwright.Locator;
+import com.microsoft.playwright.Page;
+import com.microsoft.playwright.options.AriaRole;
+import net.serenitybdd.annotations.Step;
+
+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);
+ }
+
+ @Step("Submitting the contact form")
+ public void submitForm() {
+ sendButton.click();
+ }
+
+ public String getAlertMessage() {
+ return page.getByRole(AriaRole.ALERT).textContent();
+ }
+
+ public void clearField(String fieldName) {
+ page.getByLabel(fieldName).clear();
+ }
+}
\ No newline at end of file
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 00000000..3d6f0d24
--- /dev/null
+++ b/src/test/java/com/serenitydojo/playwright/toolshop/contact/ContactFormTest.java
@@ -0,0 +1,118 @@
+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 net.serenitybdd.junit5.SerenityJUnit5Extension;
+import net.serenitybdd.playwright.junit5.SerenityPlaywrightExtension;
+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.extension.ExtendWith;
+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;
+
+@ExtendWith(SerenityJUnit5Extension.class)
+@ExtendWith(SerenityPlaywrightExtension.class)
+@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");
+ }
+}
\ No newline at end of file
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 00000000..753c15d9
--- /dev/null
+++ b/src/test/java/com/serenitydojo/playwright/toolshop/cucumber/CucumberTests.java
@@ -0,0 +1,21 @@
+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.SelectPackages;
+import org.junit.platform.suite.api.Suite;
+
+@Suite
+@IncludeEngines("cucumber")
+@SelectPackages("features")
+@ConfigurationParameter(
+ key = "cucumber.plugin",
+ value =
+// "io.qameta.allure.cucumber7jvm.AllureCucumber7Jvm," +
+ "net.serenitybdd.cucumber.core.plugin.SerenityReporterParallel," +
+ "pretty," +
+ "html:target/cucumber-reports/cucumber.html"
+)
+@ConfigurationParameter(key = "cucumber.glue", value = "com.serenitydojo.playwright.toolshop")
+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 00000000..b5bf6c65
--- /dev/null
+++ b/src/test/java/com/serenitydojo/playwright/toolshop/cucumber/stepdefinitions/PlaywrightCucumberFixtures.java
@@ -0,0 +1,70 @@
+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 io.qameta.allure.Allure;
+
+
+import java.io.ByteArrayInputStream;
+import java.util.Arrays;
+
+public class PlaywrightCucumberFixtures {
+
+ //this is for parallel execution
+ 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(false)
+ .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() {
+ takeScreenshot("End of test");
+ browserContext.get().close();
+ }
+
+ protected void takeScreenshot(String name){
+ var screenshot = page.get().screenshot(
+ new Page.ScreenshotOptions().setFullPage(true)
+ );
+ Allure.addAttachment(name, new ByteArrayInputStream(screenshot));
+ }
+
+ @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 00000000..1531ae37
--- /dev/null
+++ b/src/test/java/com/serenitydojo/playwright/toolshop/cucumber/stepdefinitions/ProductCatalogStepDefinitions.java
@@ -0,0 +1,93 @@
+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.*;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.as;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ProductCatalogStepDefinitions {
+
+ NavBar navBar;
+ SearchComponent searchComponent;
+ ProductList productList;
+
+ @Before
+ public void setupPageObject(){
+ 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();
+ }
+ @Then("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 matchingProduct = productList.getProductNames();
+ assertThat(matchingProduct).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) {
+ var matchingProducts = productList.getProductSummaries();
+// List