feat: add Playwright setup and login flow test (#13578)

## Description

Adds Playwright E2E testing infrastructure with project configuration
and a login flow test. This is
Phase 1 of the Playwright E2E suite, kept minimal with only the core
setup and login component.

  Ref: Discussion #13500, PR #13067

## Type of change

Please delete options that are not relevant.

- [x] New feature (non-breaking change which adds functionality)


## How Has This Been Tested?

- Verified all imports resolve correctly
(`login-flow-ui-validation.spec.ts` only imports `Login` from
  `@components/ui`)
  - Ran login flow test locally against a running Chatwoot instance


## Checklist:

  - [x] My code follows the style guidelines of this project
  - [x] I have performed a self-review of my code
  - [x] I have made corresponding changes to the documentation
  - [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works

---------

Co-authored-by: Sony Mathew <sony@chatwoot.com>
Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
This commit is contained in:
Ajith KV
2026-04-28 18:21:05 +05:30
committed by GitHub
co-authored by Sony Mathew Sony Mathew
parent fed32d5964
commit 6aeda0ddf6
12 changed files with 1776 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
# Playwright Test Configuration
# Base URL for the Chatwoot instance under test
BASE_URL=http://localhost:3000
# Admin credentials
TEST_USER_EMAIL=admin@chatwoot.com
TEST_USER_PASSWORD="Password123@#"
# Add additional variables as needed by specific test suites
# VARIABLE_NAME=value
+54
View File
@@ -0,0 +1,54 @@
# Dependencies
node_modules/
package-lock.json
# Environment files
.env
.env.local
.env.*.local
.github/.env
# Playwright test results
test-results/
playwright-report/
blob-report/
# Playwright cache and auth
playwright/.cache/
playwright/.auth/
.cache/
.auth/
# Test artifacts
screenshots/
videos/
downloads/
traces/
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
.claude
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Generated schemas (if you want to version control them, remove this)
# response-schemas/
# OS files
Thumbs.db
.DS_Store
# Temporary files
*.tmp
*.temp
.tmp/
+146
View File
@@ -0,0 +1,146 @@
# Chatwoot E2E Testing - Documentation
Complete guide for writing and maintaining E2E tests for Chatwoot.
---
## Overview
End-to-end testing suite for Chatwoot built with Playwright and TypeScript using the Component Object Model (COM) pattern.
---
## Architecture
```
tests/playwright/
├── components/
│ ├── api/ # API interaction (auth.component.ts, inbox.component.ts ...)
│ └── ui/ # UI page objects (login.component.ts, agent-page.component.ts ...)
├── tests/e2e/ # Test specs (api/ and ui/)
├── utils/ # Shared utilities (fixture.ts, test-data.ts, db.ts)
├── response-schemas/ # API response schemas for validation
├── fixtures/ # Test fixtures
├── helpers/ # Helper functions
└── playwright.config.ts
```
---
## Configuration
All configuration managed through `.env` file. Copy `.env.example` to `.env`:
```
BASE_URL=http://localhost:3000
TEST_USER_EMAIL=admin@chatwoot.com
TEST_USER_PASSWORD="Password123@#"
ACCOUNT_ID=1
# Add additional variables as needed by specific test suites
# VARIABLE_NAME=value
```
> **Note:** `npx playwright install` is required after `pnpm install` to download browser binaries.
---
## Testing Patterns
### API Testing
```typescript
test('API operation', async ({ api }) => {
const authHeaders = await authComponent.login(email, password);
const result = await component.create(api, authHeaders, data);
expect(result.id).toBeTruthy();
});
```
### UI Testing
```typescript
test('UI interaction', async ({ page }) => {
const loginComponent = new Login(page);
await loginComponent.login(email, password);
await expect(page.getByText('Success')).toBeVisible();
});
```
### Hybrid Pattern
```typescript
test('UI with API setup', async ({ page, api }) => {
// Fast: Create test data via API
const inbox = await inboxComponent.createApiInbox(api, authHeaders, data);
// Test UI interactions
await page.goto(`/app/accounts/2/inbox/${inbox.id}`);
await expect(page.getByText(inbox.name)).toBeVisible();
});
```
---
## Request Handler
```typescript
const data = await api
.path('/api/v1/accounts/2/agents')
.headers(authHeaders)
.body({ name: 'John', email: 'john@test.com' })
.logs(true)
.postRequest(200);
```
**Methods:** `getRequest()`, `postRequest()`, `putRequest()`, `deleteRequest()`
---
## Test Data Generation
```typescript
import { fake } from '@utils/test-data';
const agent = fake.agent({ role: 'agent' });
const inboxName = fake.inboxName();
```
**Available:** `fake.fullName`, `fake.email`, `fake.phoneNumber`, `fake.password`, `fake.agent()`, `fake.inboxName()`
---
## Best Practices
**Do:**
- Use existing components
- Use `fake` for test data
- Use semantic selectors (`getByRole`, `getByLabel`)
- Clean up test data in `afterAll`
- Validate API schemas
**Don't:**
- Use CSS selectors
- Hardcode wait times
- Skip cleanup
- Commit sensitive data
---
## Troubleshooting
**Authentication errors:**
- Verify `.env` credentials match Chatwoot
- Check for rate limiting (429 errors)
**Database errors:**
- Verify database is running
- Check credentials in `.env`
**Timeout errors:**
- Ensure Chatwoot is running at `BASE_URL`
- Increase timeout: `{ timeout: 60000 }`
**Element not found:**
- Use `page.pause()` to inspect
- Check for timing issues
+59
View File
@@ -0,0 +1,59 @@
# Chatwoot E2E Testing
End-to-end testing framework for Chatwoot using Component Object Model pattern.
## Setup
```bash
# Install dependencies
pnpm install
# Install Playwright browsers
npx playwright install
# Configure environment
cp .env.example .env
```
Edit `.env` with your Chatwoot instance URL and credentials.
## Usage
```bash
# Run all tests
pnpm run playwright:run
# Run tests in UI mode
pnpm run playwright:open
# Lint tests and page objects
pnpm run lint
# Generate test code
pnpm run playwright:codegen
```
## Project Structure
```
tests/playwright/
├── components/
│ ├── api/ # API interaction components
│ └── ui/ # UI page objects
├── tests/
│ └── e2e/
│ ├── api/ # Pure API tests
│ └── ui/ # UI tests
├── utils/ # Shared utilities and helpers
├── response-schemas/ # API response schemas for validation
├── fixtures/ # Test fixtures
└── helpers/ # Helper functions
```
## Documentation
See [DOCS.md](./DOCS.md) for complete testing guide including patterns, conventions, and troubleshooting.
## Configuration
See `.env.example` for the full list of variables.
+1
View File
@@ -0,0 +1 @@
export { Login } from './login.component';
@@ -0,0 +1,55 @@
import { Page } from '@playwright/test';
export class Login {
private page: Page;
constructor(page: Page) {
this.page = page;
}
async navigate() {
await this.page.goto('/');
}
async fillEmail(email: string) {
await this.page.getByTestId('email_input').fill(email);
}
async fillPassword(password: string) {
await this.page.getByTestId('password_input').fill(password);
}
async clickLoginButton() {
await this.page.getByTestId('submit_button').click();
}
async login(email: string, password: string) {
await this.fillEmail(email);
await this.fillPassword(password);
await this.clickLoginButton();
}
getEmailInput() {
return this.page.getByTestId('email_input');
}
getPasswordInput() {
return this.page.getByTestId('password_input');
}
getLoginButton() {
return this.page.getByTestId('submit_button');
}
getLoginHeading() {
return this.page.getByRole('heading', { name: 'Login to Chatwoot' });
}
getSSOLink() {
return this.page.getByRole('link', { name: 'Login via SSO' });
}
getForgotPasswordLink() {
return this.page.getByRole('link', { name: 'Forgot your password?' });
}
}
+42
View File
@@ -0,0 +1,42 @@
import js from '@eslint/js';
import playwright from 'eslint-plugin-playwright';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{
ignores: [
'node_modules/',
'dist/',
'test-results/',
'playwright-report/',
'blob-report/',
'.cache/',
'.auth/',
],
},
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ['**/*.{js,mjs,ts}'],
languageOptions: {
globals: {
__dirname: 'readonly',
console: 'readonly',
process: 'readonly',
},
},
},
{
files: ['**/*.ts'],
languageOptions: {
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: import.meta.dirname,
},
},
},
{
...playwright.configs['flat/recommended'],
files: ['tests/**/*.ts'],
}
);
+35
View File
@@ -0,0 +1,35 @@
{
"name": "playwright",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"playwright:open": "npx playwright test --ui",
"playwright:run": "npx playwright test",
"playwright:codegen": "npx playwright codegen"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {
"@eslint/js": "^9.39.4",
"@faker-js/faker": "^9.9.0",
"@playwright/mcp": "^0.0.48",
"@playwright/test": "^1.56.1",
"@types/node": "^24.10.1",
"@types/uuid": "^10.0.0",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"dotenv": "^17.2.3",
"eslint": "^9.39.4",
"eslint-plugin-playwright": "^2.10.2",
"genson-js": "^0.0.8",
"pg": "^8.16.3",
"playwright": "^1.56.1",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.1",
"uuid": "^13.0.0"
}
}
+36
View File
@@ -0,0 +1,36 @@
import { defineConfig, devices } from '@playwright/test';
import dotenv from 'dotenv';
import path from 'path';
dotenv.config({ path: path.resolve(__dirname, '.env') });
export default defineConfig({
testDir: './tests',
timeout: 60 * 1000,
expect: {
timeout: 30 * 1000,
},
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
actionTimeout: 30 * 1000,
navigationTimeout: 30 * 1000,
baseURL: process.env.BASE_URL || 'http://localhost:3000',
trace: 'retain-on-failure',
headless: false,
viewport: {
width: 1440,
height: 1080,
},
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});
+1247
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,65 @@
import { test, expect } from '@playwright/test';
import { Login } from '@components/ui';
const TEST_EMAIL = process.env.TEST_USER_EMAIL || 'admin@chatwoot.com';
const TEST_PASSWORD = process.env.TEST_USER_PASSWORD || 'Password123@#';
const INVALID_PASSWORD = 'Password';
test.describe('Login page', () => {
let loginComponent: Login;
test.beforeEach(async ({ page }) => {
loginComponent = new Login(page);
await loginComponent.navigate();
});
test('should allow user to login with valid credentials', async ({
page,
}) => {
await loginComponent.login(TEST_EMAIL, TEST_PASSWORD);
await expect(page).toHaveURL(/\/app\/accounts\/\d+\/dashboard/);
});
test('renders all critical components', async ({ page }) => {
await expect(page).toHaveTitle('Chatwoot');
await expect(loginComponent.getLoginHeading()).toBeVisible();
const emailInput = loginComponent.getEmailInput();
await expect(emailInput).toBeVisible();
await expect(emailInput).toHaveAttribute('name', 'email_address');
await expect(page.getByText('Email')).toBeVisible();
const passwordInput = loginComponent.getPasswordInput();
await expect(passwordInput).toBeVisible();
await expect(passwordInput).toHaveAttribute('type', 'password');
await expect(page.getByText('PasswordForgot your password?')).toBeVisible();
const togglePasswordButton = page.getByRole('button', {
name: 'Show password',
});
await expect(togglePasswordButton).toBeVisible();
await expect(togglePasswordButton).toHaveAttribute('type', 'button');
await expect(togglePasswordButton.locator('.i-lucide-eye')).toBeVisible();
const forgotPasswordLink = loginComponent.getForgotPasswordLink();
await expect(forgotPasswordLink).toBeVisible();
await expect(forgotPasswordLink).toHaveAttribute(
'href',
'/app/auth/reset/password'
);
const loginButton = loginComponent.getLoginButton();
await expect(loginButton).toBeVisible();
await expect(loginButton).toHaveAttribute('type', 'submit');
});
test('should show error for invalid password', async ({ page }) => {
await loginComponent.login(TEST_EMAIL, INVALID_PASSWORD);
await expect(
page.getByText('Invalid login credentials. Please try again.')
).toBeVisible();
await expect(page).toHaveURL(/\/app\/login/);
});
});
+25
View File
@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"outDir": "./dist",
"rootDir": ".",
"baseUrl": ".",
"paths": {
"@components/*": ["components/*"],
"@utils/*": ["utils/*"],
"@fixtures/*": ["fixtures/*"],
"@helpers/*": ["helpers/*"],
"@tests/*": ["tests/*"]
},
"types": ["node", "@playwright/test"]
},
"include": ["**/*.ts"],
"exclude": ["node_modules"]
}