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

This commit is contained in:
upstream-mirror
2026-09-18 01:36:01 +05:00
commit db69de1c11
905 changed files with 102713 additions and 0 deletions

54
e2e/tests/auth.setup.ts Normal file
View File

@@ -0,0 +1,54 @@
import * as fs from 'fs'
import * as path from 'path'
import { expect, test as setup } from '@playwright/test'
const authFile = 'e2e/.auth/user.json'
const csrfFile = 'e2e/.auth/csrf.json'
/**
* Authentication setup - runs once before all tests.
* Logs in via the Frappe API, captures the CSRF token, and saves storage state.
*/
setup('authenticate', async ({ page }) => {
const authDir = path.dirname(authFile)
if (!fs.existsSync(authDir)) {
fs.mkdirSync(authDir, { recursive: true })
}
const loginResponse = await page.request.post('/api/method/login', {
form: {
usr: process.env.FRAPPE_USER || 'Administrator',
pwd: process.env.FRAPPE_PASSWORD || 'admin',
},
})
expect(loginResponse.ok()).toBeTruthy()
const userResponse = await page.request.get(
'/api/method/frappe.auth.get_logged_user',
)
expect(userResponse.ok()).toBeTruthy()
const userData = await userResponse.json()
expect(userData.message).not.toBe('Guest')
console.log(`Authenticated as: ${userData.message}`)
// Load the CRM app so window.frappe.csrf_token is available.
await page.goto('/crm')
await page.waitForLoadState('networkidle')
// The CRM SPA injects boot keys onto window (see crm/www/crm.html),
// so the token lives at window.csrf_token.
const csrfToken = await page.evaluate(() => {
const w = window as unknown as {
csrf_token?: string
frappe?: { csrf_token?: string }
}
return w.csrf_token || w.frappe?.csrf_token
})
// A missing token means later API writes would fail with an opaque 417;
// fail the setup loudly instead of proceeding with no CSRF header.
expect(csrfToken).toBeTruthy()
fs.writeFileSync(csrfFile, JSON.stringify({ csrf_token: csrfToken }))
await page.context().storageState({ path: authFile })
})

28
e2e/tests/convert.spec.ts Normal file
View File

@@ -0,0 +1,28 @@
import { test, expect } from '@playwright/test'
import { LeadPage } from '../pages'
import { cleanupE2ERecords, DEAL_DOCTYPE, getList, seedLead } from '../helpers'
test.describe('Lead to deal conversion', () => {
test.afterAll(async ({ request }) => {
await cleanupE2ERecords(request)
})
test('converts an existing lead into a deal', async ({ page, request }) => {
const lead = await seedLead(request)
const leadPage = new LeadPage(page)
await leadPage.goto(lead.name)
await leadPage.convertToDeal()
// A deal now carries the converted lead's email.
await expect
.poll(async () => {
const rows = await getList(request, DEAL_DOCTYPE, {
filters: { email: lead.email },
fields: ['name'],
})
return rows.length
})
.toBeGreaterThan(0)
})
})

33
e2e/tests/deal.spec.ts Normal file
View File

@@ -0,0 +1,33 @@
import { test, expect } from '@playwright/test'
import { DealsPage } from '../pages'
import {
cleanupE2ERecords,
DEAL_DOCTYPE,
getList,
uniqueSuffix,
} from '../helpers'
test.describe('Deal creation', () => {
test.afterAll(async ({ request }) => {
await cleanupE2ERecords(request)
})
test('creates a deal from the deals list view', async ({ page, request }) => {
const email = `e2e-deal-${uniqueSuffix()}@example.com`
const deals = new DealsPage(page)
await deals.goto()
await deals.openCreateModal()
await deals.createDeal(email)
await expect
.poll(async () => {
const rows = await getList(request, DEAL_DOCTYPE, {
filters: { email },
fields: ['name'],
})
return rows.length
})
.toBeGreaterThan(0)
})
})

44
e2e/tests/email.spec.ts Normal file
View File

@@ -0,0 +1,44 @@
import { test, expect } from '@playwright/test'
import { LeadPage } from '../pages'
import { cleanupE2ERecords, getList, seedLead } from '../helpers'
test.describe('Send email from lead view', () => {
test.afterAll(async ({ request }) => {
await cleanupE2ERecords(request)
})
test('composes and sends an email that is recorded as a Communication', async ({
page,
request,
}) => {
const lead = await seedLead(request)
const body = `E2E email body ${Date.now()}`
const leadPage = new LeadPage(page)
await leadPage.goto(lead.name)
await leadPage.clearComposerDraft()
await leadPage.openEmailBox()
await leadPage.sendEmail(body)
// The send creates a Communication linked to the lead (no SMTP delivery needed).
await expect
.poll(async () => {
const rows = await getList(request, 'Communication', {
filters: {
reference_doctype: 'CRM Lead',
reference_name: lead.name,
communication_type: 'Communication',
},
fields: ['name'],
})
return rows.length
})
.toBeGreaterThan(0)
// And it surfaces in the lead's Emails tab. With a cleared draft the
// recipient row reflects this lead's email and renders outside the
// sandboxed body iframe, so it is reliable to assert on.
await leadPage.openTab('Emails')
await expect(page.getByText(lead.email!).first()).toBeVisible()
})
})

29
e2e/tests/lead.spec.ts Normal file
View File

@@ -0,0 +1,29 @@
import { test, expect } from '@playwright/test'
import { LeadsPage } from '../pages'
import { buildLead, cleanupE2ERecords, getList, LEAD_DOCTYPE } from '../helpers'
test.describe('Lead creation', () => {
test.afterAll(async ({ request }) => {
await cleanupE2ERecords(request)
})
test('creates a lead from the leads list view', async ({ page, request }) => {
const lead = buildLead()
const leads = new LeadsPage(page)
await leads.goto()
await leads.openCreateModal()
await leads.createLead(lead)
// The new lead is persisted and queryable via the API.
await expect
.poll(async () => {
const rows = await getList(request, LEAD_DOCTYPE, {
filters: { email: lead.email },
fields: ['name'],
})
return rows.length
})
.toBeGreaterThan(0)
})
})

35
e2e/tests/login.spec.ts Normal file
View File

@@ -0,0 +1,35 @@
import { test, expect } from '@playwright/test'
import { LoginPage } from '../pages'
/**
* Exercises the real login flow through Frappe's login page into the CRM SPA.
* Runs without the shared auth state so it can assert the guest -> logged-in
* transition itself.
*/
test.describe('Login', () => {
test.use({ storageState: { cookies: [], origins: [] } })
test('logs in and lands on the CRM app', async ({ page }) => {
const login = new LoginPage(page)
await login.login(
process.env.FRAPPE_USER || 'Administrator',
process.env.FRAPPE_PASSWORD || 'admin',
)
await expect(page).toHaveURL(/\/crm/)
// The CRM shell renders its primary navigation once booted.
await expect(
page.getByRole('link', { name: 'Leads' }).first(),
).toBeVisible()
})
test('rejects invalid credentials', async ({ page }) => {
const login = new LoginPage(page)
await login.goto()
await page.fill('#login_email', 'Administrator')
await page.fill('#login_password', 'wrong-password')
await page.click('button.btn-login')
await login.expectOnLoginPage()
})
})