d53f3b42e2
* fix: use string-parsed year to avoid UTC/timezone boundary issues * feat: Add option to split words in RSVP mode * fix: change the checkbox to a toggle * fix(rsvp): speed up countdown from 2.4s to 1.5s * feat(rsvp): skip to next page when no readable text is found * fix(rsvp): replace lookbehind regex with lookahead-only split in getHyphenParts
279 lines
8.3 KiB
TypeScript
279 lines
8.3 KiB
TypeScript
import { describe, test, expect } from 'vitest';
|
||
import {
|
||
isCJK,
|
||
containsCJK,
|
||
isCJKPunctuation,
|
||
getSegmenterLocale,
|
||
segmentCJKText,
|
||
splitTextIntoWords,
|
||
getHyphenParts,
|
||
} from '@/services/rsvp/utils';
|
||
|
||
describe('rsvp/utils', () => {
|
||
describe('isCJK', () => {
|
||
test('returns true for CJK Unified Ideographs', () => {
|
||
expect(isCJK('\u4e00')).toBe(true); // first CJK character
|
||
expect(isCJK('\u9fff')).toBe(true); // last CJK character
|
||
expect(isCJK('\u5f00')).toBe(true); // 开
|
||
});
|
||
|
||
test('returns true for Hiragana', () => {
|
||
expect(isCJK('\u3042')).toBe(true); // あ
|
||
});
|
||
|
||
test('returns true for Katakana', () => {
|
||
expect(isCJK('\u30A2')).toBe(true); // ア
|
||
});
|
||
|
||
test('returns true for Hangul', () => {
|
||
expect(isCJK('\uAC00')).toBe(true); // 가
|
||
});
|
||
|
||
test('returns true for CJK Extension A', () => {
|
||
expect(isCJK('\u3400')).toBe(true);
|
||
});
|
||
|
||
test('returns true for CJK Compatibility Ideographs', () => {
|
||
expect(isCJK('\uF900')).toBe(true);
|
||
});
|
||
|
||
test('returns false for Latin characters', () => {
|
||
expect(isCJK('a')).toBe(false);
|
||
expect(isCJK('Z')).toBe(false);
|
||
});
|
||
|
||
test('returns false for digits', () => {
|
||
expect(isCJK('1')).toBe(false);
|
||
});
|
||
|
||
test('returns false for spaces', () => {
|
||
expect(isCJK(' ')).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('containsCJK', () => {
|
||
test('returns true for text with CJK characters', () => {
|
||
expect(containsCJK('Hello 你好')).toBe(true);
|
||
});
|
||
|
||
test('returns true for pure CJK text', () => {
|
||
expect(containsCJK('你好世界')).toBe(true);
|
||
});
|
||
|
||
test('returns false for pure Latin text', () => {
|
||
expect(containsCJK('Hello World')).toBe(false);
|
||
});
|
||
|
||
test('returns false for empty string', () => {
|
||
expect(containsCJK('')).toBe(false);
|
||
});
|
||
|
||
test('returns true for Japanese hiragana', () => {
|
||
expect(containsCJK('こんにちは')).toBe(true);
|
||
});
|
||
|
||
test('returns true for Korean', () => {
|
||
expect(containsCJK('안녕하세요')).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('isCJKPunctuation', () => {
|
||
test('returns true for Chinese period', () => {
|
||
expect(isCJKPunctuation('。')).toBe(true);
|
||
});
|
||
|
||
test('returns true for Chinese comma', () => {
|
||
expect(isCJKPunctuation(',')).toBe(true);
|
||
});
|
||
|
||
test('returns true for full-width exclamation', () => {
|
||
expect(isCJKPunctuation('!')).toBe(true);
|
||
});
|
||
|
||
test('returns true for full-width question mark', () => {
|
||
expect(isCJKPunctuation('?')).toBe(true);
|
||
});
|
||
|
||
test('returns true for brackets', () => {
|
||
expect(isCJKPunctuation('【')).toBe(true);
|
||
expect(isCJKPunctuation('】')).toBe(true);
|
||
expect(isCJKPunctuation('「')).toBe(true);
|
||
expect(isCJKPunctuation('」')).toBe(true);
|
||
});
|
||
|
||
test('returns true for ellipsis', () => {
|
||
expect(isCJKPunctuation('…')).toBe(true);
|
||
});
|
||
|
||
test('returns false for standard Latin comma', () => {
|
||
expect(isCJKPunctuation(',')).toBe(false);
|
||
});
|
||
|
||
test('returns false for Latin period', () => {
|
||
expect(isCJKPunctuation('.')).toBe(false);
|
||
});
|
||
|
||
test('returns false for a letter', () => {
|
||
expect(isCJKPunctuation('A')).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('getSegmenterLocale', () => {
|
||
test('returns ja for Japanese hiragana text', () => {
|
||
expect(getSegmenterLocale('こんにちは')).toBe('ja');
|
||
});
|
||
|
||
test('returns ja for Katakana text', () => {
|
||
expect(getSegmenterLocale('アイウ')).toBe('ja');
|
||
});
|
||
|
||
test('returns ko for Korean text', () => {
|
||
expect(getSegmenterLocale('안녕하세요')).toBe('ko');
|
||
});
|
||
|
||
test('returns zh for Chinese text', () => {
|
||
expect(getSegmenterLocale('你好世界')).toBe('zh');
|
||
});
|
||
|
||
test('returns null for pure Latin text', () => {
|
||
expect(getSegmenterLocale('Hello World')).toBeNull();
|
||
});
|
||
|
||
test('returns null for empty string', () => {
|
||
expect(getSegmenterLocale('')).toBeNull();
|
||
});
|
||
|
||
test('detects first CJK script in mixed text', () => {
|
||
// Japanese hiragana appears first
|
||
expect(getSegmenterLocale('あ你好')).toBe('ja');
|
||
});
|
||
});
|
||
|
||
describe('segmentCJKText', () => {
|
||
test('segments Chinese text into words', () => {
|
||
const words = segmentCJKText('你好世界');
|
||
expect(words.length).toBeGreaterThan(0);
|
||
expect(words.join('')).toContain('你好');
|
||
});
|
||
|
||
test('segments Japanese text', () => {
|
||
const words = segmentCJKText('こんにちは');
|
||
expect(words.length).toBeGreaterThan(0);
|
||
});
|
||
|
||
test('handles text with punctuation', () => {
|
||
const words = segmentCJKText('你好。世界!');
|
||
expect(words.length).toBeGreaterThan(0);
|
||
});
|
||
|
||
test('handles empty text', () => {
|
||
const words = segmentCJKText('');
|
||
expect(words).toEqual([]);
|
||
});
|
||
|
||
test('handles single character', () => {
|
||
const words = segmentCJKText('你');
|
||
expect(words.length).toBeGreaterThanOrEqual(1);
|
||
});
|
||
|
||
test('attaches trailing CJK punctuation', () => {
|
||
const words = segmentCJKText('你好!');
|
||
// The punctuation should be attached to a word
|
||
const hasWordWithPunct = words.some((w) => w.includes('!'));
|
||
expect(hasWordWithPunct).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('splitTextIntoWords', () => {
|
||
test('splits English text by spaces', () => {
|
||
const words = splitTextIntoWords('Hello World');
|
||
expect(words).toEqual(['Hello', 'World']);
|
||
});
|
||
|
||
test('splits multi-word English text', () => {
|
||
const words = splitTextIntoWords('The quick brown fox');
|
||
expect(words).toEqual(['The', 'quick', 'brown', 'fox']);
|
||
});
|
||
|
||
test('filters empty words', () => {
|
||
const words = splitTextIntoWords(' Hello World ');
|
||
expect(words.every((w) => w.trim().length > 0)).toBe(true);
|
||
});
|
||
|
||
test('handles CJK text', () => {
|
||
const words = splitTextIntoWords('你好世界');
|
||
expect(words.length).toBeGreaterThan(0);
|
||
});
|
||
|
||
test('handles mixed CJK and Latin text', () => {
|
||
const words = splitTextIntoWords('Hello 你好 World');
|
||
expect(words.length).toBeGreaterThan(0);
|
||
// Should contain both CJK and Latin segments
|
||
});
|
||
|
||
test('handles empty string', () => {
|
||
const words = splitTextIntoWords('');
|
||
expect(words).toEqual([]);
|
||
});
|
||
|
||
test('handles CJK text with punctuation', () => {
|
||
const words = splitTextIntoWords('你好。世界!');
|
||
expect(words.length).toBeGreaterThan(0);
|
||
});
|
||
|
||
test('handles CJK followed immediately by non-CJK', () => {
|
||
const words = splitTextIntoWords('你好Hello');
|
||
expect(words.length).toBeGreaterThanOrEqual(1);
|
||
});
|
||
|
||
test('handles standalone CJK punctuation at start', () => {
|
||
const words = splitTextIntoWords('。Hello');
|
||
expect(words.length).toBeGreaterThan(0);
|
||
});
|
||
|
||
test('handles non-CJK text followed by CJK punctuation', () => {
|
||
const words = splitTextIntoWords('Hello。');
|
||
expect(words.length).toBeGreaterThan(0);
|
||
});
|
||
|
||
test('handles whitespace between CJK segments', () => {
|
||
const words = splitTextIntoWords('你好 世界');
|
||
expect(words.length).toBeGreaterThan(0);
|
||
});
|
||
});
|
||
|
||
describe('getHyphenParts', () => {
|
||
test('splits a hyphenated word into two parts with trailing hyphen on first', () => {
|
||
expect(getHyphenParts('well-known')).toEqual(['well-', 'known']);
|
||
});
|
||
|
||
test('splits multiple letter-hyphens keeping trailing hyphen on each non-last part', () => {
|
||
expect(getHyphenParts('one-two-three')).toEqual(['one-', 'two-', 'three']);
|
||
});
|
||
|
||
test('returns word unchanged when no letter-hyphen-letter pattern', () => {
|
||
expect(getHyphenParts('hello')).toEqual(['hello']);
|
||
});
|
||
|
||
test('returns double-hyphen unchanged (em-dash style)', () => {
|
||
expect(getHyphenParts('--')).toEqual(['--']);
|
||
});
|
||
|
||
test('returns lone hyphen unchanged', () => {
|
||
expect(getHyphenParts('-')).toEqual(['-']);
|
||
});
|
||
|
||
test('returns consecutive-hyphen word unchanged', () => {
|
||
expect(getHyphenParts('foo--bar')).toEqual(['foo--bar']);
|
||
});
|
||
|
||
test('returns leading-hyphen word unchanged', () => {
|
||
expect(getHyphenParts('-word')).toEqual(['-word']);
|
||
});
|
||
|
||
test('returns trailing-hyphen word unchanged', () => {
|
||
expect(getHyphenParts('word-')).toEqual(['word-']);
|
||
});
|
||
});
|
||
});
|