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:
co-authored by
Sony Mathew
Sony Mathew
parent
fed32d5964
commit
6aeda0ddf6
@@ -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
|
||||
@@ -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/
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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?' });
|
||||
}
|
||||
}
|
||||
@@ -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'],
|
||||
}
|
||||
);
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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'] },
|
||||
},
|
||||
],
|
||||
});
|
||||
Generated
+1247
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/);
|
||||
});
|
||||
});
|
||||
@@ -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"]
|
||||
}
|
||||
Reference in New Issue
Block a user