snapshot of https://github.com/frappe/crm.git (develop, 2026-09-18; blobs > 131072B omitted)
Some checks failed
Frontend / Unit Tests & Coverage (push) Has been skipped
Regenerate POT file (translatable strings) / Regenerate POT file (develop) (push) Failing after 1m13s
Regenerate POT file (translatable strings) / Regenerate POT file (main-hotfix) (push) Failing after 6s
Server / Tests (frappe ${{ matrix.frappe-branch }}) (push) Has been skipped
Server / Resolve frappe matrix (push) Failing after 1s
Frontend / Lint (push) Failing after 1s
Frontend / Production Build (push) Failing after 1s
Server / Coverage Wrap Up (push) Has been skipped
Frontend / Coverage Wrap Up (push) Has been skipped
Some checks failed
Frontend / Unit Tests & Coverage (push) Has been skipped
Regenerate POT file (translatable strings) / Regenerate POT file (develop) (push) Failing after 1m13s
Regenerate POT file (translatable strings) / Regenerate POT file (main-hotfix) (push) Failing after 6s
Server / Tests (frappe ${{ matrix.frappe-branch }}) (push) Has been skipped
Server / Resolve frappe matrix (push) Failing after 1s
Frontend / Lint (push) Failing after 1s
Frontend / Production Build (push) Failing after 1s
Server / Coverage Wrap Up (push) Has been skipped
Frontend / Coverage Wrap Up (push) Has been skipped
This commit is contained in:
49
e2e/pages/deals.page.ts
Normal file
49
e2e/pages/deals.page.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Page, expect } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* Deals list view (/crm/deals) and the "Create Deal" modal.
|
||||
*/
|
||||
export class DealsPage {
|
||||
constructor(private page: Page) {}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto('/crm/deals')
|
||||
await this.page.waitForLoadState('networkidle')
|
||||
}
|
||||
|
||||
async openCreateModal() {
|
||||
await this.page.getByRole('button', { name: 'Create', exact: true }).click()
|
||||
await expect(
|
||||
this.page.getByRole('heading', { name: 'Create Deal' }),
|
||||
).toBeVisible()
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit the deal. "Primary email" identifies it later. Value and closure
|
||||
* date are only mandatory when a site customizes them (Property Setter), so
|
||||
* they are filled conditionally to stay portable across sites.
|
||||
*/
|
||||
async createDeal(email: string) {
|
||||
const dialog = this.page.getByRole('dialog')
|
||||
await dialog.getByPlaceholder('Primary email', { exact: true }).fill(email)
|
||||
await this.fillIfPresent(dialog, 'Expected Deal Value', '1000')
|
||||
await this.fillIfPresent(dialog, 'Expected Closure Date', '2026-12-31', true)
|
||||
|
||||
await dialog.getByRole('button', { name: 'Create', exact: true }).click()
|
||||
await expect(
|
||||
this.page.getByRole('heading', { name: 'Create Deal' }),
|
||||
).toBeHidden()
|
||||
}
|
||||
|
||||
private async fillIfPresent(
|
||||
scope: ReturnType<Page['getByRole']>,
|
||||
placeholder: string,
|
||||
value: string,
|
||||
isDate = false,
|
||||
) {
|
||||
const field = scope.getByPlaceholder(placeholder, { exact: true })
|
||||
if (!(await field.count())) return
|
||||
await field.fill(value)
|
||||
if (isDate) await field.press('Enter')
|
||||
}
|
||||
}
|
||||
4
e2e/pages/index.ts
Normal file
4
e2e/pages/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './login.page'
|
||||
export * from './leads.page'
|
||||
export * from './lead.page'
|
||||
export * from './deals.page'
|
||||
59
e2e/pages/lead.page.ts
Normal file
59
e2e/pages/lead.page.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Page, expect } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* Lead detail view (/crm/leads/:name): convert-to-deal and email compose.
|
||||
*/
|
||||
export class LeadPage {
|
||||
constructor(private page: Page) {}
|
||||
|
||||
async goto(name: string) {
|
||||
await this.page.goto(`/crm/leads/${name}`)
|
||||
await this.page.waitForLoadState('networkidle')
|
||||
}
|
||||
|
||||
/**
|
||||
* The email composer caches its draft (recipients/subject/body) in
|
||||
* localStorage. Clear it so the composer starts from this lead's own data,
|
||||
* keeping the flow deterministic across tests that share storage state.
|
||||
*/
|
||||
async clearComposerDraft() {
|
||||
await this.page.evaluate(() => window.localStorage.clear())
|
||||
await this.page.reload()
|
||||
await this.page.waitForLoadState('networkidle')
|
||||
}
|
||||
|
||||
/** Open the Convert-to-Deal modal and confirm; resolves once it closes. */
|
||||
async convertToDeal() {
|
||||
await this.page.getByRole('button', { name: 'Convert to Deal' }).click()
|
||||
const dialog = this.page.getByRole('dialog')
|
||||
await expect(
|
||||
dialog.getByRole('heading', { name: 'Convert to Deal' }),
|
||||
).toBeVisible()
|
||||
await dialog.getByRole('button', { name: 'Convert', exact: true }).click()
|
||||
// A successful conversion closes the modal; the created deal is then
|
||||
// asserted via the API (see convert.spec).
|
||||
await expect(
|
||||
dialog.getByRole('heading', { name: 'Convert to Deal' }),
|
||||
).toBeHidden()
|
||||
}
|
||||
|
||||
/** Toggle the email composer open. */
|
||||
async openEmailBox() {
|
||||
await this.page.getByRole('button', { name: 'Reply' }).click()
|
||||
}
|
||||
|
||||
/** Type a body into the composer and send. Subject is pre-filled by CRM. */
|
||||
async sendEmail(body: string) {
|
||||
const editor = this.page.locator('[contenteditable="true"]').last()
|
||||
await editor.click()
|
||||
await editor.fill(body)
|
||||
// The label carries a platform-dependent shortcut hint, e.g. "Send (⌘⏎)".
|
||||
// Anchored both ends so it can't match "Send an Email" / "Send Template".
|
||||
await this.page.getByRole('button', { name: /^Send(\s*\(.*\))?$/ }).click()
|
||||
}
|
||||
|
||||
/** Switch to a named tab in the activity area (Activity, Emails, ...). */
|
||||
async openTab(name: string) {
|
||||
await this.page.getByRole('tab', { name }).click()
|
||||
}
|
||||
}
|
||||
45
e2e/pages/leads.page.ts
Normal file
45
e2e/pages/leads.page.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Locator, Page, expect } from '@playwright/test'
|
||||
import { LeadData } from '../helpers/crm'
|
||||
|
||||
/**
|
||||
* Leads list view (/crm/leads) and the "Create Lead" modal.
|
||||
*
|
||||
* Fields render via FieldLayout; the quick-entry text fields carry the
|
||||
* label itself as their placeholder (e.g. "First Name", "Email"), so we
|
||||
* drive them by placeholder scoped to the dialog for stable selectors.
|
||||
*/
|
||||
export class LeadsPage {
|
||||
constructor(private page: Page) {}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto('/crm/leads')
|
||||
await this.page.waitForLoadState('networkidle')
|
||||
}
|
||||
|
||||
async openCreateModal() {
|
||||
await this.page.getByRole('button', { name: 'Create', exact: true }).click()
|
||||
await expect(
|
||||
this.page.getByRole('heading', { name: 'Create Lead' }),
|
||||
).toBeVisible()
|
||||
}
|
||||
|
||||
/** Fill the mandatory text fields and submit; resolves once the modal closes. */
|
||||
async createLead(data: LeadData) {
|
||||
const dialog = this.page.getByRole('dialog')
|
||||
await dialog.getByPlaceholder('First Name', { exact: true }).fill(data.first_name)
|
||||
if (data.last_name)
|
||||
await dialog.getByPlaceholder('Last Name', { exact: true }).fill(data.last_name)
|
||||
if (data.email)
|
||||
await dialog.getByPlaceholder('Email', { exact: true }).fill(data.email)
|
||||
|
||||
await dialog.getByRole('button', { name: 'Create', exact: true }).click()
|
||||
await expect(
|
||||
this.page.getByRole('heading', { name: 'Create Lead' }),
|
||||
).toBeHidden()
|
||||
}
|
||||
|
||||
/** A list row matching the given text (e.g. the lead's name/email). */
|
||||
row(text: string): Locator {
|
||||
return this.page.getByRole('link').filter({ hasText: text }).first()
|
||||
}
|
||||
}
|
||||
26
e2e/pages/login.page.ts
Normal file
26
e2e/pages/login.page.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Page, expect } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* Frappe's standard login page. After a successful login with
|
||||
* redirect-to=/crm the browser lands on the CRM SPA.
|
||||
*/
|
||||
export class LoginPage {
|
||||
constructor(private page: Page) {}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto('/login?redirect-to=/crm')
|
||||
await this.page.waitForLoadState('networkidle')
|
||||
}
|
||||
|
||||
async login(email = 'Administrator', password = 'admin') {
|
||||
await this.goto()
|
||||
await this.page.fill('#login_email', email)
|
||||
await this.page.fill('#login_password', password)
|
||||
await this.page.click('button.btn-login')
|
||||
await this.page.waitForURL(/\/crm/, { timeout: 30000 })
|
||||
}
|
||||
|
||||
async expectOnLoginPage() {
|
||||
await expect(this.page).toHaveURL(/.*login.*/)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user