Assessor_2.0/tests/features/result-window-layout.test.ts

328 lines
15 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @vitest-environment jsdom
import { act, createElement, useState } 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 { useFloatingWindowLayer } from '../../src/components/use-floating-window-layer';
import { FloatingDataWindow } from '../../src/components/FloatingDataWindow/FloatingDataWindow';
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.each(['light', 'dark'])('keeps restored Results above every minimized sibling after pointer and keyboard activation (%s)', theme => {
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
document.documentElement.dataset.theme = theme;
vi.stubGlobal('ResizeObserver', vi.fn(() => ({ observe: vi.fn(), disconnect: vi.fn() })));
const host = document.createElement('div'); document.body.append(host);
const root = createRoot(host);
function Cluster() {
const [windows, setWindows] = useState([windowState('first', true), windowState('second', true), windowState('third', false)]);
const { layer, activate } = useFloatingWindowLayer();
return createElement('div', null,
...windows.map(item => createElement(FloatingResultsWindow, {
key: item.recordId, id: item.recordId, title: `Results (1) – ${item.recordId}`, sheets: [],
sections: [{ title: 'Test result', value: `Original evidence ${item.recordId}` }],
color: 'var(--status-high)', zIndex: 2100, offset: 0, search: '', minimized: item.minimized,
clusterPosition: { left: 40, top: 132 }, clusterIndex: item.minimized ? windows.filter(w => w.minimized).indexOf(item) : 0,
clusterCount: windows.length, clusterLayer: layer, onActivateCluster: activate,
onActivate: () => undefined, onClose: () => undefined,
onToggleMinimize: () => setWindows(current => toggleResultWindowMinimized(current, item.recordId))
})), createElement(FloatingDataWindow, { title: 'Other window', onClose: () => undefined, children: 'Other content' }));
}
const dialog = (id: string) => host.querySelector<HTMLElement>(`#${id}`) ?? host.querySelector<HTMLElement>(`.results-window[aria-label="Results (1) – ${id}"]`)!;
const z = (element: HTMLElement) => Number(element.style.zIndex);
const assertForeground = (id: string) => {
const active = dialog(id);
expect(active.classList.contains('is-minimized')).toBe(false);
expect(active.querySelector('.results-window-body')?.textContent).toContain(`Original evidence ${id}`);
for (const sibling of host.querySelectorAll<HTMLElement>('.results-window.is-minimized')) expect(z(active)).toBeGreaterThan(z(sibling));
expect(active.querySelector('.results-window-head')?.lastElementChild?.lastElementChild?.getAttribute('aria-label')).toBe('Close results window');
};
try {
act(() => root.render(createElement(Cluster)));
assertForeground('third');
for (const id of ['first', 'second', 'third', 'first']) {
act(() => dialog(id).dispatchEvent(new Event('pointerdown', { bubbles: true })));
act(() => dialog(id).querySelector<HTMLElement>('.results-window-toggle')!.click());
assertForeground(id);
const sibling = host.querySelector<HTMLElement>('.results-window.is-minimized')!;
act(() => sibling.querySelector<HTMLElement>('.results-window-toggle')!.focus());
assertForeground(id);
const other = document.querySelector<HTMLElement>('.floating-data-window')!;
act(() => other.dispatchEvent(new Event('pointerdown', { bubbles: true })));
expect(z(other)).toBeGreaterThan(z(dialog(id)));
act(() => dialog(id).dispatchEvent(new Event('pointerdown', { bubbles: true })));
expect(z(dialog(id))).toBeGreaterThan(z(other));
}
act(() => dialog('first').querySelector('.results-window-head')!.dispatchEvent(new MouseEvent('dblclick', { bubbles: true })));
expect(dialog('first').classList.contains('is-maximized')).toBe(true);
assertForeground('first');
} finally {
act(() => root.unmount()); host.remove(); vi.unstubAllGlobals(); delete document.documentElement.dataset.theme;
}
});
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" title="Synthetic result">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();
});
});