更新换行算法和APP-PUSH
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user