[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M7149AWRGGE1VQYCSWXBQWJ4":3,"skill-same:01M7149AWRGGE1VQYCSWXBQWJ4":107},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":26,"publishedAt":47,"linkOnly":48,"markdownContent":49,"markdownBody":50,"frontMatter":51,"refName":59,"commitSha":60,"skillDirectory":61,"forksCount":14,"repositoryHtmlUrl":62,"repositoryHomepageUrl":6,"ownerHtmlUrl":63,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":64,"install":65,"resources":78},"01M7149AWRGGE1VQYCSWXBQWJ4","playwright-java",null,"Scaffold, write, debug, and enhance enterprise-grade Playwright E2E tests in Java using Page Object Model, JUnit 5, Allure reporting, and parallel execution.","sapsapshen\u002Fai-memory-router","sapsapshen","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F20347959?v=4","01M6XDQEF1DP50DKF37H81J3J5","shenmian","01M6XDQEJPE41Y3DT2763X8ZJN",0,"MIT","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fcopilot\u002Fplaywright-java\u002FSKILL.md",[18,23],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000008","测试与质量","SCENARIO","使用场景",{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46],"java","playwright","playwright java","test-automation","parallel","safe","write","reporting","e2e tests","playwright e2e tests","selenium","2025","testing","selenium-alternative","allure","page-object-model","junit5","e2e-testing","model","community","2026-11-10 10:12:30",false,"---\nname: playwright-java\ndescription: \"Scaffold, write, debug, and enhance enterprise-grade Playwright E2E tests in Java using Page Object Model, JUnit 5, Allure reporting, and parallel execution.\"\ncategory: test-automation\nrisk: safe\nsource: community\ndate_added: \"2025-03-08\"\nauthor: amalsam18\ntags: [playwright, java, e2e-testing, junit5, page-object-model, allure, selenium-alternative]\ntools: [claude, cursor,antigravity]\n---\n\n# Playwright Java – Advanced Test Automation\n\n## Overview\n\nThis skill produces production-quality, enterprise-grade Playwright Java test code.\nIt enforces the Page Object Model (POM), strict locator strategies, thread-safe parallel\nexecution, and full Allure reporting integration. Targets Java 17+ and Playwright 1.44+.\n\nSupporting reference files are available for deeper topics:\n\n| Topic | File |\n|-------|------|\n| Maven POM, ConfigReader, Docker\u002FCI setup | `references\u002Fconfig.md` |\n| Component pattern, dropdowns, uploads, waits | `references\u002Fpage-objects.md` |\n| Full assertion API, soft assertions, visual testing | `references\u002Fassertions.md` |\n| Fixtures, test data factory, auth state, retry | `references\u002Ffixtures.md` |\n| Drop-in base class templates | `templates\u002FBaseTest.java`, `templates\u002FBasePage.java` |\n\n---\n\n## When to Use This Skill\n\n- Use when scaffolding a new Playwright Java project from scratch\n- Use when writing Page Object classes or JUnit 5 test classes\n- Use when the user asks about cross-browser testing, parallel execution, or Allure reports\n- Use when fixing flaky tests or replacing `Thread.sleep()` with proper waits\n- Use when setting up Playwright in CI\u002FCD pipelines (GitHub Actions, Jenkins, Docker)\n- Use when combining API calls and UI assertions in a single test (hybrid testing)\n- Use when the user mentions \"POM pattern\", \"BrowserContext\", \"Playwright fixtures\", or \"traces\"\n\n---\n\n## How It Works\n\n### Step 1: Decide the Approach\n\nUse this matrix to pick the right pattern before writing any code:\n\n| User Request | Approach |\n|---|---|\n| New project from scratch | Full scaffold — see `references\u002Fconfig.md` |\n| Single feature test | POM page class + JUnit5 test class |\n| API + UI hybrid | `APIRequestContext` alongside `Page` |\n| Cross-browser | `@MethodSource` parameterized over browser names |\n| Flaky test fix | Replace `sleep` with `waitFor` \u002F `waitForResponse` |\n| CI integration | `playwright install --with-deps` in pipeline |\n| Parallel execution | `junit-platform.properties` + `ThreadLocal` |\n| Rich reporting | Allure + Playwright trace + video recording |\n\n---\n\n### Step 2: Scaffold the Project Structure\n\nAlways use this layout when creating a new project:\n\n```\nsrc\u002F\n├── test\u002F\n│   ├── java\u002Fcom\u002Fcompany\u002Ftests\u002F\n│   │   ├── base\u002F\n│   │   │   ├── BaseTest.java        ← templates\u002FBaseTest.java\n│   │   │   └── BasePage.java        ← templates\u002FBasePage.java\n│   │   ├── pages\u002F\n│   │   │   └── LoginPage.java\n│   │   ├── tests\u002F\n│   │   │   └── LoginTest.java\n│   │   ├── utils\u002F\n│   │   │   ├── TestDataFactory.java\n│   │   │   └── WaitUtils.java\n│   │   └── config\u002F\n│   │       └── ConfigReader.java\n│   └── resources\u002F\n│       ├── test.properties\n│       ├── junit-platform.properties\n│       └── testdata\u002Fusers.json\npom.xml\n```\n\n---\n\n### Step 3: Set Up Thread-Safe BaseTest\n\n```java\npublic class BaseTest {\n    protected static ThreadLocal\u003CPlaywright>     playwrightTL = new ThreadLocal\u003C>();\n    protected static ThreadLocal\u003CBrowser>        browserTL    = new ThreadLocal\u003C>();\n    protected static ThreadLocal\u003CBrowserContext> contextTL    = new ThreadLocal\u003C>();\n    protected static ThreadLocal\u003CPage>           pageTL       = new ThreadLocal\u003C>();\n\n    protected Page page() { return pageTL.get(); }\n\n    @BeforeEach\n    void setUp() {\n        Playwright playwright = Playwright.create();\n        playwrightTL.set(playwright);\n\n        Browser browser = resolveBrowser(playwright).launch(\n            new BrowserType.LaunchOptions()\n                .setHeadless(ConfigReader.isHeadless()));\n        browserTL.set(browser);\n\n        BrowserContext context = browser.newContext(new Browser.NewContextOptions()\n            .setViewportSize(1920, 1080)\n            .setRecordVideoDir(Paths.get(\"target\u002Fvideos\u002F\"))\n            .setLocale(\"en-US\"));\n        context.tracing().start(new Tracing.StartOptions()\n            .setScreenshots(true).setSnapshots(true));\n        contextTL.set(context);\n        pageTL.set(context.newPage());\n    }\n\n    @AfterEach\n    void tearDown(TestInfo testInfo) {\n        String name = testInfo.getDisplayName().replaceAll(\"[^a-zA-Z0-9]\", \"_\");\n        contextTL.get().tracing().stop(new Tracing.StopOptions()\n            .setPath(Paths.get(\"target\u002Ftraces\u002F\" + name + \".zip\")));\n        pageTL.get().close();\n        contextTL.get().close();\n        browserTL.get().close();\n        playwrightTL.get().close();\n    }\n\n    private BrowserType resolveBrowser(Playwright pw) {\n        return switch (System.getProperty(\"browser\", \"chromium\").toLowerCase()) {\n            case \"firefox\" -> pw.firefox();\n            case \"webkit\"  -> pw.webkit();\n            default        -> pw.chromium();\n        };\n    }\n}\n```\n\n---\n\n### Step 4: Build Page Object Classes\n\n```java\npublic class LoginPage extends BasePage {\n\n    \u002F\u002F Declare ALL locators as fields — never inline in action methods\n    private final Locator emailInput;\n    private final Locator passwordInput;\n    private final Locator loginButton;\n    private final Locator errorMessage;\n\n    public LoginPage(Page page) {\n        super(page);\n        emailInput    = page.getByLabel(\"Email address\");\n        passwordInput = page.getByLabel(\"Password\");\n        loginButton   = page.getByRole(AriaRole.BUTTON,\n                            new Page.GetByRoleOptions().setName(\"Sign in\"));\n        errorMessage  = page.getByTestId(\"login-error\");\n    }\n\n    @Override protected String getUrl() { return \"\u002Flogin\"; }\n\n    \u002F\u002F Navigation methods return the next Page Object — enables fluent chaining\n    public DashboardPage loginAs(String email, String password) {\n        fill(emailInput, email);\n        fill(passwordInput, password);\n        clickAndWaitForNav(loginButton);\n        return new DashboardPage(page);\n    }\n\n    public LoginPage loginExpectingError(String email, String password) {\n        fill(emailInput, email);\n        fill(passwordInput, password);\n        loginButton.click();\n        errorMessage.waitFor();\n        return this;\n    }\n\n    public String getErrorMessage() { return errorMessage.textContent(); }\n}\n```\n\n---\n\n### Step 5: Write Tests with Allure Annotations\n\n```java\n@ExtendWith(AllureJunit5.class)\nclass LoginTest extends BaseTest {\n\n    private LoginPage loginPage;\n\n    @BeforeEach\n    void openLoginPage() {\n        loginPage = new LoginPage(page());\n        loginPage.navigate();\n    }\n\n    @Test\n    @Severity(SeverityLevel.BLOCKER)\n    @DisplayName(\"Valid credentials redirect to dashboard\")\n    void shouldLoginWithValidCredentials() {\n        User user = TestDataFactory.getDefaultUser();\n        DashboardPage dash = loginPage.loginAs(user.email(), user.password());\n\n        assertThat(page()).hasURL(Pattern.compile(\".*\u002Fdashboard\"));\n        assertThat(dash.getWelcomeBanner()).containsText(\"Welcome, \" + user.firstName());\n    }\n\n    @Test\n    void shouldShowErrorOnInvalidCredentials() {\n        loginPage.loginExpectingError(\"bad@test.com\", \"wrongpass\");\n\n        SoftAssertions softly = new SoftAssertions();\n        softly.assertThat(loginPage.getErrorMessage()).contains(\"Invalid email or password\");\n        softly.assertThat(page()).hasURL(Pattern.compile(\".*\u002Flogin\"));\n        softly.assertAll();\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"provideInvalidCredentials\")\n    void shouldRejectInvalidCredentials(String email, String password, String expectedError) {\n        loginPage.loginExpectingError(email, password);\n        assertThat(loginPage.getErrorMessage()).containsText(expectedError);\n    }\n\n    static Stream\u003CArguments> provideInvalidCredentials() {\n        return Stream.of(\n            Arguments.of(\"\", \"password123\", \"Email is required\"),\n            Arguments.of(\"user@test.com\", \"\", \"Password is required\"),\n            Arguments.of(\"notanemail\", \"pass\", \"Invalid email format\")\n        );\n    }\n}\n```\n\n---\n\n## Examples\n\n### Example 1: API + UI Hybrid Test\n\n```java\n@Test\nvoid shouldDisplayNewlyCreatedOrder() {\n    \u002F\u002F Arrange via API — faster than navigating through UI\n    APIRequestContext api = page().context().request();\n    APIResponse response = api.post(\"\u002Fapi\u002Forders\",\n        RequestOptions.create()\n            .setHeader(\"Authorization\", \"Bearer \" + authToken)\n            .setData(Map.of(\"productId\", \"SKU-001\", \"quantity\", 2)));\n    assertThat(response).isOK();\n\n    String orderId = new JsonParser().parse(response.text())\n        .getAsJsonObject().get(\"id\").getAsString();\n\n    OrdersPage orders = new OrdersPage(page());\n    orders.navigate();\n    assertThat(orders.getOrderRowById(orderId)).isVisible();\n}\n```\n\n### Example 2: Network Mocking\n\n```java\n@Test\nvoid shouldHandleApiFailureGracefully() {\n    page().route(\"**\u002Fapi\u002Fproducts\", route -> route.fulfill(\n        new Route.FulfillOptions()\n            .setStatus(503)\n            .setBody(\"{\\\"error\\\":\\\"Service Unavailable\\\"}\")\n            .setContentType(\"application\u002Fjson\")));\n\n    ProductsPage products = new ProductsPage(page());\n    products.navigate();\n\n    assertThat(products.getErrorBanner())\n        .hasText(\"We're having trouble loading products. Please try again.\");\n}\n```\n\n### Example 3: Parallel Cross-Browser Test\n\n```java\n@ParameterizedTest\n@MethodSource(\"browsers\")\nvoid shouldRenderCheckoutOnAllBrowsers(String browserName) {\n    System.setProperty(\"browser\", browserName);\n    new CheckoutPage(page()).navigate();\n    assertThat(page().locator(\".checkout-form\")).isVisible();\n}\n\nstatic Stream\u003CString> browsers() {\n    return Stream.of(\"chromium\", \"firefox\", \"webkit\");\n}\n```\n\n### Example 4: Parallel Execution Config\n\n```properties\n# src\u002Ftest\u002Fresources\u002Fjunit-platform.properties\njunit.jupiter.execution.parallel.enabled=true\njunit.jupiter.execution.parallel.mode.default=concurrent\njunit.jupiter.execution.parallel.config.strategy=fixed\njunit.jupiter.execution.parallel.config.fixed.parallelism=4\n```\n\n### Example 5: GitHub Actions CI Pipeline\n\n```yaml\n- name: Install Playwright browsers\n  run: mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args=\"install --with-deps\"\n\n- name: Run tests\n  run: mvn test -Dbrowser=${{ matrix.browser }} -Dheadless=true\n\n- name: Upload traces on failure\n  uses: actions\u002Fupload-artifact@v4\n  if: failure()\n  with:\n    name: playwright-traces\n    path: target\u002Ftraces\u002F\n\n- name: Upload Allure results\n  uses: actions\u002Fupload-artifact@v4\n  if: always()\n  with:\n    name: allure-results\n    path: target\u002Fallure-results\u002F\n```\n\n---\n\n## Best Practices\n\n- ✅ Use `ThreadLocal\u003CPage>` for every parallel-safe test suite\n- ✅ Declare all `Locator` fields at the top of the Page Object class\n- ✅ Return the next Page Object from navigation methods (fluent chaining)\n- ✅ Use `assertThat(locator)` — it auto-retries until timeout\n- ✅ Use `getByRole`, `getByLabel`, `getByTestId` as first-choice locators\n- ✅ Start tracing in `@BeforeEach` and stop with a file path in `@AfterEach`\n- ✅ Use `SoftAssertions` when validating multiple fields on a single page\n- ✅ Set up saved auth state (`storageState`) to skip login across test classes\n- ❌ Never use `Thread.sleep()` — replace with `waitFor()` or `waitForResponse()`\n- ❌ Never hardcode base URLs — always use `ConfigReader.getBaseUrl()`\n- ❌ Never create a `Playwright` instance inside a Page Object\n- ❌ Never use XPath for dynamic or frequently changing elements\n\n---\n\n## Common Pitfalls\n\n- **Problem:** Tests fail randomly in parallel mode\n  **Solution:** Ensure every test creates its own `Playwright → Browser → BrowserContext → Page` chain via `ThreadLocal`. Never share a `Page` across threads.\n\n- **Problem:** `assertThat(locator).isVisible()` times out even when the element appears\n  **Solution:** Increase timeout with `.setTimeout(10_000)` or raise `context.setDefaultTimeout()` in `BaseTest`.\n\n- **Problem:** `Thread.sleep(2000)` was added but tests are still flaky\n  **Solution:** Replace with `page.waitForResponse(\"**\u002Fapi\u002Fendpoint\", () -> action())` or `assertThat(locator).hasText(\"Done\")` which polls automatically.\n\n- **Problem:** Playwright trace zip is empty or missing\n  **Solution:** Ensure `tracing().start()` is called before test actions and `tracing().stop()` is in `@AfterEach` — not `@AfterAll`.\n\n- **Problem:** Allure report is blank or missing steps\n  **Solution:** Add the AspectJ agent to `maven-surefire-plugin` `\u003CargLine>` in `pom.xml` — see `references\u002Fconfig.md` for the exact snippet.\n\n- **Problem:** `storageState` auth file is stale and tests redirect to login\n  **Solution:** Re-run `AuthSetup` to regenerate `target\u002Fauth\u002Fuser-state.json` before the suite, or add a `@BeforeAll` that conditionally refreshes it.\n\n---\n\n## Related Skills\n\n- `@rest-assured-java` — Use for pure API test suites without any UI interaction\n- `@selenium-java` — Legacy alternative; prefer Playwright for all new projects\n- `@allure-reporting` — Deep-dive into Allure annotations, categories, and history trends\n- `@testcontainers-java` — Use alongside this skill when tests need a live database or service\n- `@github-actions-ci` — For building complete multi-browser matrix CI pipelines\n","# Playwright Java – Advanced Test Automation\n\n## Overview\n\nThis skill produces production-quality, enterprise-grade Playwright Java test code.\nIt enforces the Page Object Model (POM), strict locator strategies, thread-safe parallel\nexecution, and full Allure reporting integration. Targets Java 17+ and Playwright 1.44+.\n\nSupporting reference files are available for deeper topics:\n\n| Topic | File |\n|-------|------|\n| Maven POM, ConfigReader, Docker\u002FCI setup | `references\u002Fconfig.md` |\n| Component pattern, dropdowns, uploads, waits | `references\u002Fpage-objects.md` |\n| Full assertion API, soft assertions, visual testing | `references\u002Fassertions.md` |\n| Fixtures, test data factory, auth state, retry | `references\u002Ffixtures.md` |\n| Drop-in base class templates | `templates\u002FBaseTest.java`, `templates\u002FBasePage.java` |\n\n---\n\n## When to Use This Skill\n\n- Use when scaffolding a new Playwright Java project from scratch\n- Use when writing Page Object classes or JUnit 5 test classes\n- Use when the user asks about cross-browser testing, parallel execution, or Allure reports\n- Use when fixing flaky tests or replacing `Thread.sleep()` with proper waits\n- Use when setting up Playwright in CI\u002FCD pipelines (GitHub Actions, Jenkins, Docker)\n- Use when combining API calls and UI assertions in a single test (hybrid testing)\n- Use when the user mentions \"POM pattern\", \"BrowserContext\", \"Playwright fixtures\", or \"traces\"\n\n---\n\n## How It Works\n\n### Step 1: Decide the Approach\n\nUse this matrix to pick the right pattern before writing any code:\n\n| User Request | Approach |\n|---|---|\n| New project from scratch | Full scaffold — see `references\u002Fconfig.md` |\n| Single feature test | POM page class + JUnit5 test class |\n| API + UI hybrid | `APIRequestContext` alongside `Page` |\n| Cross-browser | `@MethodSource` parameterized over browser names |\n| Flaky test fix | Replace `sleep` with `waitFor` \u002F `waitForResponse` |\n| CI integration | `playwright install --with-deps` in pipeline |\n| Parallel execution | `junit-platform.properties` + `ThreadLocal` |\n| Rich reporting | Allure + Playwright trace + video recording |\n\n---\n\n### Step 2: Scaffold the Project Structure\n\nAlways use this layout when creating a new project:\n\n```\nsrc\u002F\n├── test\u002F\n│   ├── java\u002Fcom\u002Fcompany\u002Ftests\u002F\n│   │   ├── base\u002F\n│   │   │   ├── BaseTest.java        ← templates\u002FBaseTest.java\n│   │   │   └── BasePage.java        ← templates\u002FBasePage.java\n│   │   ├── pages\u002F\n│   │   │   └── LoginPage.java\n│   │   ├── tests\u002F\n│   │   │   └── LoginTest.java\n│   │   ├── utils\u002F\n│   │   │   ├── TestDataFactory.java\n│   │   │   └── WaitUtils.java\n│   │   └── config\u002F\n│   │       └── ConfigReader.java\n│   └── resources\u002F\n│       ├── test.properties\n│       ├── junit-platform.properties\n│       └── testdata\u002Fusers.json\npom.xml\n```\n\n---\n\n### Step 3: Set Up Thread-Safe BaseTest\n\n```java\npublic class BaseTest {\n    protected static ThreadLocal\u003CPlaywright>     playwrightTL = new ThreadLocal\u003C>();\n    protected static ThreadLocal\u003CBrowser>        browserTL    = new ThreadLocal\u003C>();\n    protected static ThreadLocal\u003CBrowserContext> contextTL    = new ThreadLocal\u003C>();\n    protected static ThreadLocal\u003CPage>           pageTL       = new ThreadLocal\u003C>();\n\n    protected Page page() { return pageTL.get(); }\n\n    @BeforeEach\n    void setUp() {\n        Playwright playwright = Playwright.create();\n        playwrightTL.set(playwright);\n\n        Browser browser = resolveBrowser(playwright).launch(\n            new BrowserType.LaunchOptions()\n                .setHeadless(ConfigReader.isHeadless()));\n        browserTL.set(browser);\n\n        BrowserContext context = browser.newContext(new Browser.NewContextOptions()\n            .setViewportSize(1920, 1080)\n            .setRecordVideoDir(Paths.get(\"target\u002Fvideos\u002F\"))\n            .setLocale(\"en-US\"));\n        context.tracing().start(new Tracing.StartOptions()\n            .setScreenshots(true).setSnapshots(true));\n        contextTL.set(context);\n        pageTL.set(context.newPage());\n    }\n\n    @AfterEach\n    void tearDown(TestInfo testInfo) {\n        String name = testInfo.getDisplayName().replaceAll(\"[^a-zA-Z0-9]\", \"_\");\n        contextTL.get().tracing().stop(new Tracing.StopOptions()\n            .setPath(Paths.get(\"target\u002Ftraces\u002F\" + name + \".zip\")));\n        pageTL.get().close();\n        contextTL.get().close();\n        browserTL.get().close();\n        playwrightTL.get().close();\n    }\n\n    private BrowserType resolveBrowser(Playwright pw) {\n        return switch (System.getProperty(\"browser\", \"chromium\").toLowerCase()) {\n            case \"firefox\" -> pw.firefox();\n            case \"webkit\"  -> pw.webkit();\n            default        -> pw.chromium();\n        };\n    }\n}\n```\n\n---\n\n### Step 4: Build Page Object Classes\n\n```java\npublic class LoginPage extends BasePage {\n\n    \u002F\u002F Declare ALL locators as fields — never inline in action methods\n    private final Locator emailInput;\n    private final Locator passwordInput;\n    private final Locator loginButton;\n    private final Locator errorMessage;\n\n    public LoginPage(Page page) {\n        super(page);\n        emailInput    = page.getByLabel(\"Email address\");\n        passwordInput = page.getByLabel(\"Password\");\n        loginButton   = page.getByRole(AriaRole.BUTTON,\n                            new Page.GetByRoleOptions().setName(\"Sign in\"));\n        errorMessage  = page.getByTestId(\"login-error\");\n    }\n\n    @Override protected String getUrl() { return \"\u002Flogin\"; }\n\n    \u002F\u002F Navigation methods return the next Page Object — enables fluent chaining\n    public DashboardPage loginAs(String email, String password) {\n        fill(emailInput, email);\n        fill(passwordInput, password);\n        clickAndWaitForNav(loginButton);\n        return new DashboardPage(page);\n    }\n\n    public LoginPage loginExpectingError(String email, String password) {\n        fill(emailInput, email);\n        fill(passwordInput, password);\n        loginButton.click();\n        errorMessage.waitFor();\n        return this;\n    }\n\n    public String getErrorMessage() { return errorMessage.textContent(); }\n}\n```\n\n---\n\n### Step 5: Write Tests with Allure Annotations\n\n```java\n@ExtendWith(AllureJunit5.class)\nclass LoginTest extends BaseTest {\n\n    private LoginPage loginPage;\n\n    @BeforeEach\n    void openLoginPage() {\n        loginPage = new LoginPage(page());\n        loginPage.navigate();\n    }\n\n    @Test\n    @Severity(SeverityLevel.BLOCKER)\n    @DisplayName(\"Valid credentials redirect to dashboard\")\n    void shouldLoginWithValidCredentials() {\n        User user = TestDataFactory.getDefaultUser();\n        DashboardPage dash = loginPage.loginAs(user.email(), user.password());\n\n        assertThat(page()).hasURL(Pattern.compile(\".*\u002Fdashboard\"));\n        assertThat(dash.getWelcomeBanner()).containsText(\"Welcome, \" + user.firstName());\n    }\n\n    @Test\n    void shouldShowErrorOnInvalidCredentials() {\n        loginPage.loginExpectingError(\"bad@test.com\", \"wrongpass\");\n\n        SoftAssertions softly = new SoftAssertions();\n        softly.assertThat(loginPage.getErrorMessage()).contains(\"Invalid email or password\");\n        softly.assertThat(page()).hasURL(Pattern.compile(\".*\u002Flogin\"));\n        softly.assertAll();\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"provideInvalidCredentials\")\n    void shouldRejectInvalidCredentials(String email, String password, String expectedError) {\n        loginPage.loginExpectingError(email, password);\n        assertThat(loginPage.getErrorMessage()).containsText(expectedError);\n    }\n\n    static Stream\u003CArguments> provideInvalidCredentials() {\n        return Stream.of(\n            Arguments.of(\"\", \"password123\", \"Email is required\"),\n            Arguments.of(\"user@test.com\", \"\", \"Password is required\"),\n            Arguments.of(\"notanemail\", \"pass\", \"Invalid email format\")\n        );\n    }\n}\n```\n\n---\n\n## Examples\n\n### Example 1: API + UI Hybrid Test\n\n```java\n@Test\nvoid shouldDisplayNewlyCreatedOrder() {\n    \u002F\u002F Arrange via API — faster than navigating through UI\n    APIRequestContext api = page().context().request();\n    APIResponse response = api.post(\"\u002Fapi\u002Forders\",\n        RequestOptions.create()\n            .setHeader(\"Authorization\", \"Bearer \" + authToken)\n            .setData(Map.of(\"productId\", \"SKU-001\", \"quantity\", 2)));\n    assertThat(response).isOK();\n\n    String orderId = new JsonParser().parse(response.text())\n        .getAsJsonObject().get(\"id\").getAsString();\n\n    OrdersPage orders = new OrdersPage(page());\n    orders.navigate();\n    assertThat(orders.getOrderRowById(orderId)).isVisible();\n}\n```\n\n### Example 2: Network Mocking\n\n```java\n@Test\nvoid shouldHandleApiFailureGracefully() {\n    page().route(\"**\u002Fapi\u002Fproducts\", route -> route.fulfill(\n        new Route.FulfillOptions()\n            .setStatus(503)\n            .setBody(\"{\\\"error\\\":\\\"Service Unavailable\\\"}\")\n            .setContentType(\"application\u002Fjson\")));\n\n    ProductsPage products = new ProductsPage(page());\n    products.navigate();\n\n    assertThat(products.getErrorBanner())\n        .hasText(\"We're having trouble loading products. Please try again.\");\n}\n```\n\n### Example 3: Parallel Cross-Browser Test\n\n```java\n@ParameterizedTest\n@MethodSource(\"browsers\")\nvoid shouldRenderCheckoutOnAllBrowsers(String browserName) {\n    System.setProperty(\"browser\", browserName);\n    new CheckoutPage(page()).navigate();\n    assertThat(page().locator(\".checkout-form\")).isVisible();\n}\n\nstatic Stream\u003CString> browsers() {\n    return Stream.of(\"chromium\", \"firefox\", \"webkit\");\n}\n```\n\n### Example 4: Parallel Execution Config\n\n```properties\n# src\u002Ftest\u002Fresources\u002Fjunit-platform.properties\njunit.jupiter.execution.parallel.enabled=true\njunit.jupiter.execution.parallel.mode.default=concurrent\njunit.jupiter.execution.parallel.config.strategy=fixed\njunit.jupiter.execution.parallel.config.fixed.parallelism=4\n```\n\n### Example 5: GitHub Actions CI Pipeline\n\n```yaml\n- name: Install Playwright browsers\n  run: mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args=\"install --with-deps\"\n\n- name: Run tests\n  run: mvn test -Dbrowser=${{ matrix.browser }} -Dheadless=true\n\n- name: Upload traces on failure\n  uses: actions\u002Fupload-artifact@v4\n  if: failure()\n  with:\n    name: playwright-traces\n    path: target\u002Ftraces\u002F\n\n- name: Upload Allure results\n  uses: actions\u002Fupload-artifact@v4\n  if: always()\n  with:\n    name: allure-results\n    path: target\u002Fallure-results\u002F\n```\n\n---\n\n## Best Practices\n\n- ✅ Use `ThreadLocal\u003CPage>` for every parallel-safe test suite\n- ✅ Declare all `Locator` fields at the top of the Page Object class\n- ✅ Return the next Page Object from navigation methods (fluent chaining)\n- ✅ Use `assertThat(locator)` — it auto-retries until timeout\n- ✅ Use `getByRole`, `getByLabel`, `getByTestId` as first-choice locators\n- ✅ Start tracing in `@BeforeEach` and stop with a file path in `@AfterEach`\n- ✅ Use `SoftAssertions` when validating multiple fields on a single page\n- ✅ Set up saved auth state (`storageState`) to skip login across test classes\n- ❌ Never use `Thread.sleep()` — replace with `waitFor()` or `waitForResponse()`\n- ❌ Never hardcode base URLs — always use `ConfigReader.getBaseUrl()`\n- ❌ Never create a `Playwright` instance inside a Page Object\n- ❌ Never use XPath for dynamic or frequently changing elements\n\n---\n\n## Common Pitfalls\n\n- **Problem:** Tests fail randomly in parallel mode\n  **Solution:** Ensure every test creates its own `Playwright → Browser → BrowserContext → Page` chain via `ThreadLocal`. Never share a `Page` across threads.\n\n- **Problem:** `assertThat(locator).isVisible()` times out even when the element appears\n  **Solution:** Increase timeout with `.setTimeout(10_000)` or raise `context.setDefaultTimeout()` in `BaseTest`.\n\n- **Problem:** `Thread.sleep(2000)` was added but tests are still flaky\n  **Solution:** Replace with `page.waitForResponse(\"**\u002Fapi\u002Fendpoint\", () -> action())` or `assertThat(locator).hasText(\"Done\")` which polls automatically.\n\n- **Problem:** Playwright trace zip is empty or missing\n  **Solution:** Ensure `tracing().start()` is called before test actions and `tracing().stop()` is in `@AfterEach` — not `@AfterAll`.\n\n- **Problem:** Allure report is blank or missing steps\n  **Solution:** Add the AspectJ agent to `maven-surefire-plugin` `\u003CargLine>` in `pom.xml` — see `references\u002Fconfig.md` for the exact snippet.\n\n- **Problem:** `storageState` auth file is stale and tests redirect to login\n  **Solution:** Re-run `AuthSetup` to regenerate `target\u002Fauth\u002Fuser-state.json` before the suite, or add a `@BeforeAll` that conditionally refreshes it.\n\n---\n\n## Related Skills\n\n- `@rest-assured-java` — Use for pure API test suites without any UI interaction\n- `@selenium-java` — Legacy alternative; prefer Playwright for all new projects\n- `@allure-reporting` — Deep-dive into Allure annotations, categories, and history trends\n- `@testcontainers-java` — Use alongside this skill when tests need a live database or service\n- `@github-actions-ci` — For building complete multi-browser matrix CI pipelines\n",{"name":5,"risk":32,"tags":52,"tools":53,"author":57,"source":46,"category":30,"date_added":58,"description":7},[28,27,44,43,42,41,40],[54,55,56],"claude","cursor","antigravity","amalsam18","2025-03-08","main","7a38d8e7073e61c16eda85681cb32fb68a8657a5","skills-store\u002Fcopilot\u002Fplaywright-java","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router","https:\u002F\u002Fgithub.com\u002Fsapsapshen","2026-04-14 21:12:12",{"source":8,"skillName":5,"commands":66,"prompt":76,"zipAvailable":77},[67,70,73],{"tool":68,"command":69},"npx","npx skills add sapsapshen\u002Fai-memory-router --skill playwright-java",{"tool":71,"command":72},"bunx","bunx skills add sapsapshen\u002Fai-memory-router --skill playwright-java",{"tool":74,"command":75},"pnpm","pnpm dlx skills add sapsapshen\u002Fai-memory-router --skill playwright-java","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fcopilot\u002Fplaywright-java\u002FSKILL.md",true,[79,85,91,95,99,103],{"path":80,"resourceType":81,"resourceTypeDesc":82,"sizeBytes":83,"sourceHtmlUrl":84},"assertions.md","REFERENCE","参考文档",4226,"https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fcopilot\u002Fplaywright-java\u002Fassertions.md",{"path":86,"resourceType":87,"resourceTypeDesc":88,"sizeBytes":89,"sourceHtmlUrl":90},"BasePage.java","SCRIPT","脚本",4830,"https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fcopilot\u002Fplaywright-java\u002FBasePage.java",{"path":92,"resourceType":87,"resourceTypeDesc":88,"sizeBytes":93,"sourceHtmlUrl":94},"BaseTest.java",4526,"https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fcopilot\u002Fplaywright-java\u002FBaseTest.java",{"path":96,"resourceType":81,"resourceTypeDesc":82,"sizeBytes":97,"sourceHtmlUrl":98},"config.md",6651,"https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fcopilot\u002Fplaywright-java\u002Fconfig.md",{"path":100,"resourceType":81,"resourceTypeDesc":82,"sizeBytes":101,"sourceHtmlUrl":102},"fixtures.md",7755,"https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fcopilot\u002Fplaywright-java\u002Ffixtures.md",{"path":104,"resourceType":81,"resourceTypeDesc":82,"sizeBytes":105,"sourceHtmlUrl":106},"page-objects.md",6104,"https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fcopilot\u002Fplaywright-java\u002Fpage-objects.md",[108,132,158,168,181,198],{"skillNo":109,"name":110,"nameZh":6,"description":111,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":112,"categories":113,"keywords":119,"publishedAt":47},"01M71ATQRKG48SH06FQ697T5G9","wechat-search","Search WeChat Official Account articles using OpenClaw's web search and fetch capabilities with compliance-focused design.","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Fwechat-search-release\u002FSKILL.md",[114],{"categoryNo":115,"categoryName":116,"taxonomyType":117,"taxonomyTypeDesc":118},"01JBSEEDCAT000000000000037","律师与法务","PROFESSION","适用职业",[120,121,122,123,124,125,126,127,128,129,130,131],"wechat search","wechat","search","capabilities","openclaw","design","web search","compliance","web","official","fetch","with",{"skillNo":133,"name":134,"nameZh":6,"description":135,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":136,"categories":137,"keywords":138,"publishedAt":47},"01M71ATNAVGZB35PFYM2GC5N00","trend-tap","Real-time trending topics aggregator across 7 platforms (X\u002FTwitter, Reddit, Google Trends, Hacker News, Zhihu, Bilibili, Weibo). Trigger: when user says 'trend', 'trends', 'trend tap', '热点', '热搜', or asks what is trending\u002Fhot. Do NOT use web_search — this skill already fetches live data from all platforms with zero API keys.","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Ftrend-tap\u002FSKILL.md",[],[139,140,141,142,143,144,122,145,146,147,148,149,150,151,152,153,154,155,156,157],"trend","trend tap","tap","api keys","Bash","time","trending","hacker","aggregator","do","when","brew","live","data","google","install","read","across","api",{"skillNo":159,"name":160,"nameZh":6,"description":161,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":162,"categories":163,"keywords":164,"publishedAt":47},"01M71ATJ9AM5KM15AA8PS302EB","trend-radar","Real-time trending topics aggregator across 7 platforms (X\u002FTwitter, Reddit, Google Trends, Hacker News, Zhihu, Bilibili, Weibo). Trigger: when user says 'trend', 'trends', 'trend pulse', '热点', '热搜', or asks what is trending\u002Fhot. Do NOT use web_search — this skill already fetches live data from all platforms with zero API keys.","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Ftrend-radar\u002FSKILL.md",[],[165,166,139,150,151,152,153,143,122,155,167,156,157,149,147,146,145,154,144,142],"trend radar","radar","pulse",{"skillNo":169,"name":170,"nameZh":6,"description":171,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":172,"categories":173,"keywords":174,"publishedAt":47},"01M71ATGF095N3EJM9G8SD0BB5","tech-news-daily","每日科技新闻汇——从全球多个 RSS 源抓取最新科技新闻，翻译（英→中）、去重、排版成每日简报。当用户询问科技新闻、科技新鲜事、每日科技、技术动态、Daily Tech News 或类似表达时触发此技能。","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Ftech-news-daily\u002FSKILL.md",[],[175,176,177,178,179,180],"news","tech","tech news daily","daily","rss","翻译",{"skillNo":182,"name":183,"nameZh":6,"description":184,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":185,"categories":186,"keywords":190,"publishedAt":47},"01M71ATFAEAMQ17M70NQNY7SR8","summarize","Summarize URLs or files with the summarize CLI (web, PDFs, images, audio, YouTube).","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Fsummarize\u002FSKILL.md",[187],{"categoryNo":188,"categoryName":189,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000021","资料检索与研究",[183,128,191,192,193,131,150,194,195,196,141,197,154],"Audio","cli","youtube","images","https","files","pdfs",{"skillNo":199,"name":200,"nameZh":6,"description":201,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":202,"categories":203,"keywords":204,"publishedAt":47},"01M71ATDNS3R0M4H936PACQAWZ","sonoscli","Control Sonos speakers (discover\u002Fstatus\u002Fplay\u002Fvolume\u002Fgroup).","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Fsonoscli\u002FSKILL.md",[],[200,205,206,207,154,208,195,209,210,211,212,213],"status","play","go","latest","github","discover","control","cmd","volume"]