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:
54
e2e/tests/auth.setup.ts
Normal file
54
e2e/tests/auth.setup.ts
Normal 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
28
e2e/tests/convert.spec.ts
Normal 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
33
e2e/tests/deal.spec.ts
Normal 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
44
e2e/tests/email.spec.ts
Normal 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
29
e2e/tests/lead.spec.ts
Normal 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
35
e2e/tests/login.spec.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user