import { describe, expect, it } from 'vitest'; import { generateBreakpoints } from '../index'; function t(text: string, start: number): { text: string; start: number; end: number } { return { text, start, end: start + text.length }; } describe('textWrap breakpoint-candidates', () => { it('EN: tokens=[I,am,so,tired] -> pos=[1,2,3](升序)', () => { const tokens = [t('I', 0), t('am', 2), t('so', 5), t('tired', 8)]; const { breakpoints, meta } = generateBreakpoints({ tokens: tokens as any, lang: 'EN', maxLines: 2, config: { tcMaxCandidateBreaks: 80, tcPunctuations: [',', '。'], balanceRange: 3 }, }); expect(breakpoints.map((b) => b.pos)).toEqual([1, 2, 3]); expect(meta.originalCount).toBe(3); expect(meta.finalCount).toBe(3); }); it('TC: 标点后断点(,)应生成 pos=i+1', () => { const tokens = [t('我', 0), t('好', 1), t('累', 2), t(',', 3), t('😮‍💨', 4)]; const { breakpoints } = generateBreakpoints({ tokens: tokens as any, lang: 'TC', maxLines: 2, config: { tcMaxCandidateBreaks: 80, tcPunctuations: [','], balanceRange: 0 }, }); expect(breakpoints.some((b) => b.kind === 'PUNCT' && b.pos === 4)).toBe(true); }); it('TC: forbiddenBreakRanges(闭区间)命中必须剔除(含 start/end)', () => { const tokens = [t('我', 0), t('好', 1), t('累', 2), t(',', 3), t('😮‍💨', 4)]; const { breakpoints } = generateBreakpoints({ tokens: tokens as any, lang: 'TC', maxLines: 2, constraints: { forbiddenBreakRanges: [{ start: 4, end: 4 }] }, config: { tcMaxCandidateBreaks: 80, tcPunctuations: [','], balanceRange: 0 }, }); expect(breakpoints.some((b) => b.pos === 4)).toBe(false); }); it('TC: 去重规则:同 pos 优先保留 priority 更高者;同 priority 按 kind 序', () => { // 构造:同一个 pos=2 同时来自 SPACE(priority=20) 和 BALANCE(priority=5),必须保留 SPACE const tokens = [t('我', 0), t(' ', 1), t('好', 2)]; const { breakpoints } = generateBreakpoints({ tokens: tokens as any, lang: 'TC', maxLines: 2, config: { tcMaxCandidateBreaks: 80, tcPunctuations: [], balanceRange: 2 }, }); const b2 = breakpoints.find((b) => b.pos === 2); expect(b2?.kind).toBe('SPACE'); expect(b2?.priority).toBe(20); }); it('TC: 裁剪上限生效且输出按 pos 升序(确定性)', () => { const tokens = Array.from({ length: 60 }).map((_, i) => t('哈', i)); const res = generateBreakpoints({ tokens: tokens as any, lang: 'TC', maxLines: 3, config: { tcMaxCandidateBreaks: 8, tcPunctuations: [], balanceRange: 6 }, }); expect(res.breakpoints.length).toBe(8); // pos 升序 for (let i = 1; i < res.breakpoints.length; i++) { expect(res.breakpoints[i]!.pos).toBeGreaterThanOrEqual(res.breakpoints[i - 1]!.pos); } // 确定性 const res2 = generateBreakpoints({ tokens: tokens as any, lang: 'TC', maxLines: 3, config: { tcMaxCandidateBreaks: 8, tcPunctuations: [], balanceRange: 6 }, }); expect(res).toEqual(res2); }); it('边界:N<=1 时不输出任何候选断点', () => { const { breakpoints } = generateBreakpoints({ tokens: [t('我', 0)] as any, lang: 'TC', maxLines: 2, config: { tcMaxCandidateBreaks: 80, tcPunctuations: [','], balanceRange: 3 }, }); expect(breakpoints).toEqual([]); }); });