// @vitest-environment jsdom
import { act } from 'react';
import { readFileSync } from 'node:fs';
import { createRoot } from 'react-dom/client';
import { describe, expect, it, vi } from 'vitest';
import { ClientInfoTabs } from '../../src/features/dashboard/ClientInfoTabs';
describe.each(['dark', 'light'])('Client Info attached tabs (%s)', theme => {
it('keeps the tablist outside the scrolling panel and preserves the scrolled Overview through a tab switch', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
document.documentElement.dataset.theme = theme;
const style = document.createElement('style');
style.textContent = readFileSync('src/styles/datahub.css', 'utf8') + readFileSync('src/styles/layout.css', 'utf8'); document.head.append(style);
const host = document.createElement('div'); document.body.append(host);
host.className = 'workspace workspace-dashboard';
const root = createRoot(host);
try {
await act(async () => root.render( Synthetic row {i}
) },
{ id: 'security', label: 'Security' }
]}/>));
const wrapper = host.querySelector('.client-info-tabs')!;
const tablist = host.querySelector('[role="tablist"]')!;
const panels = host.querySelectorAll('[role="tabpanel"]');
const tabs = host.querySelectorAll('[role="tab"]');
expect(getComputedStyle(wrapper).height).toBe('100%');
expect(getComputedStyle(wrapper).overflow).toBe('hidden');
expect(getComputedStyle(tablist).flex).toBe('0 0 auto');
expect(getComputedStyle(tablist).paddingTop).toBe('0px');
expect(getComputedStyle(tabs[0]).borderTopLeftRadius).toBe('var(--panel-radius)');
expect(getComputedStyle(tabs[1]).borderTopLeftRadius).not.toBe('var(--panel-radius)');
expect(readFileSync('src/styles/layout.css', 'utf8')).toContain('.app-header-panel, .viewer { border-radius: var(--panel-radius); }');
expect(readFileSync('src/styles/ui-tokens.css', 'utf8')).toContain('--panel-radius: 10px;');
expect(getComputedStyle(host).overflow).toBe('visible');
expect(getComputedStyle(host.querySelector('.viewer')!).overflow).toBe('visible');
expect(readFileSync('src/styles/layout.css', 'utf8')).toMatch(/\.viewer\.app-area-dashboard\s*\{[^}]*margin-top:\s*calc\(10px - var\(--client-info-tab-height\)\)/);
expect(getComputedStyle(panels[0]).overflowY).toBe('auto');
expect(panels[0].contains(tablist)).toBe(false);
panels[0].scrollTop = 400;
await act(async () => panels[0].dispatchEvent(new Event('scroll')));
expect(tabs[0].getAttribute('aria-selected')).toBe('true');
await act(async () => tabs[1].click());
expect(panels[1].hidden).toBe(false);
await act(async () => tabs[1].dispatchEvent(new KeyboardEvent('keydown', { key: 'Home', bubbles: true })));
expect(panels[0].hidden).toBe(false);
expect(panels[0].scrollTop).toBe(400);
expect(getComputedStyle(tablist).display).toBe('flex');
} finally { await act(async () => root.unmount()); host.remove(); style.remove(); vi.unstubAllGlobals(); }
});
it('starts on Overview, opens an empty Security panel and preserves Overview through mouse and keyboard switches', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
document.documentElement.dataset.theme = theme;
const container = document.createElement('div'); document.body.append(container);
const root = createRoot(container);
try {
await act(async () => root.render( },
{ id: 'security', label: 'Security' },
{ id: 'future', label: 'Future', content: 'Synthetic future panel' }
]}/>));
const tabs = Array.from(container.querySelectorAll('[role="tab"]'));
const panels = Array.from(container.querySelectorAll('[role="tabpanel"]'));
expect(tabs.map(tab => tab.className)).toEqual(Array(3).fill('client-info-tab'));
expect(tabs.map(tab => tab.getAttribute('aria-selected'))).toEqual(['true', 'false', 'false']);
const input = container.querySelector('input')!;
input.value = 'changed';
await act(async () => tabs[1].click());
expect(panels.map(panel => panel.hidden)).toEqual([true, false, true]);
expect(panels[1].childNodes).toHaveLength(0);
expect(tabs[1].getAttribute('aria-controls')).toBe(panels[1].id);
await act(async () => tabs[1].dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true })));
expect(panels[2].hidden).toBe(false);
expect(document.activeElement).toBe(tabs[2]);
await act(async () => tabs[2].dispatchEvent(new KeyboardEvent('keydown', { key: 'Home', bubbles: true })));
expect(panels[0].hidden).toBe(false);
expect(input.value).toBe('changed');
expect(tabs.map(tab => tab.tabIndex)).toEqual([0, -1, -1]);
await act(async () => tabs[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true })));
expect(panels[2].hidden).toBe(false);
} finally { await act(async () => root.unmount()); container.remove(); vi.unstubAllGlobals(); }
});
});
describe('Client Info lazy engine tabs', () => {
it('mounts a lazy tab only while it is selected, so hidden engine tabs start no requests', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
const mounted = vi.fn();
function Probe() { mounted(); return Synthetic engine tab
; }
const host = document.createElement('div'); document.body.append(host);
const root = createRoot(host);
try {
await act(async () => root.render(Synthetic overview
},
{ id: 'compliance', label: 'Compliance', content: , lazy: true }
]}/>));
const tabs = host.querySelectorAll('[role="tab"]');
const panels = host.querySelectorAll('[role="tabpanel"]');
expect(mounted).not.toHaveBeenCalled();
expect(panels[1].childNodes).toHaveLength(0);
await act(async () => tabs[1].click());
expect(mounted).toHaveBeenCalled();
expect(panels[1].textContent).toBe('Synthetic engine tab');
await act(async () => tabs[0].click());
expect(panels[1].childNodes).toHaveLength(0);
} finally { await act(async () => root.unmount()); host.remove(); vi.unstubAllGlobals(); }
});
});