/** * 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(); } 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(); }); });