128 lines
4.5 KiB
TypeScript
128 lines
4.5 KiB
TypeScript
|
|
/**
|
||
|
|
* N2 — WorkspaceManager integration: dynamic scope editor replaces the
|
||
|
|
* raw JSON textarea (Phase N). The module editor renders filter UI from
|
||
|
|
* /scope-definitions and saves scope values inside workspace_modules.config.
|
||
|
|
*/
|
||
|
|
import React from 'react';
|
||
|
|
import { describe, it, expect, vi } from 'vitest';
|
||
|
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||
|
|
import { WorkspaceManager } from '@/components/settings/WorkspaceManager';
|
||
|
|
|
||
|
|
const mockWorkspaces = {
|
||
|
|
items: [
|
||
|
|
{
|
||
|
|
id: 'ws1',
|
||
|
|
name: 'Vertrieb',
|
||
|
|
icon: 'LayoutGrid',
|
||
|
|
description: null,
|
||
|
|
is_default: false,
|
||
|
|
is_active: true,
|
||
|
|
created_by: null,
|
||
|
|
created_at: null,
|
||
|
|
updated_at: null,
|
||
|
|
user_count: 2,
|
||
|
|
modules: [
|
||
|
|
{
|
||
|
|
module_key: 'contacts',
|
||
|
|
is_visible: true,
|
||
|
|
menu_order: 0,
|
||
|
|
config: { contact_types: ['company'] },
|
||
|
|
},
|
||
|
|
],
|
||
|
|
},
|
||
|
|
],
|
||
|
|
total: 1,
|
||
|
|
};
|
||
|
|
|
||
|
|
const mockScopeDefinitions = {
|
||
|
|
modules: {
|
||
|
|
contacts: [
|
||
|
|
{
|
||
|
|
key: 'contact_types',
|
||
|
|
label: 'Kontakt-Typen',
|
||
|
|
control: 'multiselect',
|
||
|
|
options: [
|
||
|
|
{ value: 'company', label: 'Firmen' },
|
||
|
|
{ value: 'person', label: 'Personen' },
|
||
|
|
],
|
||
|
|
value_source: null,
|
||
|
|
default: null,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
// dashboard: module without scope dimensions
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
const setModulesMock = vi.fn().mockResolvedValue({});
|
||
|
|
|
||
|
|
vi.mock('@/api/hooks/workspaces', () => ({
|
||
|
|
useWorkspaces: () => ({ data: mockWorkspaces, isLoading: false, isError: false, error: null, refetch: vi.fn() }),
|
||
|
|
useCreateWorkspace: () => ({ mutateAsync: vi.fn().mockResolvedValue({ id: 'ws2' }), isPending: false }),
|
||
|
|
useUpdateWorkspace: () => ({ mutateAsync: vi.fn().mockResolvedValue({}), isPending: false }),
|
||
|
|
useDeleteWorkspace: () => ({ mutateAsync: vi.fn().mockResolvedValue({}), isPending: false }),
|
||
|
|
useSetWorkspaceModules: () => ({ mutateAsync: setModulesMock, isPending: false }),
|
||
|
|
useAssignWorkspaceUser: () => ({ mutateAsync: vi.fn(), isPending: false }),
|
||
|
|
useRemoveWorkspaceUser: () => ({ mutateAsync: vi.fn(), isPending: false }),
|
||
|
|
useWorkspaceScopeDefinitions: () => ({ data: mockScopeDefinitions, isLoading: false, isError: false }),
|
||
|
|
useScopeValues: () => ({ data: [], isLoading: false, isError: false }),
|
||
|
|
}));
|
||
|
|
|
||
|
|
vi.mock('@/store/pluginStore', () => ({
|
||
|
|
usePluginStore: (selector: (state: { manifests: unknown[] }) => unknown) =>
|
||
|
|
selector({
|
||
|
|
manifests: [
|
||
|
|
{ menu_items: [{ path: '/contacts', label: 'Kontakte', label_key: 'nav.contacts' }] },
|
||
|
|
],
|
||
|
|
}),
|
||
|
|
}));
|
||
|
|
|
||
|
|
function renderManager() {
|
||
|
|
return render(<WorkspaceManager />);
|
||
|
|
}
|
||
|
|
|
||
|
|
function openModuleEditor() {
|
||
|
|
fireEvent.click(screen.getByTitle('Module'));
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('WorkspaceManager N2 scope editor integration', () => {
|
||
|
|
it('replaces the raw JSON textarea with the dynamic scope editor', () => {
|
||
|
|
renderManager();
|
||
|
|
openModuleEditor();
|
||
|
|
// N1 scope editor fields are rendered ...
|
||
|
|
expect(screen.getByTestId('scope-field-contact_types')).toBeInTheDocument();
|
||
|
|
// ... and the legacy JSON textarea is gone.
|
||
|
|
expect(document.querySelector('textarea')).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('loads existing scope values from the stored module config', () => {
|
||
|
|
renderManager();
|
||
|
|
openModuleEditor();
|
||
|
|
const company = screen.getByTestId('scope-option-contact_types-company') as HTMLInputElement;
|
||
|
|
const person = screen.getByTestId('scope-option-contact_types-person') as HTMLInputElement;
|
||
|
|
expect(company.checked).toBe(true);
|
||
|
|
expect(person.checked).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('saves scope values inside module config on save', async () => {
|
||
|
|
renderManager();
|
||
|
|
openModuleEditor();
|
||
|
|
fireEvent.click(screen.getByTestId('scope-option-contact_types-person'));
|
||
|
|
fireEvent.click(screen.getByText('Speichern'));
|
||
|
|
|
||
|
|
await waitFor(() => expect(setModulesMock).toHaveBeenCalled());
|
||
|
|
const payload = setModulesMock.mock.calls[0][0];
|
||
|
|
expect(payload.workspaceId).toBe('ws1');
|
||
|
|
const contactsModule = payload.modules.find((m: any) => m.module_key === 'contacts');
|
||
|
|
expect(contactsModule.config).toEqual({ contact_types: ['company', 'person'] });
|
||
|
|
});
|
||
|
|
|
||
|
|
it('shows the no-dimensions hint for modules without scope definitions', () => {
|
||
|
|
renderManager();
|
||
|
|
openModuleEditor();
|
||
|
|
// dashboard starts hidden (workspace config only covers contacts) —
|
||
|
|
// make it visible first, then the scope editor renders its hint.
|
||
|
|
fireEvent.click(screen.getByLabelText('Dashboard'));
|
||
|
|
expect(screen.getByTestId('scope-no-dimensions-dashboard')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
});
|