2026-05-30 21:49:25 +03:00
|
|
|
import {
|
|
|
|
|
describe,
|
|
|
|
|
expect,
|
|
|
|
|
it,
|
|
|
|
|
} from 'vitest';
|
|
|
|
|
import type { ComparisonLine } from '../DualFontLayout/DualFontLayout';
|
|
|
|
|
import { computeLineRenderModel } from './computeLineRenderModel';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Build a ComparisonLine fixture with given per-char widths. xA/xB are
|
|
|
|
|
* cumulative prefix sums of widthA/widthB respectively.
|
|
|
|
|
*/
|
|
|
|
|
function makeLine(
|
|
|
|
|
chars: { char: string; widthA: number; widthB: number }[],
|
|
|
|
|
): ComparisonLine {
|
|
|
|
|
let xA = 0;
|
|
|
|
|
let xB = 0;
|
|
|
|
|
const out: ComparisonLine = {
|
|
|
|
|
text: chars.map(c => c.char).join(''),
|
|
|
|
|
width: chars.reduce((s, c) => s + Math.max(c.widthA, c.widthB), 0),
|
|
|
|
|
chars: chars.map(c => {
|
|
|
|
|
const entry = {
|
|
|
|
|
char: c.char,
|
|
|
|
|
xA,
|
|
|
|
|
xB,
|
|
|
|
|
widthA: c.widthA,
|
|
|
|
|
widthB: c.widthB,
|
|
|
|
|
};
|
|
|
|
|
xA += c.widthA;
|
|
|
|
|
xB += c.widthB;
|
|
|
|
|
return entry;
|
|
|
|
|
}),
|
|
|
|
|
};
|
|
|
|
|
return out;
|
|
|
|
|
}
|
2026-05-30 21:48:56 +03:00
|
|
|
|
|
|
|
|
describe('computeLineRenderModel', () => {
|
2026-05-30 21:49:25 +03:00
|
|
|
it('returns empty model for an empty line', () => {
|
|
|
|
|
const line = makeLine([]);
|
|
|
|
|
const model = computeLineRenderModel(line, 50, 500, 5);
|
|
|
|
|
expect(model.leftText).toBe('');
|
|
|
|
|
expect(model.windowChars).toEqual([]);
|
|
|
|
|
expect(model.rightText).toBe('');
|
|
|
|
|
});
|
2026-05-30 21:50:02 +03:00
|
|
|
|
|
|
|
|
it('places entire line in rightText when slider is at 0', () => {
|
|
|
|
|
const line = makeLine([
|
|
|
|
|
{ char: 'A', widthA: 10, widthB: 10 },
|
|
|
|
|
{ char: 'B', widthA: 10, widthB: 10 },
|
|
|
|
|
{ char: 'C', widthA: 10, widthB: 10 },
|
|
|
|
|
]);
|
|
|
|
|
const model = computeLineRenderModel(line, 0, 500, 0);
|
|
|
|
|
expect(model.leftText).toBe('');
|
|
|
|
|
expect(model.windowChars).toEqual([]);
|
|
|
|
|
expect(model.rightText).toBe('ABC');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('places entire line in leftText when slider is at 100', () => {
|
|
|
|
|
const line = makeLine([
|
|
|
|
|
{ char: 'A', widthA: 10, widthB: 10 },
|
|
|
|
|
{ char: 'B', widthA: 10, widthB: 10 },
|
|
|
|
|
{ char: 'C', widthA: 10, widthB: 10 },
|
|
|
|
|
]);
|
|
|
|
|
const model = computeLineRenderModel(line, 100, 500, 0);
|
|
|
|
|
expect(model.leftText).toBe('ABC');
|
|
|
|
|
expect(model.windowChars).toEqual([]);
|
|
|
|
|
expect(model.rightText).toBe('');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('splits line correctly with slider mid-line (window=0)', () => {
|
|
|
|
|
// Equal widths → line is centered. Container=300, total=30 → xOffset=135.
|
|
|
|
|
// Char thresholds (per the threshold formula in the design):
|
|
|
|
|
// threshold[i] = xOffset + prefA[i] + widthA[i]/2
|
|
|
|
|
// i=0: 135 + 0 + 5 = 140 → 140/300 = 46.67%
|
|
|
|
|
// i=1: 135 + 10 + 5 = 150 → 150/300 = 50.00%
|
|
|
|
|
// i=2: 135 + 20 + 5 = 160 → 160/300 = 53.33%
|
|
|
|
|
const line = makeLine([
|
|
|
|
|
{ char: 'A', widthA: 10, widthB: 10 },
|
|
|
|
|
{ char: 'B', widthA: 10, widthB: 10 },
|
|
|
|
|
{ char: 'C', widthA: 10, widthB: 10 },
|
|
|
|
|
]);
|
|
|
|
|
// Slider just past B's threshold (50%) but not C's (53.33%).
|
|
|
|
|
const model = computeLineRenderModel(line, 51, 300, 0);
|
|
|
|
|
expect(model.leftText).toBe('AB');
|
|
|
|
|
expect(model.rightText).toBe('C');
|
|
|
|
|
});
|
2026-05-30 21:48:56 +03:00
|
|
|
});
|