274 lines
11 KiB
TypeScript
274 lines
11 KiB
TypeScript
// @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('<span class="results-window-name">Synthetic result</span>');
|
||
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</span>');
|
||
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();
|
||
});
|
||
});
|