llm-semantic-visualizer / tests /visualizationControls.test.js
hbauzan's picture
fix(ui): Galaxy/Compare polish and startup defaults
74f4fb6
Raw
History Blame Contribute Delete
17.1 kB
import { describe, it, expect, beforeEach } from 'vitest';
import {
DEFAULT_VISUALIZATION_SETTINGS,
DEFAULT_VIZ_COLORS,
DEFAULT_VIZ_FILTER,
VIZ_STORAGE_KEYS,
isValidHex,
normalizeHex,
hexToRgb01,
resolveVisualizationSettings,
loadVisualizationSettings,
saveVisualizationSettings,
resetVisualizationSettings,
anchorsFromSettings,
remapAbsTWithZeroCoverage,
normalizeZeroCoverage,
normalizeHighCoverage,
highCoverageFromSlider,
highCoverageToSlider,
formatHighCoverage,
effectiveZeroCoveragePercent,
HIGH_COVERAGE_MIN,
HIGH_COVERAGE_MAX,
HIGH_COVERAGE_SLIDER_MAX,
} from '../src/ui/visualizationControlsDefaults.js';
import {
visualizationControlsMarkup,
setVisualizationPanelCollapsed,
setVisualizationPanelLayout,
vizPanelTabGlyph,
vizPanelLayoutForViewport,
resolveVisualizationMountParent,
syncGroupHueColorRows,
} from '../src/ui/VisualizationControls.js';
import {
NEAR_ZERO_EPS,
shouldShowActivation,
filterPointsData,
lineSegmentIndices,
wideRibbonQuadIndices,
} from '../src/visualizer/activationFilter.js';
import { getDivergentColor } from '../src/visualizer/DivergentShading.js';
function mockStorage(initial = {}) {
const map = new Map(Object.entries(initial));
return {
getItem: (k) => (map.has(k) ? map.get(k) : null),
setItem: (k, v) => { map.set(k, String(v)); },
removeItem: (k) => { map.delete(k); },
_map: map,
};
}
describe('visualizationControlsDefaults', () => {
it('defaults match §0.2 anchors and filter All', () => {
expect(DEFAULT_VIZ_FILTER).toBe('all');
expect(DEFAULT_VIZ_COLORS.colorPositive).toBe('#FFE600');
expect(DEFAULT_VIZ_COLORS.colorZero).toBe('#000000');
expect(DEFAULT_VIZ_COLORS.colorNegative).toBe('#9900E6');
expect(DEFAULT_VISUALIZATION_SETTINGS).toEqual({
vizFilterMode: 'all',
colorPositive: '#FFE600',
colorZero: '#000000',
colorNegative: '#9900E6',
zeroCoverageEnabled: false,
zeroCoverage: 30,
labelsVisible: true,
sameSignCancelEnabled: false,
sameSignCancelCoverage: 30,
oppositeHighlightEnabled: false,
oppositeHighlightColor: '#00E5FF',
oppositeHighlightStrength: 70,
oppositeCancelCoverage: 0,
groupHueEnabled: false,
groupHueColors: {},
});
});
it('validates and normalizes hex', () => {
expect(isValidHex('#FFE600')).toBe(true);
expect(isValidHex('#ff00')).toBe(false);
expect(isValidHex('not-a-color')).toBe(false);
expect(normalizeHex('ffe600')).toBe('#FFE600');
expect(normalizeHex('#abc')).toBeNull();
});
it('hexToRgb01 parses endpoints', () => {
expect(hexToRgb01('#FFE600')).toEqual({
r: 1,
g: 230 / 255,
b: 0,
});
expect(hexToRgb01('#000000')).toEqual({ r: 0, g: 0, b: 0 });
expect(hexToRgb01('#9900E6')).toEqual({
r: 153 / 255,
g: 0,
b: 230 / 255,
});
});
it('resolveVisualizationSettings falls back invalid hex and mode', () => {
const resolved = resolveVisualizationSettings({
vizFilterMode: 'nope',
colorPositive: 'bad',
colorZero: '#112233',
colorNegative: '#9900E6',
});
expect(resolved.vizFilterMode).toBe('all');
expect(resolved.colorPositive).toBe('#FFE600');
expect(resolved.colorZero).toBe('#112233');
expect(resolved.colorNegative).toBe('#9900E6');
});
it('persist round-trip via mock localStorage', () => {
const storage = mockStorage();
const settings = resolveVisualizationSettings({
vizFilterMode: 'positive',
colorPositive: '#FF0000',
colorZero: '#111111',
colorNegative: '#0000FF',
});
saveVisualizationSettings(settings, storage);
expect(storage.getItem(VIZ_STORAGE_KEYS.filter)).toBe('positive');
expect(loadVisualizationSettings(storage)).toEqual(settings);
});
it('reset restores defaults and All', () => {
const storage = mockStorage({
[VIZ_STORAGE_KEYS.filter]: 'negative',
[VIZ_STORAGE_KEYS.colorPositive]: '#FF0000',
});
const reset = resetVisualizationSettings(storage);
expect(reset).toEqual(DEFAULT_VISUALIZATION_SETTINGS);
expect(loadVisualizationSettings(storage)).toEqual(DEFAULT_VISUALIZATION_SETTINGS);
});
it('anchorsFromSettings yields RGB01 trio', () => {
const a = anchorsFromSettings(DEFAULT_VISUALIZATION_SETTINGS);
expect(a.positive.r).toBeCloseTo(1, 5);
expect(a.zero).toEqual({ r: 0, g: 0, b: 0 });
expect(a.negative.b).toBeCloseTo(230 / 255, 5);
});
});
describe('Visualization panel collapse tab', () => {
it('edge layout uses vertical glyphs (▼ raised, ▲ resting on HUD)', () => {
expect(vizPanelTabGlyph(false, 'edge')).toBe('▼');
expect(vizPanelTabGlyph(true, 'edge')).toBe('▲');
const expanded = visualizationControlsMarkup(DEFAULT_VISUALIZATION_SETTINGS, {
collapsed: false,
layout: 'edge',
});
expect(expanded).toContain('▼');
expect(expanded).not.toContain('▶');
const collapsed = visualizationControlsMarkup(DEFAULT_VISUALIZATION_SETTINGS, {
collapsed: true,
layout: 'edge',
});
expect(collapsed).toContain('▲');
expect(collapsed).not.toContain('◀');
});
it('markup includes edge tab and aria-expanded', () => {
const html = visualizationControlsMarkup(DEFAULT_VISUALIZATION_SETTINGS, { collapsed: false });
expect(html).toContain('viz-panel-tab');
expect(html).toContain('data-viz-layout="edge"');
expect(html).toContain('aria-expanded="true"');
expect(html).toContain('▼');
expect(html).toContain('viz-zero-coverage-slider');
expect(html).toContain('viz-zero-coverage-enabled');
expect(html).toContain('Zero coverage');
expect(html).toContain('viz-group-contrast');
expect(html).toContain('Group contrast');
expect(html).toContain('Group hue');
expect(html).toContain('viz-group-hue-enabled');
expect(html).toContain('viz-group-hue-rows');
expect(html).toContain('viz-same-sign-enabled');
expect(html).toContain('viz-opposite-enabled');
expect(html).toContain('viz-labels-toggle');
expect(html).toContain('Hide labels');
expect(html).toContain('field-info-btn');
expect(html).toContain('data-field-info="Which signs show."');
expect(html).toContain('data-field-info="Hold range at zero."');
expect(html).toContain('data-field-info="Groups only (G1↔G2)."');
});
it('collapse tab is a sibling of viz-panel-body (outside card chrome)', () => {
const html = visualizationControlsMarkup(DEFAULT_VISUALIZATION_SETTINGS, { collapsed: false });
const hostOpen = html.indexOf('id="visualization-controls-container"');
const tabOpen = html.indexOf('class="viz-panel-tab dock-tab"');
const bodyOpen = html.indexOf('id="viz-panel-body"');
const bodyClose = html.indexOf('</div>\n</div>', bodyOpen);
expect(hostOpen).toBeGreaterThan(-1);
expect(tabOpen).toBeGreaterThan(hostOpen);
expect(bodyOpen).toBeGreaterThan(tabOpen);
// Tab must not be nested inside the body card markup.
expect(tabOpen).toBeLessThan(bodyOpen);
expect(bodyClose).toBeGreaterThan(bodyOpen);
expect(html.slice(bodyOpen, bodyClose)).not.toContain('viz-panel-tab');
});
it('collapsed markup starts with expand glyph', () => {
const html = visualizationControlsMarkup(DEFAULT_VISUALIZATION_SETTINGS, { collapsed: true });
expect(html).toContain('is-collapsed');
expect(html).toContain('aria-expanded="false"');
expect(html).toContain('▲');
});
it('sheet layout uses the same vertical glyphs and layout attribute', () => {
expect(vizPanelLayoutForViewport(true)).toBe('sheet');
expect(vizPanelLayoutForViewport(false)).toBe('edge');
expect(vizPanelTabGlyph(true, 'sheet')).toBe('▲');
expect(vizPanelTabGlyph(false, 'sheet')).toBe('▼');
const html = visualizationControlsMarkup(DEFAULT_VISUALIZATION_SETTINGS, {
collapsed: true,
layout: 'sheet',
});
expect(html).toContain('data-viz-layout="sheet"');
expect(html).toContain('▲');
expect(html).toContain('Expand Visualization sheet');
});
it('resolveVisualizationMountParent always uses app root (HUD glue)', () => {
const dockBody = { id: 'dock' };
const appRoot = { id: 'app' };
expect(resolveVisualizationMountParent({
isMobile: true,
dockBody,
appRoot,
})).toBe(appRoot);
expect(resolveVisualizationMountParent({
isMobile: false,
dockBody,
appRoot,
})).toBe(appRoot);
});
it('setVisualizationPanelCollapsed toggles class and glyph', () => {
const classList = new Set(['section-card', 'viz-panel']);
const tab = {
textContent: '▼',
attrs: {},
setAttribute(k, v) { this.attrs[k] = String(v); },
};
const el = {
dataset: { vizLayout: 'edge' },
classList: {
toggle: (cls, force) => {
if (force) classList.add(cls);
else classList.delete(cls);
},
contains: (cls) => classList.has(cls),
},
querySelector: (sel) => (sel === '.viz-panel-tab' ? tab : null),
};
setVisualizationPanelCollapsed(el, true);
expect(classList.has('is-collapsed')).toBe(true);
expect(tab.textContent).toBe('▲');
expect(tab.attrs['aria-expanded']).toBe('false');
setVisualizationPanelCollapsed(el, false);
expect(classList.has('is-collapsed')).toBe(false);
expect(tab.textContent).toBe('▼');
});
it('setVisualizationPanelLayout keeps vertical glyphs on sheet', () => {
const classList = new Set(['section-card', 'viz-panel', 'is-collapsed']);
const tab = {
textContent: '▲',
attrs: {},
setAttribute(k, v) { this.attrs[k] = String(v); },
};
const el = {
dataset: { vizLayout: 'edge' },
classList: {
toggle: (cls, force) => {
if (force) classList.add(cls);
else classList.delete(cls);
},
contains: (cls) => classList.has(cls),
},
querySelector: (sel) => (sel === '.viz-panel-tab' ? tab : null),
};
setVisualizationPanelLayout(el, 'sheet');
expect(el.dataset.vizLayout).toBe('sheet');
expect(tab.textContent).toBe('▲');
});
});
describe('shouldShowActivation / filter helpers', () => {
const samples = [1, 0.5, 0.005, -0.5, -1];
it('all mode shows every sample', () => {
for (const v of samples) {
expect(shouldShowActivation(v, 'all')).toBe(true);
}
});
it('+ only hides negatives and near-zero', () => {
expect(shouldShowActivation(1, 'positive')).toBe(true);
expect(shouldShowActivation(0.5, 'positive')).toBe(true);
expect(shouldShowActivation(0.005, 'positive')).toBe(false);
expect(shouldShowActivation(-0.5, 'positive')).toBe(false);
expect(shouldShowActivation(-1, 'positive')).toBe(false);
});
it('− only hides positives and near-zero', () => {
expect(shouldShowActivation(1, 'negative')).toBe(false);
expect(shouldShowActivation(0.5, 'negative')).toBe(false);
expect(shouldShowActivation(0.005, 'negative')).toBe(false);
expect(shouldShowActivation(-0.5, 'negative')).toBe(true);
expect(shouldShowActivation(-1, 'negative')).toBe(true);
});
it('uses ε = 0.01 by default', () => {
expect(NEAR_ZERO_EPS).toBe(0.01);
expect(shouldShowActivation(0.009, 'positive')).toBe(false);
expect(shouldShowActivation(0.01, 'positive')).toBe(true);
});
it('filterPointsData keeps aligned entries', () => {
const points = samples.map((activation, i) => ({ i, activation }));
const kept = filterPointsData(points, samples, 'positive');
expect(kept.map((p) => p.i)).toEqual([0, 1]);
});
it('lineSegmentIndices breaks across hidden signs', () => {
// visible, visible, near-zero, visible-neg → only first segment
const norm = [0.5, 0.8, 0.0, -0.5];
expect(lineSegmentIndices(norm, 'positive')).toEqual([0, 1]);
expect(lineSegmentIndices(norm, 'all')).toEqual([0, 1, 1, 2, 2, 3]);
});
it('wideRibbonQuadIndices omits quads for filtered pairs', () => {
const norm = [0.5, 0.8, -0.5];
expect(wideRibbonQuadIndices(norm, 'positive')).toEqual([0, 2, 1, 1, 2, 3]);
expect(wideRibbonQuadIndices(norm, 'negative')).toEqual([]);
});
});
describe('getDivergentColor with color anchors', () => {
const anchors = anchorsFromSettings(DEFAULT_VISUALIZATION_SETTINGS);
it('t=1 → +1 hex', () => {
const c = getDivergentColor(1, 1, anchors);
expect(c.r).toBeCloseTo(anchors.positive.r, 5);
expect(c.g).toBeCloseTo(anchors.positive.g, 5);
expect(c.b).toBeCloseTo(anchors.positive.b, 5);
});
it('t=0 → 0 hex with low alpha', () => {
const c = getDivergentColor(0, 1, anchors);
expect(c.r).toBeCloseTo(anchors.zero.r, 5);
expect(c.g).toBeCloseTo(anchors.zero.g, 5);
expect(c.b).toBeCloseTo(anchors.zero.b, 5);
expect(c.alpha).toBeCloseTo(0.05, 5);
});
it('t=-1 → −1 hex', () => {
const c = getDivergentColor(-1, 1, anchors);
expect(c.r).toBeCloseTo(anchors.negative.r, 5);
expect(c.g).toBeCloseTo(anchors.negative.g, 5);
expect(c.b).toBeCloseTo(anchors.negative.b, 5);
});
it('t=0.5 → midpoint lerp 0↔+1', () => {
const c = getDivergentColor(0.5, 1, anchors);
expect(c.r).toBeCloseTo((anchors.zero.r + anchors.positive.r) / 2, 5);
expect(c.g).toBeCloseTo((anchors.zero.g + anchors.positive.g) / 2, 5);
expect(c.b).toBeCloseTo((anchors.zero.b + anchors.positive.b) / 2, 5);
});
it('zero coverage 50% keeps mid activations at zero color', () => {
const c = getDivergentColor(0.4, 1, anchors, 50);
expect(c.r).toBeCloseTo(anchors.zero.r, 5);
expect(c.g).toBeCloseTo(anchors.zero.g, 5);
expect(c.b).toBeCloseTo(anchors.zero.b, 5);
const midPos = getDivergentColor(0.75, 1, anchors, 50);
// remapped k = (0.75-0.5)/(1-0.5) = 0.5 → midpoint
expect(midPos.r).toBeCloseTo((anchors.zero.r + anchors.positive.r) / 2, 5);
});
});
describe('remapAbsTWithZeroCoverage', () => {
it('identity at 0 coverage', () => {
expect(remapAbsTWithZeroCoverage(0.4, 0)).toBeCloseTo(0.4, 5);
});
it('holds zero then stretches', () => {
expect(remapAbsTWithZeroCoverage(0.3, 0.5)).toBe(0);
expect(remapAbsTWithZeroCoverage(0.5, 0.5)).toBe(0);
expect(remapAbsTWithZeroCoverage(0.75, 0.5)).toBeCloseTo(0.5, 5);
expect(remapAbsTWithZeroCoverage(1, 0.5)).toBeCloseTo(1, 5);
});
it('supports near-100% coverage', () => {
const c = HIGH_COVERAGE_MAX / 100;
expect(remapAbsTWithZeroCoverage(0.99, c)).toBe(0);
expect(remapAbsTWithZeroCoverage(1, c)).toBeCloseTo(1, 5);
});
it('normalizeZeroCoverage clamps conflict cover 0–90', () => {
expect(normalizeZeroCoverage(150)).toBe(90);
expect(normalizeZeroCoverage(-3)).toBe(0);
expect(normalizeZeroCoverage('40')).toBe(40);
});
it('normalizeHighCoverage clamps 30…99.9999', () => {
expect(normalizeHighCoverage(10)).toBe(HIGH_COVERAGE_MIN);
expect(normalizeHighCoverage(100)).toBe(HIGH_COVERAGE_MAX);
expect(normalizeHighCoverage(50.12345)).toBeCloseTo(50.1235, 4);
});
it('highCoverage slider maps endpoints', () => {
expect(highCoverageFromSlider(0)).toBe(HIGH_COVERAGE_MIN);
expect(highCoverageFromSlider(HIGH_COVERAGE_SLIDER_MAX)).toBe(HIGH_COVERAGE_MAX);
expect(highCoverageToSlider(HIGH_COVERAGE_MIN)).toBe(0);
expect(highCoverageToSlider(HIGH_COVERAGE_MAX)).toBe(HIGH_COVERAGE_SLIDER_MAX);
expect(formatHighCoverage(HIGH_COVERAGE_MAX)).toBe('99.9999%');
});
it('effectiveZeroCoveragePercent is 0 when Off', () => {
expect(effectiveZeroCoveragePercent({ zeroCoverageEnabled: false, zeroCoverage: 80 })).toBe(0);
expect(effectiveZeroCoveragePercent({ zeroCoverageEnabled: true, zeroCoverage: 80 })).toBe(80);
});
it('syncGroupHueColorRows uses textarea labels and scrolls past 3 groups', () => {
let rowsHtml = '';
let scrollable = false;
const host = {
classList: {
toggle: (cls, on) => {
if (cls === 'is-scrollable') scrollable = Boolean(on);
},
},
set innerHTML(v) { rowsHtml = String(v); },
get innerHTML() { return rowsHtml; },
querySelectorAll: () => [],
};
const container = {
querySelector: (sel) => {
if (sel === '#viz-group-hue-rows') return host;
return null;
},
querySelectorAll: () => [],
};
const config = resolveVisualizationSettings({ groupHueEnabled: true });
syncGroupHueColorRows(container, [
{ id: 'animals', label: 'animals' },
{ id: 'it_core', label: 'it_core' },
{ id: 'women', label: 'women' },
], config);
expect(rowsHtml).toContain('>animals<');
expect(rowsHtml).toContain('>it_core<');
expect(rowsHtml).toContain('>women<');
expect(rowsHtml).toContain('data-group-id="animals"');
expect(scrollable).toBe(false);
syncGroupHueColorRows(container, ['G1', 'G2', 'G3', 'G4'], config);
expect(rowsHtml).toContain('>G1<');
expect(rowsHtml).toContain('>G4<');
expect(scrollable).toBe(true);
});
});