pompidou
v2.4.3
Published
CLI tool to auto-generate strongly-typed Page Object Models from Angular components for multiple test frameworks
Maintainers
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.tsfiles - 🎯 Element Mapping: Extracts elements with
data-testidattributes from HTML templates - 🔗 Typed Transitions: Parses
@pom-returnJSDoc 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, generatesgetX(index)andgetXContainingText(text)for row/item access - 🌍 Multi-Language: Generates POMs in TypeScript, Java, C#, or Python
- ☕ Java: For
java-playwrightandjava-selenium, emits apom.xmlso 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 --devQuick 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-languagesScan 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 --verboseThe 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-inputmeans a call site that leavessearchableat itsfalsedefault gets no-search-inputaccessor. 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?: stringand@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-dirat the directory holding the child (see below), or pass--allow-unresolved-testidsas 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, andgetOfferRowByIndex(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/libDirectories 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 languagesCustom Templates
You can provide custom Handlebars templates to customize the generated code:
pompidou ./src/app ./output --templates ./my-templatesTemplate directory structure:
my-templates/
├── typescript-playwright/
│ ├── page-object.hbs
│ └── index.hbs
├── java-selenium/
│ └── page-object.hbs
└── csharp-playwright/
└── page-object.hbsTemplate 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:
- Create a new directory in
src/templates/ - Add
page-object.hbstemplate - Add language config to
LANGUAGE_CONFIGSintypes.ts - Add tests
License
MIT License - see the LICENSE file for details.
