Initial commit

This commit is contained in:
admin1
2026-07-14 11:11:36 +08:00
commit 656499cf94
604 changed files with 119518 additions and 0 deletions
@@ -0,0 +1,204 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:smplayer/core/media/embedded_subtitle_cues.dart';
import 'package:smplayer/core/media/external_subtitle_loader.dart';
void main() {
group('ExternalSubtitleLoader.parseSubtitleContent — SRT', () {
const srt = '''
1
00:00:01,000 --> 00:00:02,500
第一句
第二行
2
00:00:04,000 --> 00:00:06,000
<i>Second cue</i>
''';
test('解析编号块、时间轴与多行文本,剥 HTML 标签', () {
final cues = ExternalSubtitleLoader.parseSubtitleContent(srt);
expect(cues, hasLength(2));
expect(cues[0].start, const Duration(seconds: 1));
expect(cues[0].end, const Duration(milliseconds: 2500));
expect(cues[0].text, '第一句\n第二行');
expect(cues[1].text, 'Second cue');
});
test('容忍 CRLF 换行与点号毫秒分隔', () {
const crlfSrt = '1\r\n00:00:01.000 --> 00:00:02.000\r\nhello\r\n\r\n';
final cues = ExternalSubtitleLoader.parseSubtitleContent(crlfSrt);
expect(cues, hasLength(1));
expect(cues.single.text, 'hello');
});
test('无时间轴的垃圾块被跳过', () {
const noisy = '''
garbage block
1
00:00:01,000 --> 00:00:02,000
ok
''';
final cues = ExternalSubtitleLoader.parseSubtitleContent(noisy);
expect(cues, hasLength(1));
expect(cues.single.text, 'ok');
});
test('end < start 的非法 cue 被丢弃', () {
const invalid = '''
1
00:00:05,000 --> 00:00:02,000
bad
2
00:00:06,000 --> 00:00:07,000
good
''';
final cues = ExternalSubtitleLoader.parseSubtitleContent(invalid);
expect(cues, hasLength(1));
expect(cues.single.text, 'good');
});
});
group('ExternalSubtitleLoader.parseSubtitleContent — VTT', () {
const vtt = '''
WEBVTT
NOTE this is a comment
cue-1
00:01.000 --> 00:02.000
Hello <b>world</b>
00:00:03.000 --> 00:00:04.500
第二条
''';
test('识别 WEBVTT 头,支持省略小时位与 cue 标识行', () {
final cues = ExternalSubtitleLoader.parseSubtitleContent(vtt);
expect(cues, hasLength(2));
expect(cues[0].start, const Duration(seconds: 1));
expect(cues[0].text, 'Hello world');
expect(cues[1].start, const Duration(seconds: 3));
expect(cues[1].end, const Duration(milliseconds: 4500));
expect(cues[1].text, '第二条');
});
});
group('ExternalSubtitleLoader.parseSubtitleContent — ASS 降级纯文本', () {
const ass = '''
[Script Info]
Title: Test
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:01.00,0:00:02.50,Default,,0,0,0,,{\\an8\\fs20}你好,世界\\N第二行
Dialogue: 0,0:00:04.00,0:00:05.00,Sign,,0,0,0,,{\\pos(1,2)}m 0 0 l 100 0
Dialogue: 0,0:00:06.00,0:00:07.00,Default,,0,0,0,,plain text
''';
test('剥样式覆盖标签、\\N 转行、Text 内逗号保留', () {
final cues = ExternalSubtitleLoader.parseSubtitleContent(ass);
final first = cues.first;
expect(first.start, const Duration(seconds: 1));
expect(first.end, const Duration(milliseconds: 2500));
expect(first.text, '你好,世界\n第二行');
expect(first.text, isNot(contains('an8')));
expect(cues.last.text, 'plain text');
});
test('自定义 Format 字段顺序按声明解析', () {
const customFormat = '''
[Events]
Format: Start, End, Text
Dialogue: 0:00:01.00,0:00:02.00,custom order
''';
final cues = ExternalSubtitleLoader.parseSubtitleContent(customFormat);
expect(cues, hasLength(1));
expect(cues.single.start, const Duration(seconds: 1));
expect(cues.single.text, 'custom order');
});
test('非法时间戳的 Dialogue 行被跳过', () {
const invalidTime = '''
[Events]
Dialogue: 0,bad,0:00:02.00,Default,,0,0,0,,skip me
Dialogue: 0,0:00:03.00,0:00:04.00,Default,,0,0,0,,keep me
''';
final cues = ExternalSubtitleLoader.parseSubtitleContent(invalidTime);
expect(cues, hasLength(1));
expect(cues.single.text, 'keep me');
});
});
group('parseSubtitleContent — 容错', () {
test('空内容/纯垃圾返回空列表而非抛异常', () {
expect(ExternalSubtitleLoader.parseSubtitleContent(''), isEmpty);
expect(
ExternalSubtitleLoader.parseSubtitleContent('random garbage\nfoo bar'),
isEmpty,
);
});
});
group('SubtitleCueSource.textAt', () {
SubtitleCueSource sourceFromSrt() => SubtitleCueSource(
ExternalSubtitleLoader.parseSubtitleContent('''
1
00:00:01,000 --> 00:00:03,000
first
2
00:00:02,000 --> 00:00:04,000
overlap
3
00:00:10,000 --> 00:00:12,000
later
'''),
);
test('位置命中单条 cue', () {
final source = sourceFromSrt();
expect(source.textAt(const Duration(milliseconds: 1500)), ['first']);
expect(source.textAt(const Duration(seconds: 11)), ['later']);
});
test('重叠 cue 全部返回、按 start 顺序', () {
final source = sourceFromSrt();
expect(source.textAt(const Duration(milliseconds: 2500)), [
'first',
'overlap',
]);
});
test('二分跳过历史 cue 时仍保留跨越多个短 cue 的长字幕', () {
final source = SubtitleCueSource(const [
TimedSubtitle(
start: Duration(seconds: 1),
end: Duration(seconds: 20),
text: 'long',
),
TimedSubtitle(
start: Duration(seconds: 2),
end: Duration(seconds: 3),
text: 'expired',
),
TimedSubtitle(
start: Duration(seconds: 10),
end: Duration(seconds: 12),
text: 'current',
),
]);
expect(source.textAt(const Duration(seconds: 11)), ['long', 'current']);
});
test('无命中返回空列表', () {
final source = sourceFromSrt();
expect(source.textAt(Duration.zero), isEmpty);
expect(source.textAt(const Duration(seconds: 5)), isEmpty);
expect(source.textAt(const Duration(seconds: 20)), isEmpty);
});
});
}