npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

pompidou

v2.4.3

Published

CLI tool to auto-generate strongly-typed Page Object Models from Angular components for multiple test frameworks

Readme

Pompidou

A powerful CLI tool and library that auto-generates strongly-typed Page Object Models (POMs) from Angular components for multiple languages and test frameworks.

Supported Languages

| Language | Framework | File Extension | |----------|-----------|----------------| | TypeScript | Playwright | .po.ts | | TypeScript | Cypress | .po.ts | | TypeScript | WebdriverIO | .po.ts | | Java | Selenium | .java | | Java | Playwright | .java | | C# | Playwright | .cs | | C# | Selenium | .cs | | Python | Playwright | .py | | Python | Selenium | .py |

Features

  • 🔍 Automatic Discovery: Scans your Angular project for *.component.ts files
  • 🎯 Element Mapping: Extracts elements with data-testid attributes from HTML templates
  • 🔗 Typed Transitions: Parses @pom-return JSDoc tags for typed navigation between POMs
  • 🧩 Nested Components: Fluent access to child components (e.g. page.getReservationList() returns a scoped POM)
  • 📋 List Items: For elements inside *ngFor, generates getX(index) and getXContainingText(text) for row/item access
  • 🌍 Multi-Language: Generates POMs in TypeScript, Java, C#, or Python
  • Java: For java-playwright and java-selenium, emits a pom.xml so the output is a compilable Maven project
  • 🛠️ Templating Engine: Uses Handlebars for customizable code generation
  • Fail-Fast: Strict validation with clear, actionable error messages
  • 📦 Publishable Package: Structured as a valid npm package

Installation

npm install pompidou --save-dev
# or
yarn add pompidou --dev

Quick Start

CLI Usage

# TypeScript + Playwright (default)
npx pompidou ./src/app ./e2e/page-objects

# Java + Selenium
npx pompidou ./src/app ./src/test/java/pages -l java-selenium -p com.myapp.pages

# C# + Playwright
npx pompidou ./src/app ./Tests/Pages -l csharp-playwright -p MyApp.Tests.Pages

# Python + Selenium
npx pompidou ./src/app ./tests/pages -l python-selenium

# List all supported languages
npx pompidou list-languages

Scan for missing locators

Run a static scan to verify that components are annotated correctly and that common display/input elements have the locator attribute (default data-testid):

npx pompidou scan ./src/app --locator data-testid --verbose

The scan prints a summary and per-component issues. Exit code is 0 when no issues are found, 2 when issues exist.

Programmatic Usage

import { generate, generateMultiLanguage } from 'pompidou';

// TypeScript + Playwright (default)
const summary = await generate({
  sourceDir: './src/app',
  outputDir: './e2e/page-objects',
  generateIndex: true,
});

// Java + Selenium
const javaSummary = await generateMultiLanguage({
  sourceDir: './src/app',
  outputDir: './src/test/java/pages',
  language: 'java-selenium',
  package: 'com.myapp.pages',
});

console.log(`Generated ${summary.successCount} POMs`);

How It Works

0. Testids a child stamps from an @Input

A shared component often renders its ids from an input rather than from literal attributes:

<!-- data-table.component.html -->
<div [attr.data-testid]="testId">
  @if (searchable) { <input [attr.data-testid]="testId + '-search-input'" /> }
  @for (row of rows; track row.id) {
    <tr [attr.data-testid]="testId + '-row-' + $index">
      <td [attr.data-testid]="testId + '-cell-' + $index + '-' + column.key"></td>
    </tr>
  }
</div>
<!-- guests.page.html -->
<ev-data-table testId="guests-table" [searchable]="true"></ev-data-table>

pompidou reads each component's own template, so without help it cannot see that the page renders guests-table-row-0. Those ids end up in no POM at all, and a page moving onto a shared component silently loses accessors that are still in the DOM.

Two passes fix this. Each component's vocabulary — what it stamps from each of its own testid-carrying inputs — is derived from its template; then each <child testId="…"> call site substitutes the bound value in and the resulting ids are attached to the parent's POM. Components are discovered, not enumerated, so a new shared component works with no configuration.

Three behaviours worth knowing:

  • Guards are respected. @if (searchable) gating -search-input means a call site that leaves searchable at its false default gets no -search-input accessor. Only literal evidence suppresses; an undecidable guard (isMobile()) still emits, flagged in the javadoc.

  • Input defaults decide unbound call sites. @Input() testId = 'data-table' renders when unbound; @Input() testId?: string and @Input() testId = '' render no attribute at all, so nothing is emitted for them.

  • Unresolvable bindings fail the build. If a bound child cannot be found, generation stops rather than quietly omitting its ids. Point --vocabulary-dir at the directory holding the child (see below), or pass --allow-unresolved-testids as a stopgap.

  • A dynamic binding keeps its variable. [testId]="'offer-row-' + row.id" is a family, not one id, so it generates the same three accessors an equivalent [attr.data-testid] binding does — getOfferRow(String rowId), a parameterless ^= prefix getter for use inside an already-scoped POM, and getOfferRowByIndex(int).

    The variable must come last. 'offer-' + row.id + '-menu', or a child that appends its own suffix to a dynamically-bound input, cannot be resolved to a sound prefix and is reported as an unresolved binding rather than guessed at.

Every derived accessor carries provenance in its javadoc — which child stamps it, from which input, via which expression — because editing a shared component now changes accessors in every POM that binds it.

Escape hatches

Both are environment variables, read at generation time. They exist so a generator change can be A/B'd from one build — comparing against a published release is not a valid control, because the release tree and the branch differ and that difference reads as phantom removals.

| Variable | Effect | |---|---| | POMPIDOU_NO_DERIVED_TESTIDS=1 | Disables @Input-derived testids entirely. | | POMPIDOU_NO_CALLSITE_TESTID_VARS=1 | Keeps derived ids but drops the call site's own variable, restoring the pre-2.3.0 exact-match accessor. |

--vocabulary-dir

A generation run is rooted at one source directory, but shared components need not live inside it:

pompidou ./projects/admin/src/app ./generated-admin-poms -l java-playwright \
  --vocabulary-dir ./projects/common/src/lib

Directories passed this way are scanned only to learn what their components stamp. No POMs are generated for them.

1. Element Mapping

Add data-testid attributes to your Angular templates:

<!-- login.component.html -->
<form>
  <input data-testid="username-input" type="text" />
  <input data-testid="password-input" type="password" />
  <button data-testid="submit-btn">Login</button>
</form>

2. Typed Transitions

Use the @pom-return JSDoc tag to define typed navigation:

// login.component.ts
@Component({
  selector: 'app-login',
  templateUrl: './login.component.html'
})
export class LoginComponent {
  /**
   * Performs login action
   * @pom-return {DashboardPO}
   */
  login(username: string, password: string): void {
    this.authService.login(username, password).subscribe(() => {
      this.router.navigate(['/dashboard']);
    });
  }
}

3. Nested Components and List Items

When a template uses another Angular component or repeats elements with *ngFor, the generator produces a fluent API so tests can navigate the structure without manual locator logic.

Nested components
If an element’s tag matches another component’s selector (e.g. <app-reservation-list data-testid="reservation-list"> and a component with selector: 'app-reservation-list'), the generator adds a getter that returns that component’s POM, scoped to that element. Child POMs receive an optional scope so all locators are relative to the host element.

Repeated elements
Elements that are inside *ngFor (on themselves or an ancestor) are treated as list items. For them the generator emits:

  • getX(index: number): Locator — the item at the given index (e.g. getReservationRow(0))
  • getXContainingText(text: string): Locator — the item that contains the given text (e.g. getReservationRowContainingText('Confirmed'))

Example: page with a list

<!-- reservation-page.component.html -->
<div data-testid="reservation-page">
  <app-reservation-list data-testid="reservation-list"></app-reservation-list>
</div>
<!-- reservation-list.component.html (selector: app-reservation-list) -->
<div *ngFor="let r of reservations" data-testid="reservation-row">
  <span data-testid="reservation-status">{{ r.status }}</span>
</div>

Generated usage (TypeScript + Playwright):

// Fluent access to nested component and list items
const list = reservationPage.getReservationList();
const firstRow = list.getReservationRow(0);
const confirmedRow = list.getReservationRowContainingText('Confirmed');

Nested POMs are constructed with (page, scope), so all locators inside the list are relative to the list root. The default generator (TypeScript + Playwright) supports nested components and repeated elements; other targets can be extended via the templating engine.

Generated Output

TypeScript + Playwright

import { Page, Locator } from '@playwright/test';
import { DashboardPO } from './dashboard.po';

export class LoginPO {
  protected readonly page: Page;
  protected readonly scope: Locator | undefined;

  constructor(page: Page, scope?: Locator) {
    this.page = page;
    this.scope = scope;
  }

  getUsernameInput(): Locator {
    return (this.scope ?? this.page).locator('[data-testid="username-input"]');
  }

  getPasswordInput(): Locator {
    return (this.scope ?? this.page).locator('[data-testid="password-input"]');
  }

  getSubmitBtn(): Locator {
    return (this.scope ?? this.page).locator('[data-testid="submit-btn"]');
  }

  async login(username: string, password: string): Promise<DashboardPO> {
    // TODO: Implement action logic
    return new DashboardPO(this.page);
  }
}

Root POMs are created with new LoginPO(page); nested component POMs are created with new ChildPO(page, parentLocator) so locators are scoped to the host element. For elements inside *ngFor, the generator also emits getX(index) and getXContainingText(text) (see Nested Components and List Items above)..

Java + Selenium

package com.myapp.pages;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;

public class LoginPO {
    protected final WebDriver driver;

    @FindBy(css = "[data-testid='username-input']")
    private WebElement usernameInput;

    @FindBy(css = "[data-testid='password-input']")
    private WebElement passwordInput;

    @FindBy(css = "[data-testid='submit-btn']")
    private WebElement submitBtn;

    public LoginPO(WebDriver driver) {
        this.driver = driver;
        PageFactory.initElements(driver, this);
    }

    public WebElement getUsernameInput() {
        return usernameInput;
    }

    public DashboardPO login(String username, String password) {
        // TODO: Implement action logic
        return new DashboardPO(driver);
    }
}

C# + Playwright

using Microsoft.Playwright;

namespace MyApp.Tests.Pages
{
    public class LoginPO
    {
        protected readonly IPage Page;

        public LoginPO(IPage page)
        {
            Page = page;
        }

        public ILocator GetUsernameInput() => Page.Locator("[data-testid='username-input']");

        public async Task<DashboardPO> LoginAsync(string username, string password)
        {
            // TODO: Implement action logic
            return new DashboardPO(Page);
        }
    }
}

CLI Options

Usage: pompidou [options] <source> <output>

Arguments:
  source                      Source directory containing Angular components
  output                      Output directory for generated POM files

Options:
  -V, --version               output the version number
  -l, --language <lang>       Target language and framework (default: "typescript-playwright")
  -p, --package <name>        Package/namespace for generated code
  -b, --base-class <name>     Base class name for POMs
  -s, --suffix <suffix>       Suffix for POM class names (default: "PO")
  -i, --index                 Generate index barrel file (default: false)
  -f, --force                 Overwrite existing files (default: false)
  --include <patterns...>     File patterns to include
  --exclude <patterns...>     File patterns to exclude
  --templates <dir>           Custom templates directory
  --strict                    Fail on unknown POM references (default: true)
  --no-strict                 Allow unknown POM references
  -v, --verbose               Enable verbose logging (default: false)
  -h, --help                  display help for command

Commands:
  list-languages              List all supported output languages

Custom Templates

You can provide custom Handlebars templates to customize the generated code:

pompidou ./src/app ./output --templates ./my-templates

Template directory structure:

my-templates/
├── typescript-playwright/
│   ├── page-object.hbs
│   └── index.hbs
├── java-selenium/
│   └── page-object.hbs
└── csharp-playwright/
    └── page-object.hbs

Template Data

Templates receive the following data:

interface TemplateData {
  className: string;           // POM class name
  sourceClassName: string;     // Angular component class name
  sourceFilePath: string;      // Source file path
  baseClass?: string;          // Base class to extend
  package?: string;            // Package/namespace
  imports: TemplateImport[];   // Import statements
  elements: TemplateElement[]; // Element getters
  actions: TemplateAction[];   // Action methods
  config: LanguageConfig;      // Language configuration
}

Available Helpers

  • Naming: pascalCase, camelCase, snakeCase, kebabCase, screamingSnakeCase
  • Conditionals: eq, neq, gt, gte, lt, lte, and, or, not
  • Arrays: first, last, length, isEmpty, isNotEmpty, join
  • Types: javaType, csharpType, pythonType
  • Documentation: javadoc, xmldoc, pydoc

Error Handling

The generator follows a fail-fast philosophy with clear, actionable error messages:

❌ Error during generation:

Validation failed with 2 error(s):
  1. [UNKNOWN_POM_REFERENCE] Method 'goToProfile' in 'login.component.ts' 
     references unknown POM 'ProfilePO'. Available POMs: LoginPO, DashboardPO.
  
  2. [INVALID_TEST_ID] Invalid data-testid '   ' in template 'login.component.html': 
     data-testid cannot be empty.

Error Types

| Error | Description | |-------|-------------| | SourceDirectoryNotFoundError | Source directory doesn't exist | | NoComponentsFoundError | No *.component.ts files found | | TemplateNotFoundError | Template file referenced but missing | | InvalidTestIdError | Invalid data-testid value | | InvalidPomReturnTagError | Malformed @pom-return tag | | UnknownPOMReferenceError | References non-existent POM class |

API Reference

generate(config, options?)

Main function for TypeScript + Playwright output.

generateMultiLanguage(config, options?)

Multi-language generation using templates.

getSupportedLanguages()

Returns list of supported language/framework combinations.

scanComponents(config)

Scans for Angular components without generating POMs.

TemplateEngine

Direct access to the templating engine for custom integrations.

Contributing

Contributions are welcome! To add a new language:

  1. Create a new directory in src/templates/
  2. Add page-object.hbs template
  3. Add language config to LANGUAGE_CONFIGS in types.ts
  4. Add tests

License

MIT License - see the LICENSE file for details.