Skip to main content

Testing

frappe-js-client/testing builds a real FrappeClient (or extended client) backed by MemoryTransport. Application code typed (client: FrappeClient) => … can take the test client with no extra types.

import {
createTestClient,
createExtendedTestClient,
fixtureUser,
fixtureValidationErrorBody,
fixturePermissionErrorBody,
} from 'frappe-js-client/testing'

const { client, transport } = createTestClient()
transport.mock({
method: 'GET',
path: '/api/v2/document/User/Administrator',
body: { data: fixtureUser },
})
const user = await client.db.getDoc('User', 'Administrator')

const ext = createExtendedTestClient({ apiVersion: 1, frappeVersion: 15 })

Helpers

ExportReturns
createTestClient(options?){ client: FrappeClient, transport: MemoryTransport }
createExtendedTestClient(options?){ client: ExtendedFrappeClient, transport: MemoryTransport }
new MemoryTransport({ auth? })In-memory Transport you can pass as transport yourself

Both helpers call createFrappeClient({ url: 'https://test.local', transport, auth, ...options }). Default auth is anonymousAuth(). Pass apiVersion, frappeVersion, auth, headers, timeout, logger, middleware the same way you would in production.

When auth is supplied, MemoryTransport calls auth.onResponse with each mocked response's headers — the same hook FetchTransport uses (cookie jar tests work).

Mocking routes

transport.mock({
method: 'GET',
path: '/api/v2/document/ToDo',
body: { data: [], has_next_page: false },
once: true, // consumed after one hit
})

transport.mock({
method: 'GET',
path: /^\/api\/v2\/document\/User/,
match: (req) => req.params?.q === 'yes',
status: 200,
headers: { 'set-cookie': 'sid=abc; Path=/' },
body: { data: fixtureUser },
})

MemoryRoute:

FieldMeaning
methodHTTP method (compared case-insensitively)
pathExact path or RegExp against the path without the query string (URL-decoded)
statusDefault 200. Status >= 400 is mapped through mapServerError
headersBecome a Headers object; cookieAuth().onResponse sees them
bodyResponse body (already unwrapped later by the executor, so wrap { data } / { message } as Frappe would)
onceRemove the route after one match
matchExtra predicate on the TransportRequest

mock() returns this so you can chain.

Non-2xx mocked responses (status >= 400) go through mapServerError, so instanceof NotFoundError works. An aborted signal throws CancelledError. No matching route throws a plain Error describing the missing mock (not a FrappeError). Status 3xx is treated as success.

transport.requests is the list of TransportRequests issued (in order). transport.reset() clears routes and requests.

MemoryTransport does not simulate timeouts or network failures unless you omit the route (plain Error) or mock a 5xx body.

Browser tests

FRAPPE_TEST_URL=http://frappe14.localhost:8000 pnpm test:browser builds the package, then runs it in Chromium, Firefox, and WebKit. Cancellation and binary-error cases are local. Login, cookies, XHR upload progress, and authenticated downloads go to a separately managed Frappe site.

Use the site hostname in FRAPPE_TEST_URL when the bench is multi-tenant. The helper connects on loopback (so *.localhost works in Node) and sends that hostname as X-Frappe-Site-Name. Frappe 14 is detected automatically; set FRAPPE_TEST_API_VERSION / FRAPPE_TEST_FRAPPE_VERSION to override. If the URL is an IP address, set FRAPPE_TEST_SITE_NAME.

Fixtures

ExportShape
fixtureUserA FrappeDoc User (test.user@example.com)
fixtureValidationErrorBodyv1 ValidationError envelope with _server_messages
fixturePermissionErrorBodyv1 PermissionError envelope
transport.mock({
method: 'GET',
path: '/api/v2/document/ToDo/missing',
status: 404,
body: { errors: [{ type: 'DoesNotExistError', message: 'not found' }] },
})