Skip to content

Commit 4e9a667

Browse files
committed
fix: add CSRF token handling and fix login selectors
CSRF Token: - Add getCsrfToken() that fetches token from frappe.auth.get_csrf_token - Cache CSRF token per request context to avoid repeated calls - Include X-Frappe-CSRF-Token header in POST/PUT/DELETE requests Login Selectors: - Use exact Frappe login page IDs (#login_email, #login_password) - Use button.btn-login for submit button - Reference: frappe/frappe/www/login.html
1 parent 25a6325 commit 4e9a667

2 files changed

Lines changed: 62 additions & 15 deletions

File tree

e2e/helpers/frappe.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,33 @@ export interface FrappeResponse<T = unknown> {
1010
_server_messages?: string;
1111
}
1212

13+
// Cache CSRF token per request context
14+
const csrfTokenCache = new WeakMap<APIRequestContext, string>();
15+
16+
/**
17+
* Get CSRF token from Frappe.
18+
* Makes a request to get the token from cookies/response.
19+
*/
20+
async function getCsrfToken(request: APIRequestContext): Promise<string> {
21+
// Check cache first
22+
const cached = csrfTokenCache.get(request);
23+
if (cached) {
24+
return cached;
25+
}
26+
27+
// Get CSRF token from Frappe's session info endpoint
28+
const response = await request.get('/api/method/frappe.auth.get_csrf_token');
29+
30+
if (response.ok()) {
31+
const data = await response.json();
32+
const token = data.message || '';
33+
csrfTokenCache.set(request, token);
34+
return token;
35+
}
36+
37+
return '';
38+
}
39+
1340
/**
1441
* Create a new document via Frappe REST API.
1542
*/
@@ -18,8 +45,14 @@ export async function createDoc<T = Record<string, unknown>>(
1845
doctype: string,
1946
doc: Record<string, unknown>
2047
): Promise<T> {
48+
const csrfToken = await getCsrfToken(request);
49+
2150
const response = await request.post(`/api/resource/${doctype}`, {
2251
data: doc,
52+
headers: {
53+
'Content-Type': 'application/json',
54+
...(csrfToken ? { 'X-Frappe-CSRF-Token': csrfToken } : {}),
55+
},
2356
});
2457

2558
if (!response.ok()) {
@@ -61,9 +94,17 @@ export async function updateDoc<T = Record<string, unknown>>(
6194
name: string,
6295
updates: Record<string, unknown>
6396
): Promise<T> {
97+
const csrfToken = await getCsrfToken(request);
98+
6499
const response = await request.put(
65100
`/api/resource/${doctype}/${encodeURIComponent(name)}`,
66-
{ data: updates }
101+
{
102+
data: updates,
103+
headers: {
104+
'Content-Type': 'application/json',
105+
...(csrfToken ? { 'X-Frappe-CSRF-Token': csrfToken } : {}),
106+
},
107+
}
67108
);
68109

69110
if (!response.ok()) {
@@ -83,8 +124,15 @@ export async function deleteDoc(
83124
doctype: string,
84125
name: string
85126
): Promise<void> {
127+
const csrfToken = await getCsrfToken(request);
128+
86129
const response = await request.delete(
87-
`/api/resource/${doctype}/${encodeURIComponent(name)}`
130+
`/api/resource/${doctype}/${encodeURIComponent(name)}`,
131+
{
132+
headers: {
133+
...(csrfToken ? { 'X-Frappe-CSRF-Token': csrfToken } : {}),
134+
},
135+
}
88136
);
89137

90138
if (!response.ok()) {
@@ -101,8 +149,14 @@ export async function callMethod<T = unknown>(
101149
method: string,
102150
args: Record<string, unknown> = {}
103151
): Promise<T> {
152+
const csrfToken = await getCsrfToken(request);
153+
104154
const response = await request.post(`/api/method/${method}`, {
105155
data: args,
156+
headers: {
157+
'Content-Type': 'application/json',
158+
...(csrfToken ? { 'X-Frappe-CSRF-Token': csrfToken } : {}),
159+
},
106160
});
107161

108162
if (!response.ok()) {

e2e/pages/login.page.ts

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,12 @@ export class LoginPage {
1717

1818
constructor(page: Page) {
1919
this.page = page;
20-
// Frappe login page selectors - try multiple options for compatibility
21-
this.emailInput = page.locator(
22-
'input#login_email, input[autocomplete="username"], input[name="usr"]'
23-
);
24-
this.passwordInput = page.locator(
25-
'input#login_password, input[type="password"]'
26-
);
27-
this.submitButton = page.locator(
28-
'.btn-login, .btn-login-area button, button:has-text("Login")'
29-
);
30-
this.errorMessage = page.locator(
31-
'.alert-danger, .msgprint, .login-content .alert'
32-
);
20+
// Frappe login page selectors - use exact IDs from frappe/www/login.html
21+
// See: https://github.com/frappe/frappe/blob/develop/frappe/www/login.html
22+
this.emailInput = page.locator('#login_email');
23+
this.passwordInput = page.locator('#login_password');
24+
this.submitButton = page.locator('button.btn-login');
25+
this.errorMessage = page.locator('.msgprint, .alert-danger').first();
3326
}
3427

3528
/**

0 commit comments

Comments
 (0)