Initial commit
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user