1"use strict";(globalThis.webpackChunkserenity_users_guide=globalThis.webpackChunkserenity_users_guide||[]).push([[9303],{352(e,t,n){n.r(t),n.d(t,{assets:()=>d,contentTitle:()=>r,default:()=>h,frontMatter:()=>a,metadata:()=>i,toc:()=>l});const i=JSON.parse('{"id":"playwright/playwright_tutorial_todomvc","title":"Tutorial: Testing TodoMVC","description":"This tutorial walks through building a Serenity Playwright test suite for the TodoMVC application. Rather than showing you the finished code upfront, we\'ll build it iteratively\u2014starting with a single test and adding only what we need as we go.","source":"@site/docs/playwright/tutorial-todomvc.md","sourceDirName":"playwright","slug":"/playwright/playwright_tutorial_todomvc","permalink":"/docs/playwright/playwright_tutorial_todomvc","draft":false,"unlisted":false,"editUrl":"https://github.com/serenity-bdd/serenity-bdd.github.io/blob/master/docs/playwright/tutorial-todomvc.md","tags":[],"version":"current","sidebarPosition":7,"frontMatter":{"id":"playwright_tutorial_todomvc","title":"Tutorial: Testing TodoMVC","sidebar_position":7},"sidebar":"tutorialSidebar","previous":{"title":"Best Practices","permalink":"/docs/playwright/playwright_best_practices"},"next":{"title":"Tutorial: Screenplay with TodoMVC","permalink":"/docs/playwright/playwright_screenplay_tutorial"}}');var o=n(4848),s=n(8453);const a={id:"playwright_tutorial_todomvc",title:"Tutorial: Testing TodoMVC",sidebar_position:7},r="Tutorial: Building a Test Suite Step by Step",d={},l=[{value:"What We're Testing",id:"what-were-testing",level:2},{value:"Project Setup",id:"project-setup",level:2},{value:"Iteration 1: Our First Test",id:"iteration-1-our-first-test",level:2},{value:"Write the Test First",id:"write-the-test-first",level:3},{value:"Create the Step Library",id:"create-the-step-library",level:3},{value:"Create the Page Object",id:"create-the-page-object",level:3},{value:"Run It",id:"run-it",level:3},{value:"Iteration 2: Adding More Assertions",id:"iteration-2-adding-more-assertions",level:2},{value:"Update the Test",id:"update-the-test",level:3},{value:"Add to Step Library",id:"add-to-step-library",level:3},{value:"Add to Page Object",id:"add-to-page-object",level:3},{value:"Iteration 3: Testing Multiple Todos",id:"iteration-3-testing-multiple-todos",level:2},{value:"Add a New Test",id:"add-a-new-test",level:3},{value:"Add to Step Library",id:"add-to-step-library-1",level:3},{value:"Add to Page Object",id:"add-to-page-object-1",level:3},{value:"Iteration 4: Extract a Base Test Class",id:"iteration-4-extract-a-base-test-class",level:2},{value:"Create the Base Class",id:"create-the-base-class",level:3},{value:"Simplify the Test Class",id:"simplify-the-test-class",level:3},{value:"Iteration 5: Testing Completion",id:"iteration-5-testing-completion",level:2},{value:"Create the Test Class",id:"create-the-test-class",level:3},{value:"Add to Step Library",id:"add-to-step-library-2",level:3},{value:"Add to Page Object",id:"add-to-page-object-2",level:3},{value:"Iteration 6: Testing the Remaining Count",id:"iteration-6-testing-the-remaining-count",level:2},{value:"Add Another Test",id:"add-another-test",level:3},{value:"Add to Step Library",id:"add-to-step-library-3",level:3},{value:"Add to Page Object",id:"add-to-page-object-3",level:3},{value:"The Pattern",id:"the-pattern",level:2},{value:"Continue Building",id:"continue-building",level:2},{value:"Final Project Structure",id:"final-project-structure",level:2},{value:"Key Takeaways",id:"key-takeaways",level:2},{value:"Complete Source Code",id:"complete-source-code",level:2}];function c(e){const t={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,s.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(t.header,{children:(0,o.jsx)(t.h1,{id:"tutorial-building-a-test-suite-step-by-step",children:"Tutorial: Building a Test Suite Step by Step"})}),"\n",(0,o.jsxs)(t.p,{children:["This tutorial walks through building a Serenity Playwright test suite for the ",(0,o.jsx)(t.a,{href:"https://todomvc.com/examples/react/dist/",children:"TodoMVC application"}),". Rather than showing you the finished code upfront, we'll build it iteratively\u2014starting with a single test and adding only what we need as we go."]}),"\n",(0,o.jsx)(t.p,{children:"This reflects how you'd actually develop tests in practice: write a test, build the minimum infrastru
1cture to support it, then expand."}),"\n",(0,o.jsx)(t.h2,{id:"what-were-testing",children:"What We're Testing"}),"\n",(0,o.jsx)(t.p,{children:"TodoMVC is a simple todo list application where users can add, complete, edit, and delete items. We'll start with the most basic functionality and build up from there."}),"\n",(0,o.jsx)(t.h2,{id:"project-setup",children:"Project Setup"}),"\n",(0,o.jsxs)(t.p,{children:["First, create a Maven project with these dependencies in ",(0,o.jsx)(t.code,{children:"pom.xml"}),":"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-xml",children:'<?xml version="1.0" encoding="UTF-8"?>\n<project xmlns="http://maven.apache.org/POM/4.0.0"\n xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n xsi:schemaLocation="http://maven.apache.org/POM/4.0.0\n http://maven.apache.org/xsd/maven-4.0.0.xsd">\n <modelVersion>4.0.0</modelVersion>\n\n <groupId>com.example</groupId>\n <artifactId>serenity-playwright-todomvc</artifactId>\n <version>1.0.0-SNAPSHOT</version>\n\n <properties>\n <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n <maven.compiler.source>17</maven.compiler.source>\n <maven.compiler.target>17</maven.compiler.target>\n <serenity.version>5.3.7</serenity.version>\n <playwright.version>1.58.0</playwright.version>\n </properties>\n\n <dependencies>\n <dependency>\n <groupId>net.serenity-bdd</groupId>\n <artifactId>serenity-playwright</artifactId>\n <version>${serenity.version}</version>\n <scope>test</scope>\n </dependency>\n <dependency>\n <groupId>net.serenity-bdd</groupId>\n <artifactId>serenity-junit5</artifactId>\n <version>${serenity.version}</version>\n <scope>test</scope>\n </dependency>\n <dependency>\n <groupId>com.microsoft.playwright</groupId>\n <artifactId>playwright</artifactId>\n <version>${playwright.version}</version>\n <scope>test</scope>\n </dependency>\n <dependency>\n <groupId>org.junit.jupiter</groupId>\n <artifactId>junit-jupiter</artifactId>\n <version>6.0.1</version>\n <scope>test</scope>\n </dependency>\n <dependency>\n <groupId>org.assertj</groupId>\n <artifactId>assertj-core</artifactId>\n <version>3.27.0</version>\n <scope>test</scope>\n </dependency>\n </dependencies>\n\n <build>\n <plugins>\n <plugin>\n <groupId>org.apache.maven.plugins</groupId>\n <artifactId>maven-failsafe-plugin</artifactId>\n <version>3.5.0</version>\n <configuration>\n <includes>\n <include>**/*Test.java</include>\n </includes>\n </configuration>\n <executions>\n <execution>\n <goals>\n <goal>integration-test</goal>\n <goal>verify</goal>\n </goals>\n </execution>\n </executions>\n </plugin>\n <plugin>\n <groupId>net.serenity-bdd.maven.plugins</groupId>\n <artifactId>serenity-maven-plugin</artifactId>\n <version>${serenity.version}</version>\n <executions>\n <execution>\n <id>serenity-reports</id>\n <phase>post-integration-test</phase>\n <goals>\n <goal>aggregate</goal>\n </goals>\n </execution>\n </executions>\n </plugin>\n </plugins>\n </build>\n</project>\n'})}),"\n",(0,o.jsx)(t.h2,{id:"iteration-1-our-first-test",children:"Iteration 1: Our First Test"}),"\n",(0,o.jsx)(t.p,{children:"Let's start with the simplest possible test: adding a single todo item."}),"\n",(0,o.jsx)(t.h3,{id:"write-the-test-first",children:"Write the Test First"}),"\n",(0,o.jsxs)(t.p,{children:["Create ",(0,o.jsx)(t.code,{children:"src/test/java/todomvc/tests/WhenAddingTodosTest.java"}),":"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-java",children:'package todomvc.tests;\n\nimport com.microsoft.playwright.Page;\nimport net.serenitybdd.annotations.Steps;\nimport net.serenitybdd.junit5.SerenityJUnit5Extension;\nimport net.serenitybdd.playwright.junit5.SerenityPlaywrightExtension;\nimport com.microsoft.playwright.junit.UsePlaywright;\nimport org.junit.jupiter.api.*;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport todomvc.steps.TodoSteps;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\n@ExtendWith(SerenityJUnit5Extension.class)\n@ExtendWith(SerenityPlaywrightExtension.class)\n@UsePlaywright\n@DisplayName("When adding todos")\nclass WhenAddingTodosTest {\n\n @Steps\n TodoSteps todo;\n\n @BeforeEach\n void setUp(Page page) {\n todo.setPage(page);\n }\n\n @Test\n @DisplayName("should add a single todo item")\n void shouldAddSingleTodoItem() {\n todo.openApplication();\n todo.addTodo("Buy milk");\n\n assertThat(todo.visibleTodoCount()).isEqualTo(1);\n }\n}\n'})}),"\n",(0,o.jsx)(t.p,{children:"Three annotations work together here:"}),"\n",(0,o.jsxs)(t.table,{children:[(0,o.jsx)(t.thead,{children:(0,o.jsxs)(t.tr,{children:[(0,o.jsx)(t.th,{children:"Annotation"}),(0,o.jsx)(t.th,{children:"Purpose"})]})}),(0,o.jsxs)(t.tbody,{children:[(0,o.jsxs)(t.tr,{children:[(0,o.jsx)(t.td,{children:(0,o.jsx)(t.code,{children:"@ExtendWith(SerenityJUnit5Extension.class)"})}),(0,o.jsx)(t.td,{children:"Integrates Serenity BDD reporting and step injection"})]}),(0,o.jsxs)(t.tr,{children:[(0,o.jsx)(t.td,{children:(0,o.jsx)(t.code,{children:"@ExtendWith(SerenityPlaywrightExtension.class)"})}),(0,o.jsx)(t.td,{children:"Registers Playwright pages with Serenity for automatic screenshots"})]}),(0,o.jsxs)(t.tr,{children:[(0,o.jsx)(t.td,{children:(0,o.jsx)(t.code,{children:"@UsePlaywright"})}),(0,o.jsx)(t.td,{children:"Manages the full Playwright lifecycle (Playwright, Browser, BrowserContext, Page)"})]})]})]}),"\n",(0,o.jsxs)(t.p,{children:["With ",(0,o.jsx)(t.code,{children:"@UsePlaywright"}),", you don't need to create or close ",(0,o.jsx)(t.code,{children:"Playwright"}),", ",(0,o.jsx)(t.code,{children:"Browser"}),", or ",(0,o.jsx)(t.code,{children:"Page"})," yourself. JUnit 5 injects a fresh ",(0,o.jsx)(t.code,{children:"Page"})," into your ",(0,o.jsx)(t.code,{children:"@BeforeEach"})," and ",(0,o.jsx)(t.code,{children:"@Test"})," methods automatically, and cleans everything up when the test finishes."]}
1),"\n",(0,o.jsx)(t.p,{children:"This test won't compile yet\u2014we need to create the step library and page object. But notice we've defined exactly what we need:"}),"\n",(0,o.jsxs)(t.ul,{children:["\n",(0,o.jsxs)(t.li,{children:[(0,o.jsx)(t.code,{children:"openApplication()"})," - navigate to the app"]}),"\n",(0,o.jsxs)(t.li,{children:[(0,o.jsx)(t.code,{children:"addTodo(String)"})," - add an item"]}),"\n",(0,o.jsxs)(t.li,{children:[(0,o.jsx)(t.code,{children:"visibleTodoCount()"})," - return how many items are visible"]}),"\n"]}),"\n",(0,o.jsx)(t.h3,{id:"create-the-step-library",children:"Create the Step Library"}),"\n",(0,o.jsxs)(t.p,{children:["Create ",(0,o.jsx)(t.code,{children:"src/test/java/todomvc/steps/TodoSteps.java"})," with only the methods we need:"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-java",children:'package todomvc.steps;\n\nimport com.microsoft.playwright.Page;\nimport net.serenitybdd.annotations.Step;\nimport todomvc.pages.TodoMvcPage;\n\npublic class TodoSteps {\n\n private TodoMvcPage todoMvcPage;\n\n public void setPage(Page page) {\n this.todoMvcPage = new TodoMvcPage(page);\n }\n\n @Step("Open the TodoMVC application")\n public void openApplication() {\n todoMvcPage.open();\n }\n\n @Step("Add a todo: \'{0}\'")\n public void addTodo(String todoText) {\n todoMvcPage.addTodo(todoText);\n }\n\n @Step("Get the number of visible todos")\n public int visibleTodoCount() {\n return todoMvcPage.getVisibleTodoCount();\n }\n}\n'})}),"\n",(0,o.jsx)(t.admonition,{title:"Steps Return Data, Tests Assert",type:"tip",children:(0,o.jsxs)(t.p,{children:["Notice that ",(0,o.jsx)(t.code,{children:"visibleTodoCount()"})," returns an ",(0,o.jsx)(t.code,{children:"int"}),". The test makes the assertion, not the step library. This keeps steps reusable\u2014the same step can be used in different tests with different expected values."]})}),"\n",(0,o.jsx)(t.h3,{id:"create-the-page-object",children:"Create the Page Object"}),"\n",(0,o.jsxs)(t.p,{children:["Create ",(0,o.jsx)(t.code,{children:"src/test/java/todomvc/pages/TodoMvcPage.java"})," with only what we need:"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-java",children:'package todomvc.pages;\n\nimport com.microsoft.playwright.Locator;\nimport com.microsoft.playwright.Page;\n\npublic class TodoMvcPage {\n\n private final Page page;\n private static final String URL = "https://todomvc.com/examples/react/dist/";\n\n public TodoMvcPage(Page page) {\n this.page = page;\n }\n\n // Locators - keep private\n private Locator newTodoInput() {\n return page.getByPlaceholder("What needs to be done?");\n }\n\n private Locator todoItems() {\n return page.locator(".todo-list li");\n }\n\n // Actions\n public void open() {\n page.navigate(URL);\n page.waitForLoadState();\n }\n\n public void addTodo(String todoText) {\n newTodoInput().fill(todoText);\n newTodoInput().press("Enter");\n }\n\n // Queries\n public int getVisibleTodoCount() {\n return todoItems().count();\n }\n}\n'})}),"\n",(0,o.jsx)(t.h3,{id:"run-it",children:"Run It"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-bash",children:"mvn clean verify\n"})}),"\n",(0,o.jsx)(t.p,{children:"The test should pass. We now have a minimal working test suite."}),"\n",(0,o.jsx)(t.h2,{id:"iteration-2-adding-more-assertions",children:"Iteration 2: Adding More Assertions"}),"\n",(0,o.jsx)(t.p,{children:"Let's enhance our test to verify the todo text appears correctly."}),"\n",(0,o.jsx)(t.h3,{id:"update-the-test",children:"Update the Test"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-java",children:'@Test\n@DisplayName("should add a single todo item")\nvoid shouldAddSingleTodoItem() {\n todo.openApplication();\n todo.addTodo("Buy milk");\n\n assertThat(todo.visibleTodoCount()).isEqualTo(1);\n assertThat(todo.todoExists("Buy milk")).isTrue(); // New assertion\n}\n'})}),"\n",(0,o.jsx)(t.h3,{id:"add-to-step-library",children:"Add to Step
1Library"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-java",children:"@Step(\"Check if todo '{0}' exists\")\npublic boolean todoExists(String todoText) {\n return todoMvcPage.hasTodo(todoText);\n}\n"})}),"\n",(0,o.jsx)(t.h3,{id:"add-to-page-object",children:"Add to Page Object"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-java",children:'private Locator todoItemByText(String text) {\n return page.locator(".todo-list li").filter(\n new Locator.FilterOptions().setHasText(text)\n );\n}\n\npublic boolean hasTodo(String todoText) {\n return todoItemByText(todoText).count() > 0;\n}\n'})}),"\n",(0,o.jsxs)(t.p,{children:["We added a new locator method ",(0,o.jsx)(t.code,{children:"todoItemByText()"})," because we needed to find a specific item. This locator will be reused for other operations later."]}),"\n",(0,o.jsx)(t.h2,{id:"iteration-3-testing-multiple-todos",children:"Iteration 3: Testing Multiple Todos"}),"\n",(0,o.jsx)(t.h3,{id:"add-a-new-test",children:"Add a New Test"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-java",children:'@Test\n@DisplayName("should add multiple todo items")\nvoid shouldAddMultipleTodoItems() {\n todo.openApplication();\n todo.addTodos("Buy milk", "Walk the dog", "Do laundry");\n\n assertThat(todo.visibleTodoCount()).isEqualTo(3);\n assertThat(todo.visibleTodos())\n .containsExactly("Buy milk", "Walk the dog", "Do laundry");\n}\n'})}),"\n",(0,o.jsx)(t.h3,{id:"add-to-step-library-1",children:"Add to Step Library"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-java",children:'@Step("Add todos: {0}")\npublic void addTodos(String... todoTexts) {\n todoMvcPage.addTodos(todoTexts);\n}\n\n@Step("Get the visible todo items")\npublic List<String> visibleTodos() {\n return todoMvcPage.getVisibleTodoTexts();\n}\n'})}),"\n",(0,o.jsx)(t.h3,{id:"add-to-page-object-1",children:"Add to Page Object"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-java",children:'public void addTodos(String... todoTexts) {\n for (String text : todoTexts) {\n addTodo(text);\n }\n}\n\npublic List<String> getVisibleTodoTexts() {\n return todoItems().locator("label").allTextContents();\n}\n'})}),"\n",(0,o.jsx)(t.h2,{id:"iteration-4-extract-a-base-test-class",children:"Iteration 4: Extract a Base Test Class"}),"\n",(0,o.jsxs)(t.p,{children:["We're about to create more test classes, and they'll all need the same annotations. Let's extract them into a base class. While we're at it, we'll configure the browser to run in headless mode using an ",(0,o.jsx)(t.code,{children:"OptionsFactory"}),"."]}),"\n",(0,o.jsx)(t.h3,{id:"create-the-base-class",children:"Create the Base Class"}),"\n",(0,o.jsxs)(t.p,{children:["Create ",(0,o.jsx)(t.code,{children:"src/test/java/todomvc/tests/SerenityPlaywrightTest.java"}),":"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-java",children:'package todomvc.tests;\n\nimport com.microsoft.playwright.BrowserType;\nimport com.microsoft.playwright.junit.Options;\nimport com.microsoft.playwright.junit.OptionsFactory;\nimport com.microsoft.playwright.junit.UsePlaywright;\nimport net.serenitybdd.junit5.SerenityJUnit5Extension;\nimport net.serenitybdd.playwright.junit5.SerenityPlaywrightExtension;\nimport org.junit.jupiter.api.extension.ExtendWith;\n\nimport java.util.Arrays;\n\n@ExtendWith(SerenityJUnit5Extension.class)\n@ExtendWith(SerenityPlaywrightExtension.class)\n@UsePlaywright(SerenityPlaywrightTest.ChromeHeadlessOptions.class)\npublic abstract class SerenityPlaywrightTest {\n\n public static class ChromeHeadlessOptions implements OptionsFactory {\n @Override\n public Options getOptions() {\n return new Options()\n .setHeadless(true)\n .setLaunchOptions(\n new BrowserType.LaunchOptions()\n .setArgs(Arrays.asList(\n "--no-sandbox",\n "--disable-extensions",\n "--disable-gpu"))\n );\n }\n }\n}\n'})}),"\n",(0,o.jsxs)(t.p,{children:["The ",(0,o.jsx)(t.code,{children:"@UsePlaywright"})," annotation accepts an ",(0,o.jsx)(t.code,{children:"OptionsFactory"})," class to configure browser options. By passing ",(0,o.jsx)(t.code,{children:"ChromeHeadlessOptions.class"}),", every test that extends this base class will run in headless Chromium with the specified launch arguments. Since ",(0,o.jsx)(t.code,{children:"@UsePlaywright"})," is ",(0,o.jsx)(t.code,{children:"@Inherited"}),", all three annotations are inherited by subclasses."]}),"\n",(0,o.jsx)(t.h3,{id:"simplify-the-test-class",children:"Simplify the Test Class"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-java",children:'@DisplayName("When adding todos")\nclass WhenAddingTodosTest extends SerenityPlaywrightTest {\n\n @Steps\n TodoSteps todo;\n\n @BeforeEach\n void setUp(Page page) {\n todo.setPage(page);\n }\n\n @Test\n @DisplayName("should add a single todo item")\n void shouldAddSingleTodoItem() {\n todo.openApplication();\n todo.addTodo("Buy milk");\n\n assertThat(todo.visibleTodoCount()).isEqualTo(1);\n assertThat(todo.todoExists("Buy milk")).isTrue();\n }\n\n // ... other tests\n}\n'})}),"\n",(0,o.jsx)(t.p,{children:"Much cleaner! The base class handles all annotations and browser configuration. Each test class focuses purely on test logic\u2014no lifecycle boilerplate, no manual cleanup."}),"\n",(0,o.jsx)(t.h2,{id:"iteration-5-testing-completion",children:"Iteration 5: Testing Completion"}),"\n",(0,o.jsx)(t.p,{children:"Now let's create a new test class for c
1ompleting todos."}),"\n",(0,o.jsx)(t.h3,{id:"create-the-test-class",children:"Create the Test Class"}),"\n",(0,o.jsxs)(t.p,{children:["Create ",(0,o.jsx)(t.code,{children:"src/test/java/todomvc/tests/WhenCompletingTodosTest.java"}),":"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-java",children:'@DisplayName("When completing todos")\nclass WhenCompletingTodosTest extends SerenityPlaywrightTest {\n\n @Steps\n TodoSteps todo;\n\n @BeforeEach\n void setUp(Page page) {\n todo.setPage(page);\n }\n\n @Test\n @DisplayName("should mark a todo as completed")\n void shouldMarkTodoAsCompleted() {\n todo.openApplication();\n todo.addTodo("Buy milk");\n todo.completeTodo("Buy milk");\n\n assertThat(todo.todoIsCompleted("Buy milk")).isTrue();\n }\n}\n'})}),"\n",(0,o.jsx)(t.h3,{id:"add-to-step-library-2",children:"Add to Step Library"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-java",children:"@Step(\"Complete the todo: '{0}'\")\npublic void completeTodo(String todoText) {\n todoMvcPage.completeTodo(todoText);\n}\n\n@Step(\"Check if todo '{0}' is completed\")\npublic boolean todoIsCompleted(String todoText) {\n return todoMvcPage.isCompleted(todoText);\n}\n"})}),"\n",(0,o.jsx)(t.h3,{id:"add-to-page-object-2",children:"Add to Page Object"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-java",children:'private Locator todoCheckbox(String text) {\n return todoItemByText(text).locator(".toggle");\n}\n\npublic void completeTodo(String todoText) {\n todoCheckbox(todoText).click();\n}\n\npublic boolean isCompleted(String todoText) {\n String classAttr = todoItemByText(todoText).getAttribute("class");\n return classAttr != null && classAttr.contains("completed");\n}\n'})}),"\n",(0,o.jsxs)(t.p,{children:["Notice how we reuse ",(0,o.jsx)(t.code,{children:"todoItemByText()"})," that we created earlier. The page object grows organically as we need more functionality."]}),"\n",(0,o.jsx)(t.h2,{id:"iteration-6-testing-the-remaining-count",children:"Iteration 6: Testing the Remaining Count"}),"\n",(0,o.jsx)(t.h3,{id:"add-another-test",children:"Add Another Test"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-java",children:'@Test\n@DisplayName("should decrease remaining count when completing")\nvoid shouldDecreaseRemainingCountWhenCompleting() {\n todo.openApplication();\n todo.addTodos("Task 1", "Task 2", "Task 3");\n\n assertThat(todo.remainingCount()).isEqualTo(3);\n\n todo.completeTodo("Task 2");\n\n assertThat(todo.remainingCount()).isEqualTo(2);\n}\n'})}),"\n",(0,o.jsx)(t.h3,{id:"add-to-step-library-3",children:"Add to Step Library"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-java",children:'@Step("Get the remaining items count")\npublic int remainingCount() {\n return todoMvcPage.getRemainingCount();\n}\n'})}),"\n",(0,o.jsx)(t.h3,{id:"add-to-page-object-3",children:"Add to Page Object"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-java",children:'private Locator todoCount() {\n return page.locator(".todo-count");\n}\n\npublic int getRemainingCount() {\n String text = todoCount().textContent();\n return Integer.parseInt(text.replaceAll("[^0-9]", ""));\n}\n'})}),"\n",(0,o.jsx)(t.h2,{id:"the-pattern",children:"The Pattern"}),"\n",(0,o.jsx)(t.p,{children:"By now, you can see the pattern:"}),"\n",(0,o.jsxs)(t.ol,{children:["\n",(0,o.jsxs)(t.li,{children:[(0,o.jsx)(t.strong,{children:"Write a test"})," that describes what you want to verify"]}),"\n",(0,o.jsxs)(t.li,{children:[(0,o.jsx)(t.strong,{children:"Add step library methods"})," that the test needs"]}),"\n",(0,o.jsxs)(t.li,{children:[(0,o.jsx)(t.strong,{children:"Add page object methods"})," that the steps need"]}),"\n",(0,o.jsxs)(t.li,{children:[(0,o.jsx)(t.strong,{children:"Add locators"})," only when a method requires them"]}),"\n"]}),"\n",(0,o.jsx)(t.p,{children:"This approach ensures:"}),"\n",(0,o.jsxs)(t.ul,{children:["\n",(0,o.jsx)(t.li,{children:"You never write code you don't need"}),"\n",(0,o.jsx)(t.li,{children:"Each addition is driven by a concrete requirement"}),"\n",(0,o.jsx)(t.li,{children:"The page object grows naturally to match your test coverage"}),"\n"]}),"\n",(0,o.jsx)(t.h2,{id:"continue-building",children:"Continue Building"}),"\n",(0,o.jsx)(t.p,{children:"Following this pattern, you can add tests for:"}),"\n",(0,o.jsxs)(t.ul,{children:["\n",(0,o.jsx)(t.li,{children:"Editing todos (double-click, type, press Enter)"}),"\n",(0,o.jsx)(t.li,{children:"Deleting todos (hover, click destroy button)"}),"\n",(0,o.jsx)(t.li,{children:"Filtering (All, Active, Completed)"}),"\n",(0,o.jsx)(t.li,{children:"Toggle all"}),"\n",(0,o.jsx)(t.li,{children:"Clear completed"}),"\n"]}),"\n",(0,o.jsx)(t.p,{children:"Each new test may require new step methods, page object methods, and locators\u2014add them as needed."}),"\n",(0,o.jsx)(t.h2,{id:"final-project-structure",children:"Final Project Structure"}),"\n",(0,o.jsx)(t.p,{children:"After several iterations, your project will look like:"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{children:"src/test/java/todomvc/\n\u251c\u2500\u2500 pages/\n\u2502 \u2514\u2500\u2500 TodoMvcPage.java # Grows with each iteration\n\u251c\u2500\u2500 steps/\n\u2502 \u2514\u2500\u2500 TodoSteps.java # Grows with each iteration\n\u2514\u2500\u2500 tests/\n \u251c\u2500\u2500 SerenityPlaywrightTest.java # Base class with @UsePlaywright\n \u251c\u2500\u2500 WhenAddingTodosTest.java\n \u251c\u2500\u2500 WhenCompletingTodosTest.java\n \u251c\u2500\u2500 WhenEditingTodosTest.java # Added later\n \u251c\u2500\u2500 WhenDeletingTodosTest.java # Added later\n \u2514\u2500\u2500 WhenFilteringTodosTest.java # Added later\n"})}),"\n",(0,o.jsx)(t.h2,{id:"key-takeaways",children:"Key Takeaways"}),"\n",(0,o.jsxs)(t.table,{children:[(0,o.jsx)(t.thead,{children:(0,o.jsxs)(t.tr,{children:[(0,o.jsx)(t.th,{children:"Principle"}),(0,o.jsx)(t.th,{children:"Why It Matters"})]})}),(0,o.jsxs)(t.tbody,{children:[(0,o.jsxs)(t.tr,{children:[(0,o.jsx)(t.td,{children:(0,o.jsx)(t.strong,{children:"Start with a test"})}),(0,o.jsx)(t.td,{children:"Tests drive what code you write"})]}),(0,o.jsxs)(t.tr,{children:[(0,o.jsx)(t.td,{children:(0,o.jsx)(t.strong,{children:"Add only what you need"})}),(0,o.jsx)(t.td,{children:"No speculative code that may never be used"})]}),(0,o.jsxs)(t.tr,{children:[(0,o.jsx)(t.td,{children:(0,o.jsx)(t.strong,{children:"Extract when patterns emerge"})}),(0,o.jsx)(t.td,{children:"Base class came after we saw duplication"})]}),(0,o.jsxs)(t.tr,{children:[(0,o.jsx)(t.td,{children:(0,o.jsxs)(t.strong,{children:["Let ",(0,o.jsx)(t.code,{children:"@UsePlaywright"})," manage the lifecycle"]})}),(0,o.jsx)(t.td,{children:"No manual Playwright/Browser/Page setup or teardown"})]}),(0,o.jsxs)(t.tr,{children:[(0,o.jsx)(t.td,{children:(0,o.jsx)(t.strong,{children:"Steps return data"})}),(0,o.jsx)(t.td,{children:"Tests make assertions, keeping steps reusable"})]}),(0,o.jsxs)(t.tr,{children:[(0,o.jsx)(t.td,{children:(0,o.jsx)(t.strong,{children:"Private locators"})}),(0,o.jsx)(t.td,{children:"Encapsulation makes changes easier"})]})]})]}),"\n",(0,o.jsx)(t.h2,{id:"complete-source-code",children:"Complete Source Code"}),"\n",(0,o.jsxs)(t.p,{children:["The complete source code for this tutorial is available at:\n",(0,o.jsx)(t.a,{href:"https://github.com/serenity-bdd/serenity-playwright-todomvc-demo",children:"github.com/serenity-bdd/serenity-playwright-todomvc-demo"})]})]})}function h(e={}){const{wrapper:t}={...(0,s.R)(),...e.components};return t?(0,o.jsx)(t,{...e,children:(0,o.jsx)(c,{...e})}):c(e)}},8453(e,t,n){n.d(t,{R:()=>a,x:()=>r});var i=n(6540);const o={},s=i.createContext(o);function a(e){const t=i.useContext(s);return i.useMemo(function(){return"function"==typeof e?e(t):{...t,...e}},[t,e])}function r(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:a(e.components),i.createElement(s.Provider,{value:t},e.children)}}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.