Files
mindfulness/client/src/features/textWrap/searchWidget/__tests__/searchWidget.test.ts
2026-02-10 11:39:33 +08:00

139 lines
5.0 KiB
TypeScript
Raw 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.
import { describe, expect, it, vi } from 'vitest';
import type { MeasureWidthImpl } from '../../measure/types';
import { DEFAULT_LEXICONS, DEFAULT_WEIGHTS } from '../../scoring/index';
import { searchBestLayoutWidget } from '../index';
function t(text: string, start: number) {
return { text, start, end: start + text.length };
}
describe('textWrap search-engine-widget', () => {
it('确定性:同输入多次调用 breaks/lines 必须一致', async () => {
const impl: MeasureWidthImpl = vi.fn(async ({ text }) => text.length);
const input = {
tokens: [t('I', 0), t('am', 2), t('so', 5), t('tired', 8)],
lang: 'EN' as const,
breakpoints: [
{ pos: 1, kind: 'SPACE', priority: 10 },
{ pos: 2, kind: 'SPACE', priority: 10 },
{ pos: 3, kind: 'SPACE', priority: 10 },
],
availableWidth: 6,
maxLines: 2,
context: 'WIDGET' as const,
measure: {
widthMode: 'MEASURE' as const,
contextProfile: 'WIDGET|small|test',
fontSpec: { fontFamily: 'PingFangSC-Regular', fontWeight: '400', fontSize: 16 },
measureWidthImpl: impl,
widgetEnableMeasure: true,
},
scoring: {
config: {
weights: DEFAULT_WEIGHTS,
idealWidthRatio: { APP: 0.9, WIDGET: 0.95 },
ellipsisToken: '…',
tcParticleWhitelist: [],
},
lexicons: DEFAULT_LEXICONS,
debug: true,
},
config: { beamK: 5, expandM: 12 },
};
const a = await searchBestLayoutWidget(input as any);
const b = await searchBestLayoutWidget(input as any);
const c = await searchBestLayoutWidget(input as any);
expect(a).toEqual(b);
expect(b).toEqual(c);
});
it('widthMode=APPROX仍可输出并标记 meta.reason=WIDTH_UNKNOWN', async () => {
const res = await searchBestLayoutWidget({
tokens: [t('我', 0), t('好', 1), t('累', 2)],
lang: 'TC',
breakpoints: [{ pos: 1, kind: 'BALANCE', priority: 5 }],
// APPROX 模式下availableWidth 与 width 单位都按“token 数”理解
availableWidth: 10,
maxLines: 2,
context: 'WIDGET',
measure: { widthMode: 'APPROX' },
scoring: {
config: { weights: DEFAULT_WEIGHTS, idealWidthRatio: { APP: 0.9, WIDGET: 0.95 }, ellipsisToken: '…', tcParticleWhitelist: [] },
lexicons: DEFAULT_LEXICONS,
debug: true,
},
config: { beamK: 3, expandM: 2 },
});
expect(res.ok).toBe(true);
if (res.ok) {
expect(res.bestLayout.meta?.reason).toBe('WIDTH_UNKNOWN');
}
});
it('性能约束expandM/beamK 生效(通过测量调用次数粗略验证)', async () => {
const impl: MeasureWidthImpl = vi.fn(async ({ text }) => text.length);
const res = await searchBestLayoutWidget({
tokens: [t('a', 0), t('b', 2), t('c', 4), t('d', 6), t('e', 8)],
lang: 'EN',
breakpoints: [
{ pos: 1, kind: 'SPACE', priority: 10 },
{ pos: 2, kind: 'SPACE', priority: 10 },
{ pos: 3, kind: 'SPACE', priority: 10 },
{ pos: 4, kind: 'SPACE', priority: 10 },
],
availableWidth: 100,
maxLines: 3,
context: 'WIDGET',
measure: {
widthMode: 'MEASURE',
contextProfile: 'WIDGET|perf',
fontSpec: { fontFamily: 'PingFangSC-Regular', fontWeight: '400', fontSize: 16 },
measureWidthImpl: impl,
widgetEnableMeasure: true,
},
scoring: {
config: { weights: DEFAULT_WEIGHTS, idealWidthRatio: { APP: 0.9, WIDGET: 0.95 }, ellipsisToken: '…', tcParticleWhitelist: [] },
lexicons: DEFAULT_LEXICONS,
},
config: { beamK: 2, expandM: 2 },
});
expect(res.ok).toBe(true);
// 每轮最多 beamK 个 beam每个 beam 最多 expandM 次扩展 -> 粗略上限maxLines*beamK*expandM
expect((impl as any).mock.calls.length).toBeLessThanOrEqual(3 * 2 * 2 + 2);
});
it('TC H6行首标点导致的转移必须被禁止', async () => {
const impl: MeasureWidthImpl = vi.fn(async ({ text }) => text.length);
const res = await searchBestLayoutWidget({
tokens: [t('我', 0), t('好', 1), t('', 2), t('累', 3)],
lang: 'TC',
breakpoints: [{ pos: 2, kind: 'PUNCT', priority: 30 }],
// 让“一行放下全文”不可能(触发超宽过滤),从而必须依赖断点切分
availableWidth: 3,
maxLines: 2,
context: 'WIDGET',
measure: {
widthMode: 'MEASURE',
contextProfile: 'WIDGET|tc',
fontSpec: { fontFamily: 'PingFangSC-Regular', fontWeight: '400', fontSize: 16 },
measureWidthImpl: impl,
widgetEnableMeasure: true,
},
scoring: {
config: { weights: DEFAULT_WEIGHTS, idealWidthRatio: { APP: 0.9, WIDGET: 0.95 }, ellipsisToken: '…', tcParticleWhitelist: [], tcPunctuations: [''] },
lexicons: DEFAULT_LEXICONS,
},
config: { beamK: 3, expandM: 3 },
});
expect(res.ok).toBe(false);
if (!res.ok) expect(res.reason).toBe('NO_CANDIDATE');
});
});