2026-07-23 22:35:03 +02:00
|
|
|
import { Page, expect } from '@playwright/test';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* E2E test helpers for LeoCRM.
|
|
|
|
|
*
|
|
|
|
|
* These helpers provide login automation, API mocking, and common
|
|
|
|
|
* fixtures used across all spec files. Since the container has no
|
|
|
|
|
* PostgreSQL/Redis, API calls are intercepted with mock responses.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
// ── Test credentials ──
|
|
|
|
|
export const TEST_USER = {
|
|
|
|
|
email: 'admin@leocrm.test',
|
|
|
|
|
password: 'TestPass123!',
|
|
|
|
|
firstName: 'Admin',
|
|
|
|
|
lastName: 'User',
|
|
|
|
|
id: 'user-001',
|
|
|
|
|
role: 'system_admin',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const TEST_TENANT = {
|
|
|
|
|
id: 'tenant-001',
|
|
|
|
|
name: 'Test Tenant',
|
|
|
|
|
slug: 'test-tenant',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ── Mock data ──
|
|
|
|
|
export const MOCK_CONTACTS = {
|
|
|
|
|
items: [
|
|
|
|
|
{
|
|
|
|
|
id: 'contact-001',
|
|
|
|
|
type: 'company' as const,
|
|
|
|
|
name: 'TechCorp GmbH',
|
|
|
|
|
displayname: 'TechCorp GmbH',
|
|
|
|
|
code: 'K-00123',
|
|
|
|
|
email_1: 'info@techcorp.test',
|
|
|
|
|
phone_1: '+49 30 12345678',
|
|
|
|
|
mailing_city: 'Berlin',
|
|
|
|
|
mailing_postalcode: '10115',
|
|
|
|
|
mailing_street: 'Hauptstrasse',
|
|
|
|
|
mailing_number: '1',
|
|
|
|
|
tags: 'kunde, premium',
|
|
|
|
|
contact_persons: [],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'contact-002',
|
|
|
|
|
type: 'person' as const,
|
|
|
|
|
firstname: 'Max',
|
|
|
|
|
surname: 'Mustermann',
|
|
|
|
|
displayname: 'Max Mustermann',
|
|
|
|
|
email_1: 'max@mustermann.test',
|
|
|
|
|
phone_1: '+49 170 9876543',
|
|
|
|
|
contact_persons: [],
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
total: 2,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const MOCK_MAIL_ACCOUNTS = [
|
|
|
|
|
{
|
|
|
|
|
id: 'acc-001',
|
|
|
|
|
email: 'test@leocrm.test',
|
|
|
|
|
display_name: 'Test Account',
|
|
|
|
|
imap_host: 'imap.test.test',
|
|
|
|
|
imap_port: 993,
|
|
|
|
|
smtp_host: 'smtp.test.test',
|
|
|
|
|
smtp_port: 587,
|
|
|
|
|
is_shared: false,
|
2026-08-07 22:03:11 +02:00
|
|
|
is_active: true,
|
2026-07-23 22:35:03 +02:00
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
export const MOCK_MAIL_FOLDERS = [
|
2026-08-07 22:03:11 +02:00
|
|
|
{ id: 'folder-001', name: 'INBOX', imap_name: 'INBOX', parent_id: null, unread_count: 3, total_count: 2, account_id: 'acc-001' },
|
|
|
|
|
{ id: 'folder-002', name: 'Sent', imap_name: 'Sent', parent_id: null, unread_count: 0, total_count: 0, account_id: 'acc-001' },
|
|
|
|
|
{ id: 'folder-003', name: 'Drafts', imap_name: 'Drafts', parent_id: null, unread_count: 0, total_count: 0, account_id: 'acc-001' },
|
2026-07-23 22:35:03 +02:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
export const MOCK_MAILS = [
|
|
|
|
|
{
|
|
|
|
|
id: 'mail-001',
|
2026-08-07 22:03:11 +02:00
|
|
|
folder_id: 'folder-001',
|
|
|
|
|
account_id: 'acc-001',
|
2026-07-23 22:35:03 +02:00
|
|
|
subject: 'Welcome to LeoCRM',
|
2026-08-07 22:03:11 +02:00
|
|
|
from_address: 'noreply@leocrm.test',
|
|
|
|
|
from_name: 'LeoCRM',
|
|
|
|
|
to_addresses: ['test@leocrm.test'],
|
|
|
|
|
cc_addresses: [],
|
|
|
|
|
bcc_addresses: [],
|
2026-07-23 22:35:03 +02:00
|
|
|
date: '2026-07-23T10:00:00Z',
|
|
|
|
|
body_text: 'Welcome to LeoCRM! Your account is ready.',
|
2026-08-07 22:03:11 +02:00
|
|
|
body_html: null,
|
|
|
|
|
sanitized_html: null,
|
|
|
|
|
is_seen: false,
|
|
|
|
|
is_flagged: false,
|
|
|
|
|
flag_type: null,
|
|
|
|
|
is_draft: false,
|
|
|
|
|
is_answered: false,
|
|
|
|
|
has_attachments: false,
|
2026-07-23 22:35:03 +02:00
|
|
|
attachments: [],
|
2026-08-07 22:03:11 +02:00
|
|
|
labels: [],
|
2026-07-23 22:35:03 +02:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'mail-002',
|
2026-08-07 22:03:11 +02:00
|
|
|
folder_id: 'folder-001',
|
|
|
|
|
account_id: 'acc-001',
|
2026-07-23 22:35:03 +02:00
|
|
|
subject: 'Meeting Tomorrow',
|
2026-08-07 22:03:11 +02:00
|
|
|
from_address: 'boss@leocrm.test',
|
|
|
|
|
from_name: 'Boss',
|
|
|
|
|
to_addresses: ['test@leocrm.test'],
|
|
|
|
|
cc_addresses: [],
|
|
|
|
|
bcc_addresses: [],
|
2026-07-23 22:35:03 +02:00
|
|
|
date: '2026-07-23T09:00:00Z',
|
|
|
|
|
body_text: 'Don\'t forget the meeting tomorrow at 10 AM.',
|
2026-08-07 22:03:11 +02:00
|
|
|
body_html: null,
|
|
|
|
|
sanitized_html: null,
|
|
|
|
|
is_seen: true,
|
|
|
|
|
is_flagged: false,
|
|
|
|
|
flag_type: null,
|
|
|
|
|
is_draft: false,
|
|
|
|
|
is_answered: false,
|
|
|
|
|
has_attachments: false,
|
2026-07-23 22:35:03 +02:00
|
|
|
attachments: [],
|
2026-08-07 22:03:11 +02:00
|
|
|
labels: [],
|
2026-07-23 22:35:03 +02:00
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
export const MOCK_DMS_FOLDERS = [
|
|
|
|
|
{ id: 'folder-001', name: 'Documents', parent_id: null },
|
|
|
|
|
{ id: 'folder-002', name: 'Images', parent_id: null },
|
|
|
|
|
{ id: 'folder-003', name: 'Contracts', parent_id: 'folder-001' },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
export const MOCK_DMS_FILES = [
|
|
|
|
|
{
|
|
|
|
|
id: 'file-001',
|
|
|
|
|
name: 'contract.pdf',
|
|
|
|
|
folder_id: 'folder-001',
|
|
|
|
|
size: 102400,
|
|
|
|
|
mime_type: 'application/pdf',
|
|
|
|
|
created_at: '2026-07-23T10:00:00Z',
|
|
|
|
|
updated_at: '2026-07-23T10:00:00Z',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'file-002',
|
|
|
|
|
name: 'photo.jpg',
|
|
|
|
|
folder_id: 'folder-002',
|
|
|
|
|
size: 204800,
|
|
|
|
|
mime_type: 'image/jpeg',
|
|
|
|
|
created_at: '2026-07-23T11:00:00Z',
|
|
|
|
|
updated_at: '2026-07-23T11:00:00Z',
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
export const MOCK_CALENDARS = [
|
|
|
|
|
{ id: 'cal-001', name: 'Personal', color: '#3b82f6' },
|
|
|
|
|
{ id: 'cal-002', name: 'Work', color: '#10b981' },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
export const MOCK_CALENDAR_ENTRIES = [
|
|
|
|
|
{
|
|
|
|
|
id: 'entry-001',
|
|
|
|
|
calendar_id: 'cal-001',
|
|
|
|
|
entry_type: 'appointment',
|
|
|
|
|
title: 'Team Meeting',
|
|
|
|
|
description: 'Weekly team sync',
|
|
|
|
|
start_at: '2026-07-24T09:00:00Z',
|
|
|
|
|
end_at: '2026-07-24T10:00:00Z',
|
|
|
|
|
all_day: false,
|
|
|
|
|
priority: 'medium',
|
|
|
|
|
status: 'open',
|
|
|
|
|
subtype: 'normal',
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
export const MOCK_PLUGINS = [
|
|
|
|
|
{
|
|
|
|
|
name: 'tags',
|
|
|
|
|
display_name: 'Tags',
|
|
|
|
|
description: 'Tag management plugin',
|
|
|
|
|
version: '1.0.0',
|
|
|
|
|
active: true,
|
|
|
|
|
installed: true,
|
|
|
|
|
status: 'active',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'entity_links',
|
|
|
|
|
display_name: 'Entity Links',
|
|
|
|
|
description: 'Link entities together',
|
|
|
|
|
version: '1.0.0',
|
|
|
|
|
active: false,
|
|
|
|
|
installed: true,
|
|
|
|
|
status: 'inactive',
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
export const MOCK_SEARCH_RESULTS = {
|
|
|
|
|
contact: [
|
|
|
|
|
{ id: 'contact-001', type: 'contact', title: 'TechCorp GmbH', snippet: 'Company in Berlin' },
|
|
|
|
|
],
|
|
|
|
|
company: [],
|
|
|
|
|
mail: [
|
|
|
|
|
{ id: 'mail-001', type: 'mail', title: 'Welcome to LeoCRM', snippet: 'Welcome email' },
|
|
|
|
|
],
|
|
|
|
|
file: [],
|
|
|
|
|
event: [],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ── API mock setup ──
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Intercept all API calls with mock responses.
|
|
|
|
|
* Call this in beforeEach to set up a fully mocked environment.
|
|
|
|
|
*/
|
|
|
|
|
export async function setupApiMocks(page: Page) {
|
2026-08-07 22:03:11 +02:00
|
|
|
await page.route('**/api/v1/**', (route) => {
|
|
|
|
|
const req = route.request();
|
|
|
|
|
const url = new URL(req.url());
|
|
|
|
|
const method = req.method();
|
|
|
|
|
const pathname = url.pathname;
|
|
|
|
|
|
|
|
|
|
// Auth endpoints
|
|
|
|
|
if (pathname === '/api/v1/auth/login') {
|
|
|
|
|
return route.fulfill({
|
|
|
|
|
status: 200,
|
|
|
|
|
contentType: 'application/json',
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
user: {
|
|
|
|
|
id: TEST_USER.id,
|
|
|
|
|
email: TEST_USER.email,
|
|
|
|
|
first_name: TEST_USER.firstName,
|
|
|
|
|
last_name: TEST_USER.lastName,
|
|
|
|
|
role: TEST_USER.role,
|
|
|
|
|
tenants: [TEST_TENANT],
|
|
|
|
|
permissions: ['*'],
|
|
|
|
|
is_system_admin: true,
|
|
|
|
|
},
|
|
|
|
|
csrf_token: 'mock-csrf-token',
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (pathname === '/api/v1/auth/me') {
|
|
|
|
|
return route.fulfill({
|
|
|
|
|
status: 200,
|
|
|
|
|
contentType: 'application/json',
|
|
|
|
|
body: JSON.stringify({
|
2026-07-23 22:35:03 +02:00
|
|
|
id: TEST_USER.id,
|
|
|
|
|
email: TEST_USER.email,
|
|
|
|
|
first_name: TEST_USER.firstName,
|
|
|
|
|
last_name: TEST_USER.lastName,
|
|
|
|
|
role: TEST_USER.role,
|
|
|
|
|
tenants: [TEST_TENANT],
|
|
|
|
|
permissions: ['*'],
|
|
|
|
|
is_system_admin: true,
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-07 22:03:11 +02:00
|
|
|
if (pathname === '/api/v1/auth/logout') {
|
|
|
|
|
return route.fulfill({ status: 200, contentType: 'application/json', body: '{}' });
|
2026-07-23 22:35:03 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-07 22:03:11 +02:00
|
|
|
// Contacts
|
|
|
|
|
if (pathname.startsWith('/api/v1/contacts')) {
|
|
|
|
|
if (pathname.includes('/contact_persons')) {
|
|
|
|
|
return route.fulfill({ status: 200, contentType: 'application/json', body: '[]' });
|
|
|
|
|
}
|
|
|
|
|
// Contact detail: /api/v1/contacts/<id>
|
|
|
|
|
const contactsDetailMatch = pathname.match(/^\/api\/v1\/contacts\/([^/]+)$/);
|
|
|
|
|
if (contactsDetailMatch && method === 'GET') {
|
|
|
|
|
return route.fulfill({
|
|
|
|
|
status: 200,
|
|
|
|
|
contentType: 'application/json',
|
|
|
|
|
body: JSON.stringify({ ...MOCK_CONTACTS.items[0], id: contactsDetailMatch[1], contact_persons: [] }),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if (method === 'GET') {
|
|
|
|
|
return route.fulfill({
|
|
|
|
|
status: 200,
|
|
|
|
|
contentType: 'application/json',
|
|
|
|
|
body: JSON.stringify(MOCK_CONTACTS),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if (method === 'POST') {
|
|
|
|
|
const body = req.postDataJSON() || {};
|
|
|
|
|
return route.fulfill({
|
|
|
|
|
status: 201,
|
|
|
|
|
contentType: 'application/json',
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
id: 'contact-new',
|
|
|
|
|
contact_persons: [],
|
|
|
|
|
displayname: body.name || `${body.firstname || ''} ${body.surname || ''}`.trim(),
|
|
|
|
|
...body,
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if (method === 'PUT') {
|
|
|
|
|
const body = req.postDataJSON() || {};
|
|
|
|
|
const id = pathname.split('/').pop() || 'contact-001';
|
|
|
|
|
return route.fulfill({
|
|
|
|
|
status: 200,
|
|
|
|
|
contentType: 'application/json',
|
|
|
|
|
body: JSON.stringify({ ...MOCK_CONTACTS.items[0], id, ...body }),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if (method === 'DELETE') {
|
|
|
|
|
return route.fulfill({ status: 204, body: '' });
|
|
|
|
|
}
|
|
|
|
|
return route.fulfill({ status: 200, contentType: 'application/json', body: '{}' });
|
2026-08-04 20:53:51 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-07 22:03:11 +02:00
|
|
|
// Mail
|
|
|
|
|
if (pathname === '/api/v1/mail/accounts') {
|
|
|
|
|
if (method === 'GET') {
|
|
|
|
|
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(MOCK_MAIL_ACCOUNTS) });
|
|
|
|
|
}
|
|
|
|
|
const body = req.postDataJSON() || {};
|
|
|
|
|
return route.fulfill({
|
2026-07-23 22:35:03 +02:00
|
|
|
status: 201,
|
|
|
|
|
contentType: 'application/json',
|
2026-08-07 22:03:11 +02:00
|
|
|
body: JSON.stringify({ id: 'acc-new', display_name: body.display_name || body.email, ...body }),
|
2026-07-23 22:35:03 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-07 22:03:11 +02:00
|
|
|
if (pathname === '/api/v1/mail/folders') {
|
|
|
|
|
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(MOCK_MAIL_FOLDERS) });
|
2026-08-04 20:53:51 +02:00
|
|
|
}
|
2026-07-23 22:35:03 +02:00
|
|
|
|
2026-08-07 22:03:11 +02:00
|
|
|
if (pathname === '/api/v1/mail') {
|
|
|
|
|
// fetchMails — /api/v1/mail?folder_id=...
|
|
|
|
|
return route.fulfill({
|
2026-07-23 22:35:03 +02:00
|
|
|
status: 200,
|
|
|
|
|
contentType: 'application/json',
|
2026-08-07 22:03:11 +02:00
|
|
|
body: JSON.stringify({ mails: MOCK_MAILS, total: MOCK_MAILS.length, page: 1, page_size: 25 }),
|
2026-07-23 22:35:03 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-07 22:03:11 +02:00
|
|
|
if (pathname === '/api/v1/mail/search') {
|
|
|
|
|
return route.fulfill({
|
2026-07-23 22:35:03 +02:00
|
|
|
status: 200,
|
|
|
|
|
contentType: 'application/json',
|
2026-08-07 22:03:11 +02:00
|
|
|
body: JSON.stringify({ mails: MOCK_MAILS, total: MOCK_MAILS.length, page: 1, page_size: 25 }),
|
2026-07-23 22:35:03 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-07 22:03:11 +02:00
|
|
|
if (pathname.startsWith('/api/v1/mail/signatures')) {
|
|
|
|
|
return route.fulfill({ status: 200, contentType: 'application/json', body: '[]' });
|
2026-07-23 22:35:03 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-07 22:03:11 +02:00
|
|
|
const singleMailMatch = pathname.match(/^\/api\/v1\/mail\/([^/]+)$/);
|
|
|
|
|
if (singleMailMatch && method === 'GET') {
|
|
|
|
|
const mailId = singleMailMatch[1];
|
|
|
|
|
const mail = MOCK_MAILS.find((m) => m.id === mailId) || MOCK_MAILS[0];
|
|
|
|
|
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mail) });
|
2026-07-23 22:35:03 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-07 22:03:11 +02:00
|
|
|
if (pathname.startsWith('/api/v1/mail/')) {
|
|
|
|
|
return route.fulfill({ status: 200, contentType: 'application/json', body: '{}' });
|
2026-07-23 22:35:03 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-07 22:03:11 +02:00
|
|
|
// DMS
|
|
|
|
|
if (pathname === '/api/v1/dms/folders') {
|
|
|
|
|
if (method === 'GET') {
|
|
|
|
|
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(MOCK_DMS_FOLDERS) });
|
|
|
|
|
}
|
|
|
|
|
const body = req.postDataJSON() || {};
|
|
|
|
|
return route.fulfill({
|
2026-07-23 22:35:03 +02:00
|
|
|
status: 201,
|
|
|
|
|
contentType: 'application/json',
|
2026-08-07 22:03:11 +02:00
|
|
|
body: JSON.stringify({ id: 'folder-new', name: body.name, parent_id: body.parent_id ?? null }),
|
2026-07-23 22:35:03 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-07 22:03:11 +02:00
|
|
|
if (pathname === '/api/v1/dms/files') {
|
|
|
|
|
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(MOCK_DMS_FILES) });
|
|
|
|
|
}
|
2026-07-23 22:35:03 +02:00
|
|
|
|
2026-08-07 22:03:11 +02:00
|
|
|
if (pathname.startsWith('/api/v1/dms/')) {
|
|
|
|
|
return route.fulfill({ status: 200, contentType: 'application/json', body: '[]' });
|
2026-08-04 20:53:51 +02:00
|
|
|
}
|
2026-08-07 22:03:11 +02:00
|
|
|
|
|
|
|
|
// Calendar
|
|
|
|
|
if (pathname === '/api/v1/calendars') {
|
|
|
|
|
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(MOCK_CALENDARS) });
|
2026-07-23 22:35:03 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-07 22:03:11 +02:00
|
|
|
if (pathname === '/api/v1/calendar/entries') {
|
|
|
|
|
if (method === 'GET') {
|
|
|
|
|
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(MOCK_CALENDAR_ENTRIES) });
|
|
|
|
|
}
|
|
|
|
|
const body = req.postDataJSON() || {};
|
|
|
|
|
return route.fulfill({ status: 201, contentType: 'application/json', body: JSON.stringify({ id: 'entry-new', ...body }) });
|
|
|
|
|
}
|
2026-07-23 22:35:03 +02:00
|
|
|
|
2026-08-07 22:03:11 +02:00
|
|
|
if (pathname === '/api/v1/calendar/kanban') {
|
|
|
|
|
return route.fulfill({
|
2026-08-04 20:53:51 +02:00
|
|
|
status: 200,
|
|
|
|
|
contentType: 'application/json',
|
2026-08-07 22:03:11 +02:00
|
|
|
body: JSON.stringify({
|
|
|
|
|
open: MOCK_CALENDAR_ENTRIES,
|
|
|
|
|
in_progress: [],
|
|
|
|
|
done: [],
|
|
|
|
|
cancelled: [],
|
|
|
|
|
}),
|
2026-08-04 20:53:51 +02:00
|
|
|
});
|
|
|
|
|
}
|
2026-07-23 22:35:03 +02:00
|
|
|
|
2026-08-07 22:03:11 +02:00
|
|
|
// Plugins
|
|
|
|
|
if (pathname === '/api/v1/plugins/active-manifests' || pathname === '/api/v1/plugins') {
|
|
|
|
|
if (method === 'GET') {
|
|
|
|
|
return route.fulfill({
|
|
|
|
|
status: 200,
|
|
|
|
|
contentType: 'application/json',
|
|
|
|
|
body: JSON.stringify({ plugins: MOCK_PLUGINS, total: MOCK_PLUGINS.length }),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return route.fulfill({ status: 200, contentType: 'application/json', body: '{}' });
|
|
|
|
|
}
|
2026-07-23 22:35:03 +02:00
|
|
|
|
2026-08-07 22:03:11 +02:00
|
|
|
const pluginToggleMatch = pathname.match(/^\/api\/v1\/plugins\/([^/]+)\/(activate|deactivate)$/);
|
|
|
|
|
if (pluginToggleMatch) {
|
|
|
|
|
const [, slug, action] = pluginToggleMatch;
|
|
|
|
|
const plugin = MOCK_PLUGINS.find((p) => p.name === slug);
|
|
|
|
|
if (plugin) {
|
|
|
|
|
plugin.active = action === 'activate';
|
|
|
|
|
plugin.status = action === 'activate' ? 'active' : 'inactive';
|
|
|
|
|
}
|
|
|
|
|
return route.fulfill({ status: 200, contentType: 'application/json', body: '{}' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Search
|
|
|
|
|
if (pathname === '/api/v1/search') {
|
|
|
|
|
if (method === 'POST') {
|
|
|
|
|
return route.fulfill({
|
|
|
|
|
status: 200,
|
|
|
|
|
contentType: 'application/json',
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
results: [
|
|
|
|
|
{ type: 'contact', id: 'contact-001', name: 'TechCorp GmbH', description: 'Company in Berlin', url: '/contacts/contact-001' },
|
|
|
|
|
{ type: 'mail', id: 'mail-001', name: 'Welcome to LeoCRM', description: 'Welcome email', url: '/mail/mail-001' },
|
|
|
|
|
],
|
|
|
|
|
facets: {},
|
|
|
|
|
summary: '2 results',
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ results: [], facets: {}, summary: '' }) });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// User preferences
|
|
|
|
|
if (pathname.startsWith('/api/v1/user/preferences')) {
|
|
|
|
|
return route.fulfill({ status: 200, contentType: 'application/json', body: '{}' });
|
2026-07-23 22:35:03 +02:00
|
|
|
}
|
2026-08-07 22:03:11 +02:00
|
|
|
|
|
|
|
|
// Fallback: empty array/object for any other API call
|
|
|
|
|
return route.fulfill({ status: 200, contentType: 'application/json', body: '[]' });
|
2026-07-23 22:35:03 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-07 22:03:11 +02:00
|
|
|
/**
|
|
|
|
|
* Perform login via the login form.}
|
|
|
|
|
|
2026-07-23 22:35:03 +02:00
|
|
|
/**
|
|
|
|
|
* Perform login via the login form.
|
|
|
|
|
* Assumes API mocks are already set up.
|
|
|
|
|
*/
|
|
|
|
|
export async function login(page: Page) {
|
2026-08-04 20:53:51 +02:00
|
|
|
// Set auth state in localStorage before navigating to the SPA.
|
|
|
|
|
// The authStore uses Zustand persist middleware with key 'auth-store'.
|
|
|
|
|
// This ensures auth state survives page navigation (page.goto reloads the JS context).
|
|
|
|
|
const authState = {
|
|
|
|
|
state: {
|
|
|
|
|
user: {
|
|
|
|
|
id: TEST_USER.id,
|
|
|
|
|
email: TEST_USER.email,
|
|
|
|
|
first_name: TEST_USER.firstName,
|
|
|
|
|
last_name: TEST_USER.lastName,
|
|
|
|
|
role: TEST_USER.role,
|
|
|
|
|
tenants: [TEST_TENANT],
|
|
|
|
|
permissions: ['*'],
|
|
|
|
|
is_system_admin: true,
|
|
|
|
|
avatar_url: null,
|
|
|
|
|
field_permissions: {},
|
|
|
|
|
},
|
|
|
|
|
currentTenant: TEST_TENANT,
|
|
|
|
|
isAuthenticated: true,
|
|
|
|
|
},
|
|
|
|
|
version: 0,
|
|
|
|
|
};
|
2026-07-23 22:35:03 +02:00
|
|
|
|
2026-08-04 20:53:51 +02:00
|
|
|
// Go to login page first (public route, always accessible)
|
|
|
|
|
await page.goto('/login');
|
|
|
|
|
await page.evaluate((state) => {
|
|
|
|
|
localStorage.setItem('auth-store', JSON.stringify(state));
|
|
|
|
|
// Dismiss welcome dialog by marking onboarding as completed
|
|
|
|
|
localStorage.setItem('leocrm_onboarding', JSON.stringify({ step: 0, completed: true, skipped: false }));
|
|
|
|
|
}, authState);
|
|
|
|
|
|
|
|
|
|
// Navigate to /start — Zustand persist will restore auth state from localStorage
|
|
|
|
|
await page.goto('/start');
|
|
|
|
|
await page.waitForLoadState('domcontentloaded');
|
|
|
|
|
await expect(page.locator('[data-testid="topbar"]')).toBeVisible({ timeout: 10_000 });
|
|
|
|
|
|
|
|
|
|
// Ensure welcome dialog is dismissed (in case it still appears)
|
|
|
|
|
const welcomeDialog = page.locator('[data-testid="welcome-dialog"]');
|
|
|
|
|
if (await welcomeDialog.isVisible({ timeout: 2_000 }).catch(() => false)) {
|
|
|
|
|
await welcomeDialog.locator('button').filter({ hasText: /überspringen|skip/i }).click().catch(() => {});
|
|
|
|
|
await page.waitForTimeout(500);
|
|
|
|
|
}
|
2026-07-23 22:35:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Perform logout via the user menu.
|
|
|
|
|
*/
|
|
|
|
|
export async function logout(page: Page) {
|
2026-08-04 20:53:51 +02:00
|
|
|
// Open user menu (use aria-label to distinguish from notification button)
|
|
|
|
|
await page.locator('[data-testid="topbar"] button[aria-label="Benutzermenü"]').click();
|
2026-07-23 22:35:03 +02:00
|
|
|
// Click logout button (text-based since no data-testid on logout button)
|
|
|
|
|
await page.locator('[role="menuitem"]').filter({ hasText: /logout|abmelden/i }).click();
|
2026-08-04 20:53:51 +02:00
|
|
|
await page.waitForURL('*/login', { timeout: 10_000 });
|
2026-07-23 22:35:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Navigate to a specific route after login.
|
|
|
|
|
*/
|
|
|
|
|
export async function navigateTo(page: Page, path: string) {
|
|
|
|
|
await page.goto(path);
|
|
|
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
|
}
|