125 lines
4.4 KiB
TypeScript
125 lines
4.4 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
||
|
||
import type { MeasureWidthImpl } from '../../measure/types';
|
||
import { searchBestLayoutApp } from '../index';
|
||
import { DEFAULT_LEXICONS, DEFAULT_WEIGHTS } from '../../scoring/index';
|
||
|
||
function t(text: string, start: number) {
|
||
return { text, start, end: start + text.length };
|
||
}
|
||
|
||
describe('textWrap search-engine-app', () => {
|
||
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,
|
||
lineMode: 'AUTO' as const,
|
||
measure: {
|
||
contextProfile: 'APP|ios|test',
|
||
fontSpec: { fontFamily: 'PingFangSC-Regular', fontWeight: '400', fontSize: 16 },
|
||
measureWidthImpl: impl,
|
||
},
|
||
scoring: {
|
||
config: {
|
||
weights: DEFAULT_WEIGHTS,
|
||
idealWidthRatio: { APP: 0.9, WIDGET: 0.95 },
|
||
ellipsisToken: '…',
|
||
tcParticleWhitelist: [],
|
||
},
|
||
lexicons: DEFAULT_LEXICONS,
|
||
debug: true,
|
||
},
|
||
};
|
||
|
||
const a = await searchBestLayoutApp(input as any);
|
||
const b = await searchBestLayoutApp(input as any);
|
||
const c = await searchBestLayoutApp(input as any);
|
||
|
||
expect(a).toEqual(b);
|
||
expect(b).toEqual(c);
|
||
expect(impl).toHaveBeenCalled(); // 有测量发生
|
||
});
|
||
|
||
it('lineMode=FIXED:无解必须返回 NO_CANDIDATE', async () => {
|
||
const impl: MeasureWidthImpl = vi.fn(async ({ text }) => text.length);
|
||
const res = await searchBestLayoutApp({
|
||
tokens: [t('I', 0), t('am', 2)],
|
||
lang: 'EN',
|
||
breakpoints: [{ pos: 1, kind: 'SPACE', priority: 10 }],
|
||
availableWidth: 100,
|
||
maxLines: 3,
|
||
lineMode: 'FIXED',
|
||
measure: {
|
||
contextProfile: 'APP|ios|fixed',
|
||
fontSpec: { fontFamily: 'PingFangSC-Regular', fontWeight: '400', fontSize: 16 },
|
||
measureWidthImpl: impl,
|
||
},
|
||
scoring: {
|
||
config: { weights: DEFAULT_WEIGHTS, idealWidthRatio: { APP: 0.9, WIDGET: 0.95 }, ellipsisToken: '…', tcParticleWhitelist: [] },
|
||
lexicons: DEFAULT_LEXICONS,
|
||
},
|
||
});
|
||
expect(res.ok).toBe(false);
|
||
if (!res.ok) expect(res.reason).toBe('NO_CANDIDATE');
|
||
});
|
||
|
||
it('TC H6:断点导致下一行行首为标点时必须禁止', async () => {
|
||
const impl: MeasureWidthImpl = vi.fn(async ({ text }) => text.length);
|
||
const res = await searchBestLayoutApp({
|
||
tokens: [t('我', 0), t('好', 1), t(',', 2), t('累', 3)],
|
||
lang: 'TC',
|
||
// 若选择 pos=2,则第二行行首 token 为 ','(应禁止)
|
||
breakpoints: [{ pos: 2, kind: 'PUNCT', priority: 30 }],
|
||
availableWidth: 100,
|
||
maxLines: 2,
|
||
lineMode: 'FIXED',
|
||
measure: {
|
||
contextProfile: 'APP|ios|tc',
|
||
fontSpec: { fontFamily: 'PingFangSC-Regular', fontWeight: '400', fontSize: 16 },
|
||
measureWidthImpl: impl,
|
||
},
|
||
scoring: {
|
||
config: { weights: DEFAULT_WEIGHTS, idealWidthRatio: { APP: 0.9, WIDGET: 0.95 }, ellipsisToken: '…', tcParticleWhitelist: [], tcPunctuations: [','] },
|
||
lexicons: DEFAULT_LEXICONS,
|
||
},
|
||
});
|
||
|
||
// lineMode=FIXED 且唯一断点被禁止,因此无解
|
||
expect(res.ok).toBe(false);
|
||
if (!res.ok) expect(res.reason).toBe('NO_CANDIDATE');
|
||
});
|
||
|
||
it('TOO_LONG:超过阈值直接返回 TOO_LONG', async () => {
|
||
const impl: MeasureWidthImpl = vi.fn(async ({ text }) => text.length);
|
||
const tokens = Array.from({ length: 31 }).map((_, i) => t('a', i));
|
||
const res = await searchBestLayoutApp({
|
||
tokens,
|
||
lang: 'EN',
|
||
breakpoints: [],
|
||
availableWidth: 100,
|
||
maxLines: 2,
|
||
lineMode: 'AUTO',
|
||
measure: {
|
||
contextProfile: 'APP|ios|toolong',
|
||
fontSpec: { fontFamily: 'PingFangSC-Regular', fontWeight: '400', fontSize: 16 },
|
||
measureWidthImpl: impl,
|
||
},
|
||
scoring: {
|
||
config: { weights: DEFAULT_WEIGHTS, idealWidthRatio: { APP: 0.9, WIDGET: 0.95 }, ellipsisToken: '…', tcParticleWhitelist: [] },
|
||
lexicons: DEFAULT_LEXICONS,
|
||
},
|
||
});
|
||
expect(res.ok).toBe(false);
|
||
if (!res.ok) expect(res.reason).toBe('TOO_LONG');
|
||
});
|
||
});
|
||
|