// @vitest-environment jsdom import { act, createElement } from 'react'; import { createRoot } from 'react-dom/client'; import { renderToStaticMarkup } from 'react-dom/server'; import { describe, expect, it, vi } from 'vitest'; import { FloatingResultsWindow } from '../../src/features/linked-results/FloatingResultsWindow'; import { makeSheet } from '../../src/importers/sheet-factory'; import { resultClusterParkingPosition, resultClusterCoverHeight, detachResultWindow, minimizeResultWindow, prepareForNewClusterWindow, restoreResultWindow, toggleResultWindowMinimized, type ClusterAwareResultWindow } from '../../src/features/linked-results/result-window-layout'; function windowState(recordId: string, minimized: boolean, detached = false): ClusterAwareResultWindow { return { recordId, minimized, detached }; } describe('Result-Fenster-Cluster', () => { it('parkt minimierte Results oben rechts wie in der Referenzansicht', () => { expect(resultClusterParkingPosition(1848, 760, 640, 142)).toEqual({ left: 1008, top: 188 }); expect(resultClusterParkingPosition(440, 428, 42, 142)).toEqual({ left: 0, top: 42 }); }); it('berechnet die Mindesthoehe des aktiven Fensters bis unter den letzten minimierten Cluster-Eintrag', () => { expect(resultClusterCoverHeight(1, 600)).toBe(0); expect(resultClusterCoverHeight(7, 600)).toBe(204); expect(resultClusterCoverHeight(20, 588)).toBe(588); }); it('laesst ein herausgezogenes maximiertes Result beim Oeffnen eines weiteren Results unangetastet', () => { const windows = [ windowState('detached', false, true), windowState('cluster-active', false), windowState('cluster-minimized', true) ]; expect(prepareForNewClusterWindow(windows)).toEqual([ windowState('detached', false, true), windowState('cluster-active', true), windowState('cluster-minimized', true) ]); }); it('kann mehrere herausgezogene Results gleichzeitig maximiert halten', () => { const windows = [ windowState('first', false, true), windowState('second', true), windowState('third', false, true) ]; expect(restoreResultWindow(windows, 'second')).toEqual([ windowState('first', false, true), windowState('second', false), windowState('third', false, true) ]); }); it('clustert ein herausgezogenes Result erst beim Minimieren wieder ein', () => { const windows = [windowState('result', false, true)]; expect(minimizeResultWindow(windows, 'result')).toEqual([ windowState('result', true, false) ]); }); it('stellt ein minimiertes Result im Cluster wieder her und minimiert nur den bisherigen Cluster-Fokus', () => { const windows = [ windowState('detached', false, true), windowState('cluster-active', false), windowState('target', true) ]; expect(toggleResultWindowMinimized(windows, 'target')).toEqual([ windowState('detached', false, true), windowState('cluster-active', true), windowState('target', false) ]); }); it('markiert nur ein maximiertes Result als herausgezogen', () => { const windows = [windowState('minimized', true), windowState('maximized', false)]; const afterMinimizedDrag = detachResultWindow(windows, 'minimized'); expect(afterMinimizedDrag).toEqual(windows); expect(detachResultWindow(afterMinimizedDrag, 'maximized')).toEqual([ windowState('minimized', true), windowState('maximized', false, true) ]); }); it('trennt die Ergebnisanzahl vom Titel ohne Restzeichen', () => { const markup = renderToStaticMarkup(createElement(FloatingResultsWindow, { id: 'separated-title', title: 'Results (1) \u2013 Synthetic result', sheets: [], color: '#7c3aed', zIndex: 2100, offset: 0, search: '', onActivate: () => undefined, onClose: () => undefined, onToggleMinimize: () => undefined })); expect(markup).toContain('Synthetic result'); expect(markup).not.toContain('results-window-minimize'); }); it('zeigt hinter dem aktiven Fenster nur schmale vertikale Farbreiter und bleibt im Viewport', () => { Object.defineProperty(window, 'innerWidth', { configurable: true, value: 800 }); Object.defineProperty(window, 'innerHeight', { configurable: true, value: 600 }); const markup = renderToStaticMarkup(createElement(FloatingResultsWindow, { id: 'synthetic-result', title: 'Results (1) – Synthetic result', sheets: [], color: '#7c3aed', zIndex: 2100, offset: 0, search: '', onActivate: () => undefined, onClose: () => undefined, clusterIndex: 0, clusterCount: 3, clusterPosition: { left: 40, top: 132 }, clusterWidth: 760 })); expect(markup).toContain('left:54px'); expect(markup).toContain('width:746px'); expect(markup).toContain('max-width:718px'); expect(markup).toContain('max-height:440px'); expect(markup).not.toContain('left:84px'); }); it('streckt ein kurzes aktives Clusterfenster ueber alle minimierten Results dahinter', () => { Object.defineProperty(window, 'innerWidth', { configurable: true, value: 1000 }); Object.defineProperty(window, 'innerHeight', { configurable: true, value: 700 }); const markup = renderToStaticMarkup(createElement(FloatingResultsWindow, { id: 'short-active-result', title: 'Results (1) – Short synthetic result', sheets: [], color: '#7c3aed', zIndex: 2100, offset: 0, search: '', onActivate: () => undefined, onClose: () => undefined, clusterIndex: 0, clusterCount: 8, clusterPosition: { left: 100, top: 160 }, clusterWidth: 760 })); expect(markup).toContain('min-height:238px'); }); it('kennzeichnet minimierte Cluster-Results als sichtbare Farbreiter', () => { const markup = renderToStaticMarkup(createElement(FloatingResultsWindow, { id: 'minimized-result', title: 'Results (1) – Synthetic result', sheets: [], color: '#7c3aed', zIndex: 2100, offset: 0, search: '', minimized: true, onActivate: () => undefined, onClose: () => undefined, clusterIndex: 0, clusterPosition: { left: 40, top: 132 }, clusterWidth: 760 })); expect(markup).toContain('results-window is-minimized'); expect(markup).toContain('--result-color:#7c3aed'); expect(markup).toContain('triangle-icon-right'); expect(markup).not.toContain('results-window-result-icon'); }); it('zeigt das farbige Zustandsdreieck und die Anzahl ohne Wuerfelsymbol', () => { const markup = renderToStaticMarkup(createElement(FloatingResultsWindow, { id: 'result-summary', title: 'Results (12) ??? Synthetic result', sheets: [], color: '#7c3aed', zIndex: 2100, offset: 0, search: '', onActivate: () => undefined, onClose: () => undefined })); expect(markup).toContain('triangle-icon-down'); expect(markup).toContain('results-window-toggle-icon'); expect(markup).not.toContain('results-window-result-icon'); expect(markup).toContain('>12'); expect(markup).not.toContain('results-window-badge'); }); it('stellt minimierte Results wieder her und maximiert geoeffnete Results per Kopfzeilen-Doppelklick', () => { (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true; vi.stubGlobal('ResizeObserver', vi.fn(() => ({ observe: vi.fn(), disconnect: vi.fn() }))); const onToggleMinimize = vi.fn(); const resizableSheet = makeSheet('Synthetic table', [['First', 'Second'], ['One', 'Two']], '', 'synthetic.xlsx'); const container = document.createElement('div'); const root = createRoot(container); const renderWindow = (minimized: boolean, detached = false, clustered = true) => createElement(FloatingResultsWindow, { id: 'double-click-result', title: 'Results (1) – Synthetic result', sheets: [resizableSheet], color: '#7c3aed', zIndex: 2100, offset: 0, search: '', minimized, detached, clusterIndex: clustered ? 0 : undefined, clusterPosition: clustered ? { left: 120, top: 160 } : undefined, onActivate: () => undefined, onClose: () => undefined, onToggleMinimize }); act(() => root.render(renderWindow(true))); expect(container.querySelector('.results-window-toggle .triangle-icon-right')).not.toBeNull(); act(() => container.querySelector('.results-window-toggle')?.dispatchEvent(new MouseEvent('click', { bubbles: true }))); expect(onToggleMinimize).toHaveBeenCalledTimes(1); act(() => container.querySelector('.results-window-head')?.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }))); expect(onToggleMinimize).toHaveBeenCalledTimes(2); act(() => root.render(renderWindow(false))); expect(container.querySelector('.results-window-toggle .triangle-icon-down')).not.toBeNull(); expect(container.querySelector('.result-column-resizer')).toBeNull(); act(() => container.querySelector('.results-window-head')?.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }))); expect(onToggleMinimize).toHaveBeenCalledTimes(2); expect(container.querySelector('.results-window.is-maximized')).not.toBeNull(); expect(container.querySelector('.result-column-resizer')).not.toBeNull(); expect((container.querySelector('.results-window') as HTMLElement).style.left).toBe('8px'); expect((container.querySelector('.results-window') as HTMLElement).style.maxWidth).toBe(`${window.innerWidth - 16}px`); act(() => container.querySelector('.results-window-head')?.dispatchEvent(new MouseEvent('click', { bubbles: true }))); expect(onToggleMinimize).toHaveBeenCalledTimes(2); expect(container.querySelector('.results-window.is-maximized')).toBeNull(); expect(container.querySelector('.result-column-resizer')).toBeNull(); // Der zum Klick gehoerende dblclick darf nicht unmittelbar erneut maximieren. act(() => container.querySelector('.results-window-head')?.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }))); expect(container.querySelector('.results-window.is-maximized')).toBeNull(); act(() => root.render(renderWindow(false, true))); expect(container.querySelector('.results-window.is-maximized')).toBeNull(); expect(container.querySelector('.result-column-resizer')).not.toBeNull(); act(() => root.render(renderWindow(false, false, false))); expect(container.querySelector('.result-column-resizer')).not.toBeNull(); act(() => root.render(renderWindow(true, false, true))); expect(container.querySelector('.result-column-resizer')).toBeNull(); act(() => container.querySelector('.results-window-close')?.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }))); expect(onToggleMinimize).toHaveBeenCalledTimes(2); act(() => root.unmount()); vi.unstubAllGlobals(); }); });