PageSourceSearch

https://serenity-bdd.github.io/assets/js/a9fedf7f.84a7e978.js

js serenity-bdd.github.io collected 2026-10-03 09:57:24 UTC 29,711 bytes, 1 lines download raw bytes

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.