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:
46
e2e/helpers/auth.ts
Normal file
46
e2e/helpers/auth.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { APIRequestContext, Page } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* Login via the Frappe API (faster than UI login).
|
||||
* Sets session cookies on the request context for subsequent API calls.
|
||||
*/
|
||||
export async function loginViaAPI(
|
||||
request: APIRequestContext,
|
||||
email = 'Administrator',
|
||||
password = 'admin',
|
||||
): Promise<void> {
|
||||
const response = await request.post('/api/method/login', {
|
||||
form: { usr: email, pwd: password },
|
||||
})
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new Error(
|
||||
`Login failed: ${response.status()} ${await response.text()}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout the current user.
|
||||
*/
|
||||
export async function logout(page: Page): Promise<void> {
|
||||
await page.goto('/api/method/logout')
|
||||
await page.waitForLoadState('networkidle')
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user is logged in by verifying the session.
|
||||
*/
|
||||
export async function isLoggedIn(request: APIRequestContext): Promise<boolean> {
|
||||
try {
|
||||
const response = await request.get(
|
||||
'/api/method/frappe.auth.get_logged_user',
|
||||
)
|
||||
if (!response.ok()) return false
|
||||
|
||||
const data = await response.json()
|
||||
return data.message && data.message !== 'Guest'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
68
e2e/helpers/crm.ts
Normal file
68
e2e/helpers/crm.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { APIRequestContext } from '@playwright/test'
|
||||
import { createDoc, deleteDoc, getList } from './frappe'
|
||||
|
||||
export const LEAD_DOCTYPE = 'CRM Lead'
|
||||
export const DEAL_DOCTYPE = 'CRM Deal'
|
||||
|
||||
export interface LeadData {
|
||||
first_name: string
|
||||
last_name?: string
|
||||
email?: string
|
||||
organization?: string
|
||||
mobile_no?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique suffix so parallel/repeat runs don't collide.
|
||||
*/
|
||||
export function uniqueSuffix(): string {
|
||||
return `${Date.now()}-${Math.floor(Math.random() * 1000)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a lead payload with unique, valid test data.
|
||||
*/
|
||||
export function buildLead(overrides: Partial<LeadData> = {}): LeadData {
|
||||
const id = uniqueSuffix()
|
||||
return {
|
||||
first_name: `E2E Lead ${id}`,
|
||||
email: `e2e-lead-${id}@example.com`,
|
||||
organization: `E2E Org ${id}`,
|
||||
mobile_no: '9999999999',
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed a lead via the API (used by tests that start from an existing lead).
|
||||
*/
|
||||
export async function seedLead(
|
||||
request: APIRequestContext,
|
||||
overrides: Partial<LeadData> = {},
|
||||
): Promise<{ name: string } & LeadData> {
|
||||
const data = buildLead(overrides)
|
||||
const doc = await createDoc<{ name: string }>(request, LEAD_DOCTYPE, data)
|
||||
return { ...data, name: doc.name }
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete every lead/deal created by E2E runs (matched by the e2e-* email).
|
||||
*/
|
||||
export async function cleanupE2ERecords(
|
||||
request: APIRequestContext,
|
||||
): Promise<void> {
|
||||
for (const doctype of [LEAD_DOCTYPE, DEAL_DOCTYPE]) {
|
||||
const rows = await getList<{ name: string }>(request, doctype, {
|
||||
fields: ['name'],
|
||||
filters: { email: ['like', 'e2e-%@example.com'] },
|
||||
limit: 500,
|
||||
})
|
||||
for (const row of rows) {
|
||||
try {
|
||||
await deleteDoc(request, doctype, row.name)
|
||||
} catch {
|
||||
// best-effort cleanup; ignore already-deleted / linked rows
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
165
e2e/helpers/frappe.ts
Normal file
165
e2e/helpers/frappe.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import * as fs from 'fs'
|
||||
import { APIRequestContext } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* Frappe API response wrapper.
|
||||
*/
|
||||
export interface FrappeResponse<T = unknown> {
|
||||
message?: T
|
||||
exc?: string
|
||||
exc_type?: string
|
||||
_server_messages?: string
|
||||
}
|
||||
|
||||
// CSRF token file saved by auth.setup.ts
|
||||
const CSRF_FILE = 'e2e/.auth/csrf.json'
|
||||
|
||||
let csrfTokenCache: string | null = null
|
||||
|
||||
/**
|
||||
* Read the CSRF token saved during auth setup (from window.frappe.csrf_token).
|
||||
*/
|
||||
function getCsrfToken(): string {
|
||||
if (csrfTokenCache !== null) {
|
||||
return csrfTokenCache
|
||||
}
|
||||
|
||||
try {
|
||||
if (fs.existsSync(CSRF_FILE)) {
|
||||
const data = JSON.parse(fs.readFileSync(CSRF_FILE, 'utf-8'))
|
||||
csrfTokenCache = data.csrf_token || ''
|
||||
return csrfTokenCache
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to read CSRF token file:', error)
|
||||
}
|
||||
|
||||
csrfTokenCache = ''
|
||||
return ''
|
||||
}
|
||||
|
||||
function jsonHeaders(): Record<string, string> {
|
||||
const csrfToken = getCsrfToken()
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
...(csrfToken ? { 'X-Frappe-CSRF-Token': csrfToken } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a document via the Frappe REST API.
|
||||
*/
|
||||
export async function createDoc<T = Record<string, unknown>>(
|
||||
request: APIRequestContext,
|
||||
doctype: string,
|
||||
doc: Record<string, unknown>,
|
||||
): Promise<T> {
|
||||
const response = await request.post(`/api/resource/${doctype}`, {
|
||||
data: doc,
|
||||
headers: jsonHeaders(),
|
||||
})
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new Error(`Failed to create ${doctype}: ${await response.text()}`)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
return result.data as T
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a document by name via the Frappe REST API.
|
||||
*/
|
||||
export async function getDoc<T = Record<string, unknown>>(
|
||||
request: APIRequestContext,
|
||||
doctype: string,
|
||||
name: string,
|
||||
): Promise<T> {
|
||||
const response = await request.get(
|
||||
`/api/resource/${doctype}/${encodeURIComponent(name)}`,
|
||||
)
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new Error(
|
||||
`Failed to get ${doctype}/${name}: ${await response.text()}`,
|
||||
)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
return result.data as T
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a document via the Frappe REST API.
|
||||
*/
|
||||
export async function deleteDoc(
|
||||
request: APIRequestContext,
|
||||
doctype: string,
|
||||
name: string,
|
||||
): Promise<void> {
|
||||
const response = await request.delete(
|
||||
`/api/resource/${doctype}/${encodeURIComponent(name)}`,
|
||||
{ headers: jsonHeaders() },
|
||||
)
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new Error(
|
||||
`Failed to delete ${doctype}/${name}: ${await response.text()}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a Frappe whitelisted method.
|
||||
*/
|
||||
export async function callMethod<T = unknown>(
|
||||
request: APIRequestContext,
|
||||
method: string,
|
||||
args: Record<string, unknown> = {},
|
||||
): Promise<T> {
|
||||
const response = await request.post(`/api/method/${method}`, {
|
||||
data: args,
|
||||
headers: jsonHeaders(),
|
||||
})
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new Error(`Failed to call ${method}: ${await response.text()}`)
|
||||
}
|
||||
|
||||
const result: FrappeResponse<T> = await response.json()
|
||||
return result.message as T
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of documents via the Frappe REST API.
|
||||
*/
|
||||
export async function getList<T = Record<string, unknown>>(
|
||||
request: APIRequestContext,
|
||||
doctype: string,
|
||||
options: {
|
||||
fields?: string[]
|
||||
filters?: Record<string, unknown>
|
||||
limit?: number
|
||||
orderBy?: string
|
||||
} = {},
|
||||
): Promise<T[]> {
|
||||
const params = new URLSearchParams()
|
||||
|
||||
if (options.fields) params.set('fields', JSON.stringify(options.fields))
|
||||
if (options.filters) params.set('filters', JSON.stringify(options.filters))
|
||||
if (options.limit) params.set('limit_page_length', options.limit.toString())
|
||||
if (options.orderBy) params.set('order_by', options.orderBy)
|
||||
|
||||
const response = await request.get(
|
||||
`/api/resource/${doctype}?${params.toString()}`,
|
||||
)
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new Error(
|
||||
`Failed to get list of ${doctype}: ${await response.text()}`,
|
||||
)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
return result.data as T[]
|
||||
}
|
||||
3
e2e/helpers/index.ts
Normal file
3
e2e/helpers/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './auth'
|
||||
export * from './frappe'
|
||||
export * from './crm'
|
||||
Reference in New Issue
Block a user