Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

pomwright

DyHex441Apache 2.01.2.0TypeScript support: included

POMWright is a complementary test framework for Playwright written in TypeScript.

Playwright, POM, Page Object Model, Locator, Chaining, Nesting, LocatorSchema, SessionStorage, Test, Test Framework, E2E, End-to-End Testing, Automation, Browser Automation, Web Testing, Logging, Log Levels, TypeScript

readme


"pomwright": minor

POMWright

GitHub Actions Workflow Status NPM Version NPM Downloads GitHub License NPM dev or peer Dependency Version Static Badge

POMWright is a TypeScript-based framework that implements the Page Object Model Design Pattern, designed specifically to augment Playwright's testing capabilities.

POMWright provides a way of abstracting the implementation details of a web page and encapsulating them into a reusable page object. This approach makes the tests easier to read, write, and maintain, and helps reduce duplicated code by breaking down the code into smaller, reusable components, making the code more maintainable and organized.

Features

Easy Creation of Page Object Classes

Simply extend a class with BasePage to create a Page Object Class (POC).

Support for Multiple Domains/BaseURLs

Define different base URLs by extending an abstract class with BasePage per domain and have your POCs for each domain extend the abstract classes.

Custom Playwright Fixture Integration

Seamlessly integrate custom Playwright Fixtures with your POMWright POCs.

LocatorSchema Interface

Define comprehensive locators for each POC and share common locators between them.

Advanced Locator Management

Efficiently manage and chain locators through LocatorSchemaPaths.

Dynamic Locator Schema Updates

Modify single or multiple locators within a chained locator dynamically during tests using the new .update() and .addFilter() methods.

Deep Copy of LocatorSchemas

Ensure that original LocatorSchemas remain immutable and reusable across tests.

Custom HTML Logger

Gain insights with detailed logs for nested locators, integrated with Playwright's HTML report. Or use the Log fixture throughout your own POCs and tests to easily attach them to the HTML report based on log levels.

SessionStorage Handling

Enhance your tests with advanced sessionStorage handling capabilities.

Enhanced Locator Filtering

  • New filter Property: Apply filters across various locator types beyond just the locator method.
  • New .addFilter() Method: Dynamically add filters to any part of the locator chain.

Installation

Ensure you have Node.js installed, then run:

npm install pomwright --save-dev

or

pnpm i -D pomwright

Playwright Example Project

Explore POMWright in action by diving into the example project located in the "example" folder. Follow these steps to get started:

Install

Navigate to the "example" folder and install the necessary dependencies:

pnpm install

Playwright Browsers

Install or update Playwright browsers:

pnpm playwright install --with-deps

Run Tests

Execute tests across Chromium, Firefox, and WebKit:

pnpm playwright test

Parallelism

Control parallel test execution. By default, up to 4 tests run in parallel. Modify this setting as needed:

pnpm playwright test --workers 2  # Set the number of parallel workers

Reports

After the tests complete, a Playwright HTML report is available in ./example/playwright-report. Open the index.html file in your browser to view the results.

Usage

Dive into using POMWright with these examples:

Create a Page Object Class (POC)

import { Page, TestInfo } from "@playwright/test";
import { BasePage, PlaywrightReportLogger, GetByMethod } from "pomwright";

type LocatorSchemaPath = 
  | "content"
  | "content.heading"
  | "content.region.details"
  | "content.region.details.button.edit";

export default class Profile extends BasePage<LocatorSchemaPath> {
  constructor(page: Page, testInfo: TestInfo, pwrl: PlaywrightReportLogger) {
    super(page, testInfo, "https://someDomain.com", "/profile", Profile.name, pwrl);
  }

  protected initLocatorSchemas() {
    this.locators.addSchema("content", {
      locator: ".main-content",
      locatorMethod: GetByMethod.locator
    });

    this.locators.addSchema("content.heading", {
      role: "heading",
      roleOptions: {
        name: "Your Profile"
      },
      locatorMethod: GetByMethod.role
    });

    this.locators.addSchema("content.region.details", {
      role: "region",
      roleOptions: {
        name: "Profile Details"
      },
      locatorMethod: GetByMethod.role
    });

    this.locators.addSchema("content.region.details.button.edit", {
      role: "button",
      roleOptions: {
        name: "Edit"
      },
      locatorMethod: GetByMethod.role
    });
  }

  // add your helper methods here...
}

Creating a Custom Playwright Fixture

import { test as base } from "pomwright";
import Profile from "...";

type fixtures = {
  profile: Profile;
}

export const test = base.extend<fixtures>({
  profile: async ({ page, log }, use, testInfo) => {
    const profile = new Profile(page, testInfo, log);
    await use(profile);
  }
});

Using the Fixture in Playwright Tests

Click Edit Button with a Single Locator

import { test } from ".../fixtures";

test("click edit button with a single locator", async ({ profile }) => {
  // perform setup/navigation...

  await profile.page.waitForURL(profile.fullUrl);

  /** 
   * returns the locator resolving to the full locatorSchemaPath
   */
  const editBtn = await profile.getLocator("content.region.details.button.edit");

  await editBtn.click();
});

Click Edit Button with a Nested Locator

import { test } from ".../fixtures";

test("click edit button with a nested locator", async ({ profile }) => {
  // perform setup/navigation...

  await profile.page.waitForURL(profile.fullUrl);

  /** 
   * returns a nested/chained locator consisting of the 3 locators: 
   * content, 
   * content.region.details
   * content.region.details.button.edit
   */
  const editBtn = await profile.getNestedLocator("content.region.details.button.edit");

  await editBtn.click();
});

Specify Index for Nested Locators

import { test } from ".../fixtures";

test("specify index for nested locator(s)", async ({ profile }) => {
  // perform setup/navigation...

  await profile.page.waitForURL(profile.fullUrl);

  /** 
   * returns a nested/chained locator consisting of the 3 locators: 
   * content 
   * content.region.details with .first()
   * content.region.details.button.edit with .nth(1)
   */
  const editBtn = await profile.getNestedLocator("content.region.details.button.edit", {
    "content.region.details": 0, 
    "content.region.details.button.edit": 1
  });

  await editBtn.click();
});

Update a Locator Before Use

import { test } from ".../fixtures";

test("update a locator before use", async ({ profile }) => {
  // perform setup/navigation...

  await profile.page.waitForURL(profile.fullUrl);

  /** 
   * returns a nested/chained locator consisting of the 3 locators: 
   * content, 
   * content.region.details
   * content.region.details.button.edit (updated)
   */
  const editBtn = await profile.getLocatorSchema("content.region.details.button.edit")
    .update("content.region.details.button.edit", { 
      roleOptions: { name: "Edit details" }
    })
    .getNestedLocator();

  await editBtn.click();
});

Update a Nested Locator Before Use

import { test } from ".../fixtures";

test("update a nested locator before use", async ({ profile }) => {
  // perform setup/navigation...

  await profile.page.waitForURL(profile.fullUrl);

  /** 
   * returns a nested/chained locator consisting of the 3 locators: 
   * content, 
   * content.region.details (updated)
   * content.region.details.button.edit
   */
  const editBtn = await profile.getLocatorSchema("content.region.details.button.edit")
    .update("content.region.details", { 
      locator: ".profile-details",
      locatorMethod: GetByMethod.locator
    })
    .getNestedLocator();

  await editBtn.click();
});

Make Multiple Versions of a Locator

import { test } from ".../fixtures";

test("make multiple versions of a locator", async ({ profile }) => {
  // perform setup/navigation...

  await profile.page.waitForURL(profile.fullUrl);

  const editBtnSchema = profile.getLocatorSchema("content.region.details.button.edit");

  const editBtn = await editBtnSchema.getLocator();
  await editBtn.click();

  editBtnSchema.update("content.region.details.button.edit", { roleOptions: { name: "Edit details" } });

  const editBtnUpdated = await editBtnSchema.getNestedLocator();
  await editBtnUpdated.click();

  /**
   * Calling profile.getLocatorSchema("content.region.details.button.edit") again 
   * will return a new deepCopy of the original LocatorSchema
   */
});

Deprecations

1. Deprecated .update and .updates Methods

  • Old .update(updates: Partial<LocatorSchemaWithoutPath>): LocatorSchemaWithMethods
  • Old .updates(indexedUpdates: { [index: number]: Partial<LocatorSchemaWithoutPath> | null }): LocatorSchemaWithMethods

Reason:
The .updates method relied on index-based updates, which are prone to errors and require manual maintenance, especially when LocatorSchemaPath strings are renamed or restructured. Additionally, the old .update method could only update the last LocatorSchema in the chain, making it less flexible.

Replacement:
Use the new .update(subPath, modifiedSchema) method, which leverages valid subPaths of LocatorSchemaPath strings for more intuitive and maintainable updates.

Removal Schedule:
These methods are deprecated and will be removed in version 2.0.0.

2. Deprecated Old getNestedLocator Method

  • Old getNestedLocator(indices?: { [key: number]: number | null }): Promise<Locator>

Reason:
Index-based indexing is less readable and requires manual updates when LocatorSchemaPath strings change.

Replacement:
Use the updated getNestedLocator(subPathIndices?: { [K in SubPaths<LocatorSchemaPathType, LocatorSubstring>]: number | null }): Promise<Locator> method, which utilizes LocatorSchemaPath strings for indexing.

Removal Schedule:
This method is deprecated and will be removed in version 2.0.0.

Migration Guide

Updating .update and .updates Methods

Old Usage:

const allCheckboxes = await poc
  .getLocatorSchema("main.products.searchControls.filterType.label.checkbox")
  .updates({ 3: { locatorOptions: { hasText: /Producer/i } } })
  .getNestedLocator();

New Usage:

const allCheckboxes = await poc
  .getLocatorSchema("main.products.searchControls.filterType.label.checkbox")
  .update("main.products.searchControls.filterType", { locatorOptions: { hasText: /Producer/i } })
  .getNestedLocator();

Updating getNestedLocator Method

Old Usage (Deprecated):

const saveBtn = await profile.getNestedLocator("content.region.details.button.save", { 4: 2 });

const editBtn = await profile.getLocatorSchema("content.region.details.button.edit")
  .getNestedLocator({ 2: index });

New Usage:

const saveBtn = await profile.getNestedLocator("content.region.details.button.save", {
  "content.region.details.button.save": 2 
});

const editBtn = await profile.getLocatorSchema("content.region.details.button.edit")
  .getNestedLocator({ "content.region.details": index });

Utilizing LocatorSchemaPath Instead of Indices

Transition from index-based to LocatorSchemaPath-based indexing to improve code readability and maintainability.

Old Example:

const allCheckboxes = await poc
  .getLocatorSchema("main.form.item.checkbox")
  .updates({ 3: { locatorOptions: { hasText: /Producer/i } } })
  .getNestedLocator();

New Example:

const allCheckboxes = await poc
  .getLocatorSchema("main.form.item.checkbox")
  .update("main.form.item", { locatorOptions: { hasText: /Producer/i } })
  .getNestedLocator();

Example in Context

Defining a LocatorSchema with filter and Using .addFilter()

// Defining LocatorSchemas
this.locators.addSchema("body.main.section@userInfo", {
  role: "region",
  roleOptions: { name: "Contact Info" },
  filter: { hasText: /e-mail/i },
  locatorMethod: GetByMethod.role
});

// Dynamically adding additional filters using `.addFilter()`
const specificSection = await poc
  .getLocatorSchema("body.main.section@userInfo")
  .addFilter("body.main.section@userInfo", { hasText: "Additional Services" })
  .getNestedLocator();

Updating LocatorSchemas with the New .update() Method

const editBtn = await profile
  .getLocatorSchema("content.region.details.button.edit")
  .update("content.region.details.button.edit", { 
    roleOptions: { name: "new accessibility name" }
  })
  .getNestedLocator();

Reusing LocatorSchemas with LocatorSchemaWithoutPath

import { GetByMethod, LocatorSchemaWithoutPath } from "pomwright";
import { missingInputError } from "@common/page-components/errors.locatorSchema.ts";

export type LocatorSchemaPath =
  | "body"
  | "body.main"
  | "body.main.section"
  | "body.main.section@products"
  | "body.main.section@userInfo"
  | "body.main.section@userInfo.input@email"
  | "body.main.section@userInfo.inputError"
  | "body.main.section@deliveryInfo";

export function initLocatorSchemas(locators: GetLocatorBase<LocatorSchemaPath>) {
  locators.addSchema("body", {
    locator: "body",
    locatorMethod: GetByMethod.locator,
  });

  locators.addSchema("body.main", {
    locator: "main",
    locatorMethod: GetByMethod.locator,
  });

  const region: LocatorSchemaWithoutPath = { role: "region", locatorMethod: GetByMethod.role };

  locators.addSchema("body.main.section", {
    ...region,
  });

  locators.addSchema("body.main.section@products", {
    ...region,
    roleOptions: { name: "Products" },
  });

  locators.addSchema("body.main.section@userInfo", {
    ...region,
    roleOptions: { name: "Contact Info" },
    filter: { hasText: /e-mail/i },
  });

  locators.addSchema("body.main.section@userInfo.input@email", {
    role: "textbox",
    roleOptions: { name: "Input your e-mail" },
    locatorMethod: GetByMethod.role,
  });

  locators.addSchema("body.main.section@userInfo.inputError", {
    ...missingInputError,
  });

  locators.addSchema("body.main.section@deliveryInfo", {
    ...region,
    roleOptions: { name: "Delivery Info" },
  });
}

Troubleshooting and Support

If you encounter any issues or have questions, please check our issues page or reach out to us directly.

Contributing

Pull Requests are welcome! Please open an issue or submit a pull request for any enhancements or bug fixes.

License

POMWright is open-source software licensed under the Apache-2.0 license.

changelog

pomwright

1.2.0

Minor Changes

  • #31 bc29062 Thanks @DyHex! - # LocatorSchema Enhancements: filter Property, .addFilter(), Updated .update(), and Enhanced getNestedLocator()

    Overview

    This release introduces several key enhancements to the POMWright. These improvements aim to provide greater flexibility, maintainability, and type safety when constructing and managing locators in your Page Object Models (POMs). The main updates include:

    • New filter Property: Extends filtering capabilities beyond the locator method.
    • New .addFilter() Method: Facilitates dynamic addition of filters to locators.
    • Updated .update() Method: Replaces deprecated .update and .updates methods with a more intuitive interface (none-breaking).
    • Enhanced getNestedLocator() Method: Shifts from index-based to subPath-based indexing for better readability and maintainability.
    • Export of LocatorSchemaWithoutPath: Enables partial or full reuse of locator schemas across different contexts.

    Changes

    1. New filter Property for LocatorSchema

    Purpose

    The existing locatorOptions property in LocatorSchema is specific to the locator method, limiting its applicability. The newly introduced filter property extends filtering capabilities to all locator types (e.g., role, label, placeholder, testid, id, etc.), excluding frameLocator.

    Behavior

    • Global Applicability: Unlike locatorOptions, the filter property can be applied to various locator methods, enhancing versatility.
    • Priority Application: When a filter is defined, it is always applied and will always be the first filter applied to the locator created from that LocatorSchema, the only exception is locatorOptions used with locator. This ensures consistent and prioritized filtering across different locator types.

    Usage Examples

    Before: Using locatorOptions (Limited to locator method)

    this.locators.addSchema("main.products.radio@junior", {
      locator: "radio",
      locatorOptions: { hasText: "some text" },
      locatorMethod: GetByMethod.locator,
    });

    After: Using filter (Applicable to Multiple Locator Methods)

    this.locators.addSchema("main.products.radio@junior", {
      locator: "input",
      filter: { hasText: "some text" },
      locatorMethod: GetByMethod.locator,
    });

    With role Locator Method:

    this.locators.addSchema("main.products.radio@junior", {
      role: "radio",
      filter: { hasText: "some text" },
      locatorMethod: GetByMethod.role,
    });

    Combined Example:

    this.locators.addSchema("main.subscription.form.item.section@header", {
      role: "region",
      roleOptions: { name: "Subscription Details" },
      filter: { hasText: "Mobile 2 GB" },
      locatorMethod: GetByMethod.role,
    });

    Benefits

    • Enhanced Flexibility: Apply default filters to various LocatorSchema.
    • Improved Locator Precision: Chain multiple filters to narrow down locators based on complex criteria.
    • Backward Compatibility: Existing locatorOptions works the same way as before, ensuring no disruption to current implementations.

    2. New .addFilter() Method for .getLocatorSchema()

    Purpose

    The .addFilter() method allows dynamic addition of filters to any part of the locator chain, enhancing flexibility in locator construction without modifying the original LocatorSchema at any point in a test.

    Method Signature

    .addFilter(
      subPath: SubPaths<LocatorSchemaPathType, LocatorSubstring>,
      filterData: FilterEntry
    ): LocatorSchemaWithMethods<LocatorSchemaPathType, LocatorSubstring>

    Parameters

    • subPath: A valid sub-path within the LocatorSchemaPath argument to .getLocatorSchema().
    • filterData: An object defining the filter criteria, which can include:
      • has?: Locator
      • hasNot?: Locator
      • hasText?: string | RegExp
      • hasNotText?: string | RegExp

    Usage Examples

    Adding Filters to Specific Sub-Paths:

    const allCheckboxesForBrandFilters = await poc
      .getLocatorSchema("main.products.searchControls.filterType.label.checkbox")
      .addFilter("main.products.searchControls.filterType", {
        hasText: /Producer/i,
      })
      .getNestedLocator();

    Chaining Multiple Filters:

    const refinedCheckboxes = await poc
      .getLocatorSchema("main.products.searchControls.filterType.label.checkbox")
      .addFilter("main.products.searchControls.filterType", {
        hasText: /Producer/i,
        hasNotText: /discontinued/i,
      })
      .addFilter("main.products.searchControls.filterType.label", {
        hasText: "Samsung",
      })
      .addFilter("main.products.searchControls.filterType.label", {
        has: poc.page.getByRole("checkbox"),
      })
      .getNestedLocator();

    Combining filter Property and .addFilter():

    this.locators.addSchema("main.subscription.form.item.section@header", {
      role: "region",
      roleOptions: { name: "Subscription Details" },
      filter: { hasText: "Mobile 2 GB" },
      locatorMethod: GetByMethod.role,
    });
    
    const specificSection = await poc
      .getLocatorSchema("main.subscription.form.item.section@header")
      .addFilter("main.subscription.form.item.section@header", {
        hasText: "Additional Services",
      })
      .getNestedLocator();

    Benefits

    • Dynamic Filtering: Add or modify filters on-the-fly without altering the original schema definitions.
    • Chainability: Easily chain multiple .addFilter() calls to build complex locator chains.
    • Error Handling: Attempts to add filters to invalid sub-paths will throw descriptive errors (compile & run-time), ensuring only valid paths are modified.

    3. Updated .update() Method

    Purpose

    The existing .update and .updates methods are deprecated and will be removed in version 2.0.0. .update could only update the last LocatorSchema in the chain and .updates relied on index-based updates, which posed maintainability challenges, especially when renaming LocatorSchemaPath strings. The new .update method leverages valid subPaths of LocatorSchemaPath instead of indices, enhancing readability and reducing manual maintenance.

    New Method Signature

    .update(
      subPath: SubPaths<LocatorSchemaPathType, LocatorSubstring>,
      modifiedSchema: Partial<LocatorSchemaWithoutPath>
    ): LocatorSchemaWithMethods<LocatorSchemaPathType, LocatorSubstring>

    Deprecation of Old Methods

    • Old .update(updates: Partial<LocatorSchemaWithoutPath>): LocatorSchemaWithMethods
    • Old .updates(indexedUpdates: { [index: number]: Partial<LocatorSchemaWithoutPath> | null }): LocatorSchemaWithMethods

    These methods are marked as deprecated and will be removed in version 2.0.0.

    Usage Examples

    Case A: Update the Last LocatorSchema in the Chain

    const editBtn = await profile
      .getLocatorSchema("content.region.details.button.edit")
      .update("content.region.details.button.edit", {
        roleOptions: { name: "new accessibility name" },
      })
      .getNestedLocator();

    Case B: Update a LocatorSchema Earlier in the Chain

    const editBtn = await profile
      .getLocatorSchema("content.region.details.button.edit")
      .update("content.region.details", {
        roleOptions: { name: "new accessibility name" },
      })
      .getNestedLocator();

    Case C: Update Multiple LocatorSchema in the Chain

    const editBtn = await profile
      .getLocatorSchema("content.region.details.button.edit")
      .update("content", { roleOptions: { name: "new accessibility name" } })
      .update("content.region", {
        roleOptions: { name: "new accessibility name" },
      })
      .update("content.region.details", {
        roleOptions: { name: "new accessibility name" },
      })
      .update("content.region.details.button", {
        roleOptions: { name: "new accessibility name" },
      })
      .update("content.region.details.button.edit", {
        roleOptions: { name: "new accessibility name" },
      })
      .getNestedLocator();

    Benefits

    • Enhanced Readability: Using LocatorSchemaPath strings makes the code more intuitive and easier to understand.
    • Reduced Maintenance: Eliminates the need to manage index-based references, especially when LocatorSchemaPath strings are renamed or restructured.
    • Type Safety: Leverages TypeScript's type system to ensure only valid subPath strings are used, enhancing developer experience with accurate Intellisense suggestions.

    4. Enhanced getNestedLocator() Method

    Purpose

    The getNestedLocator() method has been updated to utilize valid subPath's of LocatorSchemaPath instead of numeric indices when specifying .nth(n) occurrences within a locator chain. The old index-based method is deprecated and will be removed in version 2.0.0.

    New Method Signature

    getNestedLocator(
      subPathIndices?: { [K in SubPaths<LocatorSchemaPathType, LocatorSubstring>]: number | null }
    ): Promise<Locator>

    Deprecation of Old Method

    • Old getNestedLocator(indices?: { [key: number]: number | null }): Promise<Locator>

    This method is marked as deprecated and will be removed in version 2.0.0.

    Usage Examples

    Old Usage (Deprecated):

    const saveBtn = await profile.getNestedLocator(
      "content.region.details.button.save",
      { 4: 2 }
    );
    
    const editBtn = await profile
      .getLocatorSchema("content.region.details.button.edit")
      .getNestedLocator({ 2: index });

    New Usage:

    const saveBtn = await profile.getNestedLocator(
      "content.region.details.button.save",
      {
        "content.region.details.button.save": 2,
      }
    );
    
    const editBtn = await profile
      .getLocatorSchema("content.region.details.button.edit")
      .getNestedLocator({ "content.region.details": index });

    Benefits

    • Improved Readability: SubPath-based indexing is more intuitive and aligns with the hierarchical nature of locator paths.
    • Enhanced Maintainability: Reduces confusion and manual updates when LocatorSchemaPath strings are modified.
    • Type Safety and Intellisense: TypeScript provides accurate suggestions for valid subPath keys, enhancing the developer experience and reducing errors.

    5. Export of LocatorSchemaWithoutPath

    Purpose

    The LocatorSchemaWithoutPath type is now exported through index.ts, enabling full or partial reuse of LocatorSchema definitions across different addSchema calls within the same or different initLocatorSchemas functions.

    Usage Example

    import { GetByMethod, LocatorSchemaWithoutPath } from "pomwright";
    import { missingInputError } from "@common/page-components/errors.locatorSchema.ts";
    
    export type LocatorSchemaPath =
      | "body"
      | "body.main"
      | "body.main.section"
      | "body.main.section@products"
      | "body.main.section@userInfo"
      | "body.main.section@userInfo.input@email"
      | "body.main.section@userInfo.inputError"
      | "body.main.section@deliveryInfo";
    
    export function initLocatorSchemas(
      locators: GetLocatorBase<LocatorSchemaPath>
    ) {
      locators.addSchema("body", {
        locator: "body",
        locatorMethod: GetByMethod.locator,
      });
    
      locators.addSchema("body.main", {
        locator: "main",
        locatorMethod: GetByMethod.locator,
      });
    
      const region: LocatorSchemaWithoutPath = {
        role: "region",
        locatorMethod: GetByMethod.role,
      };
    
      locators.addSchema("body.main.section", {
        ...region,
      });
    
      locators.addSchema("body.main.section@products", {
        ...region,
        roleOptions: { name: "Products" },
      });
    
      locators.addSchema("body.main.section@userInfo", {
        ...region,
        roleOptions: { name: "Contact Info" },
        filter: { hasText: /e-mail/i },
      });
    
      locators.addSchema("body.main.section@userInfo.input@email", {
        role: "textbox",
        roleOptions: { name: "Input your e-mail" },
        locatorMethod: GetByMethod.role,
      });
    
      locators.addSchema("body.main.section@userInfo.inputError", {
        ...missingInputError,
      });
    
      locators.addSchema("body.main.section@deliveryInfo", {
        ...region,
        roleOptions: { name: "Delivery Info" },
      });
    }

    Benefits

    • Code Reusability: Facilitates the reuse of common locator schema fragments across different contexts, reducing redundancy.
    • Modular Definitions: Encourages a modular approach to defining locators, enhancing organization and scalability.

    Deprecations

    1. Deprecated .update and .updates Methods

    • Old .update(updates: Partial<LocatorSchemaWithoutPath>): LocatorSchemaWithMethods
    • Old .updates(indexedUpdates: { [index: number]: Partial<LocatorSchemaWithoutPath> | null }): LocatorSchemaWithMethods

    Reason: .updates relied on index-based updates, which are prone to errors and require manual maintenance, especially when LocatorSchemaPath strings are renamed or restructured. While the old .update method could only update the last LocatorSchema in the path. Confusing with multiple methods instead of just one.

    Replacement: Use the new .update(subPath, modifiedSchema) method, which leverages subpath's of valid LocatorSchemaPath strings for more intuitive and maintainable updates.

    Removal Schedule: These methods are deprecated and will be removed in version 2.0.0.

    2. Deprecated Old getNestedLocator Method

    • Old getNestedLocator(indices?: { [key: number]: number | null }): Promise<Locator>

    Reason: Index-based indexing is less readable and requires manual updates when LocatorSchemaPath strings change.

    Replacement: Use the updated getNestedLocator(subPathIndices?: { [K in SubPaths<LocatorSchemaPathType, LocatorSubstring>]: number | null }): Promise<Locator> method, which utilizes LocatorSchemaPath strings for indexing.

    Removal Schedule: This method is deprecated and will be removed in version 2.0.0.

    Migration Guide

    Updating .update and .updates Methods

    Old Usage:

    const allCheckboxes = await poc
      .getLocatorSchema("main.products.searchControls.filterType.label.checkbox")
      .updates({ 3: { locatorOptions: { hasText: /Producer/i } } })
      .getNestedLocator();

    New Usage:

    const allCheckboxes = await poc
      .getLocatorSchema("main.products.searchControls.filterType.label.checkbox")
      .update("main.products.searchControls.filterType", {
        locatorOptions: { hasText: /Producer/i },
      })
      .getNestedLocator();

    Updating getNestedLocator Method

    Old Usage (Deprecated):

    const saveBtn = await profile.getNestedLocator(
      "content.region.details.button.save",
      { 4: 2 }
    );
    
    const editBtn = await profile
      .getLocatorSchema("content.region.details.button.edit")
      .getNestedLocator({ 2: index });

    New Usage:

    const saveBtn = await profile.getNestedLocator(
      "content.region.details.button.save",
      {
        "content.region.details.button.save": 2,
      }
    );
    
    const editBtn = await profile
      .getLocatorSchema("content.region.details.button.edit")
      .getNestedLocator({ "content.region.details": index });

    Utilizing LocatorSchemaPath Instead of Indices

    Transition from index-based to LocatorSchemaPath-based indexing to improve code readability and maintainability.

    Old Example:

    const allCheckboxes = await poc
      .getLocatorSchema("main.form.item.checkbox")
      .updates({ 3: { locatorOptions: { hasText: /Producer/i } } })
      .getNestedLocator();

    New Example:

    const allCheckboxes = await poc
      .getLocatorSchema("main.form.item.checkbox")
      .update("main.form.item", { locatorOptions: { hasText: /Producer/i } })
      .getNestedLocator();

    Example in Context

    Defining a LocatorSchema with filter and Using .addFilter()

    // Defining LocatorSchemas
    this.locators.addSchema("body.main.section@userInfo", {
      role: "region",
      roleOptions: { name: "Contact Info" },
      filter: { hasText: /e-mail/i },
      locatorMethod: GetByMethod.role,
    });
    
    // Dynamically adding additional filters using `.addFilter()`
    const specificSection = await poc
      .getLocatorSchema("body.main.section@userInfo")
      .addFilter("body.main.section@userInfo", { hasText: "Additional Services" })
      .getNestedLocator();

    Updating LocatorSchemas with the New .update() Method

    const editBtn = await profile
      .getLocatorSchema("content.region.details.button.edit")
      .update("content.region.details.button.edit", {
        roleOptions: { name: "new accessibility name" },
      })
      .getNestedLocator();

    Reusing LocatorSchemas with LocatorSchemaWithoutPath

    import { GetByMethod, LocatorSchemaWithoutPath } from "pomwright";
    import { missingInputError } from "@common/page-components/errors.locatorSchema.ts";
    
    export type LocatorSchemaPath =
      | "body"
      | "body.main"
      | "body.main.section"
      | "body.main.section@products"
      | "body.main.section@userInfo"
      | "body.main.section@userInfo.input@email"
      | "body.main.section@userInfo.inputError"
      | "body.main.section@deliveryInfo";
    
    export function initLocatorSchemas(
      locators: GetLocatorBase<LocatorSchemaPath>
    ) {
      locators.addSchema("body", {
        locator: "body",
        locatorMethod: GetByMethod.locator,
      });
    
      locators.addSchema("body.main", {
        locator: "main",
        locatorMethod: GetByMethod.locator,
      });
    
      const region: LocatorSchemaWithoutPath = {
        role: "region",
        locatorMethod: GetByMethod.role,
      };
    
      locators.addSchema("body.main.section", {
        ...region,
      });
    
      locators.addSchema("body.main.section@products", {
        ...region,
        roleOptions: { name: "Products" },
      });
    
      locators.addSchema("body.main.section@userInfo", {
        ...region,
        roleOptions: { name: "Contact Info" },
        filter: { hasText: /e-mail/i },
      });
    
      locators.addSchema("body.main.section@userInfo.input@email", {
        role: "textbox",
        roleOptions: { name: "Input your e-mail" },
        locatorMethod: GetByMethod.role,
      });
    
      locators.addSchema("body.main.section@userInfo.inputError", {
        ...missingInputError,
      });
    
      locators.addSchema("body.main.section@deliveryInfo", {
        ...region,
        roleOptions: { name: "Delivery Info" },
      });
    }

    Notes

    • Filter Application Order: The filter property in LocatorSchema is always applied first, followed by any filters added via .addFilter(). This ensures that predefined filters have priority over dynamically added ones.

    • Exclusion of frameLocator: The filter property does not apply to frameLocator locators. If you need to filter within frames, use the appropriate Playwright frame methods.

    • Type Safety and Intellisense: The new .update() and enhanced getNestedLocator() methods leverage TypeScript's type system to provide accurate Intellisense suggestions, enhancing developer experience and reducing errors.

    Conclusion

    These enhancements to LocatorSchema and its associated methods significantly improve the flexibility, readability, and maintainability of locator management within your POMs. By adopting the new filter property, .addFilter() method, and updated .update() & getNestedLocator() methods, you can construct more precise and adaptable locators, facilitating robust and scalable test automation.

    Note: Ensure to migrate away from the deprecated .update, .updates, and old getNestedLocator methods before upgrading to version 2.0.0 to maintain compatibility and leverage the full benefits of these enhancements.

1.1.1

Patch Changes

  • #29 14b9bff Thanks @DyHex! - Exports ExtractFullUrlType, updates devDeps and adds additional keywords

1.1.0

Minor Changes

  • #27 ab778b2 Thanks @DyHex! - Added optional support for dynamic URL types (string and/or RegExp) for baseUrl and urlPath via an options-based approach.

    • WHAT: This update allows baseUrl and urlPath to be defined as either string or RegExp using BasePageOptions. The fullUrl is automatically inferred based on the types of baseUrl and urlPath. If either baseUrl or urlPath is a RegExp, fullUrl will also be a RegExp.

    • WHY: This change provides flexibility for handling dynamic URLs, such as those containing variables or patterns. It allows validating URLs that follow a predictable format but may have dynamic values that are not known before navigation.

    • HOW:

      • No changes are required for existing implementations since string remains the default type.
      • To use RegExp for baseUrl or urlPath, define them explicitly in the POC using BasePageOptions, which will automatically infer fullUrl.
      • Note: POCs using RegExp for baseUrl or urlPath cannot use page.goto(fullUrl) since it requires a string. Instead, methods like page.waitForURL(), which accept RegExp, can be used to validate navigation. This feature is ideal for scenarios where URL values are dynamic and can only be validated by format.

    Examples

    Example: Abstract Base Class extending BasePage without BasePageOptions (default, same as prior versions)

    import { type Page, type TestInfo } from "@playwright/test";
    import { BasePage, PlaywrightReportLogger } from "pomwright";
    // import helper methods / classes etc, here... (To be used in the Base POC)
    
    export default abstract class Base<
      LocatorSchemaPathType extends string
    > extends BasePage<LocatorSchemaPathType> {
      // add properties here (available to all POCs extending this abstract Base POC)
    
      constructor(
        page: Page,
        testInfo: TestInfo,
        urlPath: string,
        pocName: string,
        pwrl: PlaywrightReportLogger
      ) {
        super(page, testInfo, "http://localhost:8080", urlPath, pocName, pwrl);
    
        // initialize properties here (available to all POCs extending this abstract Base POC)
      }
    
      // add helper methods here (available to all POCs extending this abstract Base POC)
    }

    Example: POC extending abstract Base Class without BasePageOptions (default, same as prior versions)

    import { type Page, type TestInfo } from "@playwright/test";
    import { PlaywrightReportLogger } from "pomwright";
    import Base from "../base/base.page";
    import {
      type LocatorSchemaPath,
      initLocatorSchemas,
    } from "./testPage.locatorSchema";
    
    export default class TestPage extends Base<LocatorSchemaPath> {
      constructor(page: Page, testInfo: TestInfo, pwrl: PlaywrightReportLogger) {
        super(page, testInfo, "/", TestPage.name, pwrl);
      }
    
      protected initLocatorSchemas() {
        initLocatorSchemas(this.locators);
      }
    
      // add your helper methods here...
    }

    Example: Abstract Base Class extending BasePage with BasePageOptions, allows urlPath type as string xOR RegExp

    import { type Page, type TestInfo } from "@playwright/test";
    import {
      BasePage,
      type BasePageOptions,
      type ExtractUrlPathType,
      PlaywrightReportLogger,
    } from "pomwright";
    
    // BaseWithOptions extends BasePage and enforces baseUrlType as string
    export default abstract class BaseWithOptions<
      LocatorSchemaPathType extends string,
      Options extends BasePageOptions = {
        urlOptions: { baseUrlType: string; urlPathType: string };
      }
    > extends BasePage<
      LocatorSchemaPathType,
      {
        urlOptions: {
          baseUrlType: string;
          urlPathType: ExtractUrlPathType<Options>;
        };
      }
    > {
      constructor(
        page: Page,
        testInfo: TestInfo,
        urlPath: ExtractUrlPathType<{
          urlOptions: { urlPathType: ExtractUrlPathType<Options> };
        }>, // Ensure the correct type for urlPath
        pocName: string,
        pwrl: PlaywrightReportLogger
      ) {
        // Pass baseUrl as a string and let urlPath be flexible
        super(page, testInfo, "http://localhost:8080", urlPath, pocName, pwrl);
    
        // Initialize additional properties if needed
      }
    
      // Add any helper methods here, if needed
    }

    Example: POC extending abstract Base Class with BasePageOptions, but uses defaults (type string)

    import {
      type LocatorSchemaPath,
      initLocatorSchemas,
    } from "@page-object-models/testApp/without-options/pages/testPage.locatorSchema"; // same page & same locator schema as above
    import { type Page, type TestInfo } from "@playwright/test";
    import { PlaywrightReportLogger } from "pomwright";
    import BaseWithOptions from "../base/baseWithOptions.page";
    
    // Note, if BasePageOptions aren't specified, default options are used
    export default class TestPage extends BaseWithOptions<LocatorSchemaPath> {
      constructor(page: Page, testInfo: TestInfo, pwrl: PlaywrightReportLogger) {
        super(page, testInfo, "/", TestPage.name, pwrl);
      }
    
      protected initLocatorSchemas() {
        initLocatorSchemas(this.locators);
      }
    
      // add your helper methods here...
    }

    Example: POC extending abstract Base Class with BasePageOptions, and defines urlPath type as RegExp

    import { test } from "@fixtures/withOptions";
    import BaseWithOptions from "@page-object-models/testApp/with-options/base/baseWithOptions.page";
    import { type Page, type TestInfo, expect } from "@playwright/test";
    import { PlaywrightReportLogger } from "pomwright";
    import {
      type LocatorSchemaPath,
      initLocatorSchemas,
    } from "./color.locatorSchema";
    
    // By providing the urlOptions, the urlPath property now has RegExp type instead of string type (default) for this POC
    export default class Color extends BaseWithOptions<
      LocatorSchemaPath,
      { urlOptions: { urlPathType: RegExp } }
    > {
      constructor(page: Page, testInfo: TestInfo, pwrl: PlaywrightReportLogger) {
        /**
         * Matches "/testpath/randomcolor/" followed by a valid 3 or 6-character hex color code.
         */
        const urlPathRegex = /\/testpath\/([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
    
        super(page, testInfo, urlPathRegex, Color.name, pwrl);
      }
    
      protected initLocatorSchemas() {
        initLocatorSchemas(this.locators);
      }
    
      async expectThisPage() {
        await test.step("Expect color page", async () => {
          await this.page.waitForURL(this.fullUrl);
    
          const heading = await this.getNestedLocator("body.heading");
          await heading.waitFor({ state: "visible" });
        });
      }
    
      // add your helper methods here...
    }

    Summary of Changes

    • Dynamic URL Support: You can now define baseUrl and urlPath as RegExp for dynamic URL handling.
    • Backward Compatibility: No changes are required for existing implementations using string.
    • Restructuring of tests: Unit tests moved from ./src to ./test, integration tests moved from ./test to ./intTest
    • New unit tests: To cover optional-dynamic-url-types (string xOR RegExp)
    • New integration tests with Playwright/test: To cover optional-dynamic-url-types (string xOR RegExp)
    • peerDependencies change: Bumped minimum Playwright/test version from 1.39.0 to 1.41.0 as everything below 1.41.0 is deprecated. Also excluded v. 2.x.x as they have known bugs for chaining of locators. Limited max version to <2.0.0

    Changes have additionally been tested with two seperate playwright/test projects (130+ E2E tests) without issues, and against the latest Playwright/test versions.

1.0.2

Patch Changes

  • #25 993b0ad Thanks @DyHex! - # Change

    buildNestedLocator will no longer attempt to auto-scroll to the final nested locator

    Was done previously in an attempt to improve test recordings, but it sometimes causes tearing in screenshots and isn't ideal when using nested locators for visual regression tests.

    Playwright/test compatibility

    Tested with the following Playwright/test versions:

    • 1.43.1
    • 1.43.0
    • 1.42.1
    • 1.42.0 (not recommended)
    • 1.41.2
    • 1.41.1
    • 1.41.0
    • 1.40.1
    • 1.40.0
    • 1.39.0

1.0.1

Patch Changes

  • #23 0cfc19f Thanks @DyHex! - # Continuous testing & bug fixes

    Bug fixes

    • getLocatorBase.applyUpdate() now correctly updates the LocatorSchemaWithMethod and maintains a circular ref. for the entry representing itself in schemasMap
    • getLocatorBase.applyUpdates() now correctly updates the LocatorSchemaWithMethod and maintains a circular ref. for the entry representing itself in schemasMap while updating other LocatorSchema in SchemasMap directly.
    • getLocatorBase.deepMerge() now correctly validates valid nested properties of LocatorSchema

    Continuous testing

    • Build workflow now runs unit tests (vitest)
    • New shell script enabling testing new packages before release
    • New test workflow for POMWright integration tests (Playwright/test)
    • 52 new unit tests, more to come..
    • 4 new integration tests, more to come..

    New release has also been tested with a seperate Playwright/test project leveraging POMWright (~100 E2E tests)

    Playwright/test compatibility

    Tested with the following Playwright/test versions:

    • 1.43.1
    • 1.43.0
    • 1.42.1
    • 1.42.0 (not recommended)
    • 1.41.2
    • 1.41.1
    • 1.41.0
    • 1.40.1
    • 1.40.0
    • 1.39.0

1.0.0

Major Changes

  • 130784f: BREAKING: The following index.ts exports have been renamed:

    • "POMWright" changed to "BasePage"
    • "POMWrightTestFixture" changed to "test"
    • "POMWrightLogger" changed to "PlaywrightReportLogger"
    • "POMWrightGetLocatorBase" changed to "GetLocatorBase"
    • "POMWrightApi" changed to "BaseApi"

    Documentation updated

    README updated

0.0.9

Patch Changes

  • 7e8b7d1: removes an abstract method from POMWright/BasePage which should have been removed previously

0.0.8

Patch Changes

  • 0d4924e: adds additional inforamtion to package.json and shields in readme

0.0.7

Patch Changes

  • 5b7ed8a: Update README

0.0.6

Patch Changes

  • 8c2af7d: fix: ensure base fixture log follows playwright's api

0.0.5

Patch Changes

  • 0c96ab7: Add Biome for linting and formatting
  • 0c96ab7: Move indices with default value to the end of arguments for buildNestedLocator

0.0.4

Patch Changes

  • f2fea3b: add codeowners

0.0.3

Patch Changes

  • fa2a954: adds release script to package.json

0.0.2

Patch Changes

  • 1e5433a: Adds changeset to repository