An end-to-end testing project using Playwright with an Angular application. This project demonstrates modern testing techniques—combining the Page Object Model (POM), component-based architecture, Dependency Injection (DI), Functional Programming, and lazy initialization of page objects. The application under test is a modified and lightweight version of the Ngx-Admin Angular 14 application from Akveo.
- Project Structure
- Concepts and Design Decisions
- Requirements
- Installation
- Running the Application
- Running Tests
- Continuous Integration
- License
- Contact
playwright-playground/
├── .github/
│ └── workflows/
│ └── playwright.yml
├── .vscode/
│ └── settings.json
├── app/
│ ├── src/
│ ├── angular.json
│ ├── package.json
│ ├── LICENSE
│ └── README.md
├── docs/
│ └── e2e/
│ └── REQUIREMENTS.md
├── e2e/
│ ├── lib/
│ │ ├── components/
│ │ │ ├── base/
│ │ │ │ └── base.component.ts
│ │ │ ├── forms/
│ │ │ │ ├── base/
│ │ │ │ │ └── base.form.component.ts
│ │ │ │ ├── inline-form.component.ts
│ │ │ │ ├── grid-form.component.ts
│ │ │ │ └── ...
│ │ │ ├── navigation/
│ │ │ │ └── navigation.component.ts
│ │ │ ├── date-picker/
│ │ │ │ └── date-picker.component.ts
│ │ │ └── table/
│ │ │ └── ...
│ │ ├── factories/
│ │ │ └── users.ts
│ │ ├── fixtures/
│ │ │ ├── base-fixtures.ts
│ │ │ └── start-page.enum.ts
│ │ ├── utils/
│ │ │ ├── console-errors-tracker.ts
│ │ │ ├── config-helpers.ts
│ │ │ ├── logger.ts
│ │ │ ├── wait-util.ts
│ │ │ └── ...
│ │ └── ...
│ ├── pages/
│ │ ├── form-layouts.page.ts
│ │ ├── toastr.page.ts
│ │ └── ...
│ ├── tests/
│ │ ├── base/
│ │ ├── forms/
│ │ ├── toastr.test.ts
│ │ └── ...
│ ├── global-setup.ts
│ ├── global-teardown.ts
│ ├── playwright.config.ts
│ ├── LICENSE
│ └── README.md
├── .env.dev
├── .env.staging
├── package.json
├── tsconfig.json
└── README.md
This project adheres to industry-standard best practices to ensure high code quality, maintainability, and scalability:
- DRY (Don’t Repeat Yourself) Eliminates code duplication by abstracting reusable components and functions.
- SOLID Principles
- Single Responsibility Principle
- Open/Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
- Object-Oriented Programming (OOP) Promotes encapsulation and inheritance.
- Functional Programming (FP) Encourages pure functions and higher-order functions where appropriate.
- Modular Design & Separation of Concerns Breaks down large functionalities into more manageable, testable units.
- Clean Code Practices & Static Typing Readable code with enforced style rules (ESLint, Prettier) and TypeScript for safety.
Each page or major feature is encapsulated in a dedicated class, centralizing all selectors and interactions. This also reduces duplication across tests.
Example: form-layouts.page.ts
import { Page } from '@playwright/test';
import { InlineFormComponent } from '@components/forms/inline-form.component';
import { GridFormComponent } from '@components/forms/grid-form.component';
class FormLayoutsPage {
private inlineForm: InlineFormComponent;
private gridForm: GridFormComponent;
constructor(private page: Page) {
this.inlineForm = new InlineFormComponent(page);
this.gridForm = new GridFormComponent(page);
}
async submitInlineFormWithOptions(
name: string,
email: string,
rememberMe: boolean
) {
await this.inlineForm.fillName(name);
await this.inlineForm.fillEmail(email);
if (rememberMe) {
await this.inlineForm.checkRememberMe();
}
await this.inlineForm.submit();
}
async submitGridFormDetails(/*...*/) {
// Implementation using GridFormComponent
}
}
export { FormLayoutsPage };
Inspired by frontend frameworks like Angular, the test code is oriented around reusable components. Each component class interacts with specific UI sections that can appear on multiple pages.
Example: inline-form.component.ts
import { Page } from '@playwright/test';
export class InlineFormComponent {
constructor(private page: Page) {}
async fillName(name: string) {
await this.page.fill('input[name="name"]', name);
}
async fillEmail(email: string) {
await this.page.fill('input[name="email"]', email);
}
async checkRememberMe() {
await this.page.check('input[name="rememberMe"]');
}
async submit() {
await this.page.click('button[type="submit"]');
}
}
Dependencies are provided through constructors or optional injection, making components modular and easily testable. This approach also aligns with Angular’s DI model.
Example: toastr.page.ts
import { Page } from '@playwright/test';
import { BaseComponent } from '@components/base.component';
import { OptionListComponent } from '@components/list/option-list.component';
export class ToastrPage extends BaseComponent {
private _positionSelect?: OptionListComponent;
constructor(page: Page) {
super(page, 'ToastrPage');
}
private get positionSelect(): OptionListComponent {
if (!this._positionSelect) {
// Lazy instantiation
this._positionSelect = new OptionListComponent(
this.page,
'nb-select[[(selected)]="position"]'
);
}
return this._positionSelect;
}
async selectPosition(position: string) {
await this.positionSelect.selectOption(position);
}
}
Where beneficial, the project uses FP concepts such as higher-order functions and pure utility methods. For example, wait-util.ts uses a higher-order function signature for condition checking, and array operations often rely on immutable transformations like map
, filter
, and reduce
.
// e2e/lib/utils/wait-util.ts
export async function waitForCondition(
conditionFn: () => Promise<boolean>,
timeout = 5000,
interval = 100
): Promise<void> {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
if (await conditionFn()) return;
await new Promise((resolve) => setTimeout(resolve, interval));
}
throw new Error('Condition not met within timeout');
}
Instead of creating all sub-components in the constructor, the project employs lazy initialization. This defers the creation of each component until it’s first needed, leading to cleaner code and better performance if certain components aren’t used.
Example (continued from above toastr.page.ts):
private get positionSelect(): OptionListComponent {
if (!this._positionSelect) {
this._positionSelect = new OptionListComponent(this.page, 'nb-select[[(selected)]="position"]');
}
return this._positionSelect;
}
Shared functionality resides in reusable utility modules under utils. Examples include:
- wait-util.ts: Polls a condition function until it becomes
true
or times out. - config-helpers.ts: Dynamically manages Playwright configurations.
- console-errors-tracker.ts: Captures and logs browser console errors to help identify front-end issues.
- Uses logger.ts for structured logging, typically via
pino
. - Tracks console errors with console-errors-tracker.ts.
- Employs decorators (e.g.,
ActionLogger
,ErrorHandler
) around certain critical methods to capture crucial debugging information if something fails.
- playwright.config.ts centralizes all test-related configurations.
.env.*
files store environment-specific variables.- The project supports multiple environments (e.g., dev, staging), facilitating integration testing and deployment pipelines.
- TypeScript Strict Mode: Enhanced type checking to catch errors early.
- Linting & Formatting: ESLint and Prettier ensure consistent style.
- Path Aliases: Simplify imports (e.g.,
@components/...
,@pages/...
) for readability.
Refer to REQUIREMENTS.md for details on project goals, acceptance criteria, and overall expectations.
Key Requirements:
- Clear architectural design to accommodate new features.
- Demonstrations of advanced testing patterns (lazy initialization, DI, FP).
- Extensive test coverage on critical functionalities (e.g., form submissions, date pickers, toasts).
- Node.js v22 or higher
- npm
Clone the repository and install dependencies for both the root and Angular app:
git clone https://github.com/tryb3l/playwright-playground.git
cd playwright-playground
npm install
cd app
npm install
From the app folder:
npm start
The app listens on http://localhost:4200.
npm run test:e2e
This runs all Playwright tests found in tests.
npm run test:e2e:debug
Launches the Playwright Inspector, allowing interactive debugging (step-by-step execution and DOM inspection).
GitHub Actions automatically triggers Playwright tests on each push and pull request. Refer to playwright.yml for details:
- Branch-specific builds (e.g., dev, main)
- Environment-based test suites
- Failure alerts via GitHub
This project is licensed under the MIT License. Refer to the LICENSE file for details.
This test automation project is maintained by tryb3l. For any inquiries, suggestions, or potential collaboration, feel free to reach out via GitHub issues.