import 'package:flutter_test/flutter_test.dart';
import 'package:reelbox/media/subtitle_webvtt.dart';
void main() {
test('WebVTT drops metadata, STYLE, REGION, NOTE, cue settings and unsafe markup', () {
final output = SubtitleWebVtt.convert('''WEBVTT synthetic header
X-TIMESTAMP-MAP=LOCAL:00:00:00.000,MPEGTS:0
STYLE
::cue { background-image: url(https://unsafe.invalid/pixel) }
REGION
id:remote
NOTE private note
https://unsafe.invalid/note
cue-id
00:01.250 --> 00:03.000 region:remote align:start
سلام
& safe
''');
expect(
output,
'WEBVTT\n\n00:00:01.250 --> 00:00:03.000\nسلام & safe\n\n',
);
expect(output, isNot(contains('unsafe.invalid')));
});
test('SRT cue payload is sanitized, not simply relabelled', () {
expect(
SubtitleWebVtt.convert(
'1\n00:00:00,000 --> 00:00:01,500\nHello {\\\\an8}friend',
),
'WEBVTT\n\n00:00:00.000 --> 00:00:01.500\nHello friend\n\n',
);
});
for (final timing in [
'00:61:00,000 --> 01:02:00,000',
'00:00:61,000 --> 00:02:00,000',
'00:00:03,000 --> 00:00:01,000',
'00:00:01,000 --> 00:00:01,000',
'-00:00:01,000 --> 00:00:02,000',
]) {
test('rejects invalid cue timing $timing', () {
expect(
() => SubtitleWebVtt.convert('1\n$timing\nSynthetic'),
throwsFormatException,
);
});
}
test('rejects decreasing cue starts but permits overlapping cues', () {
expect(
() => SubtitleWebVtt.convert(
'1\n00:00:02,000 --> 00:00:04,000\na\n\n2\n00:00:01,000 --> 00:00:03,000\nb',
),
throwsFormatException,
);
expect(
SubtitleWebVtt.convert(
'1\n00:00:01,000 --> 00:00:04,000\na\n\n2\n00:00:02,000 --> 00:00:03,000\nb',
),
contains('00:00:02.000 --> 00:00:03.000'),
);
});
for (final unsupported in [
'',
'TTML is not supported',
'plain arbitrary text',
'WEBVTT\n\nSTYLE\n::cue {color:red}',
]) {
test('rejects empty/unsupported input honestly: $unsupported', () {
expect(() => SubtitleWebVtt.convert(unsupported), throwsFormatException);
});
}
test('rejects invalid ASS time and drawing-mode dialogue', () {
for (final body in [
'Dialogue: 0,0:99:01.00,0:99:02.00,text',
r'Dialogue: 0,0:00:01.00,0:00:02.00,{\p1}m 1 1 l 2 2',
]) {
expect(
() => SubtitleWebVtt.convert(
'[Script Info]\n[Events]\nFormat: Layer, Start, End, Text\n$body',
),
throwsA(anyOf(isA(), isA())),
);
}
});
for (final format in ['ASS', 'SSA']) {
test('synthetic $format uses Events format and dialogue, never styles/fonts', () {
final fields = format == 'ASS'
? 'Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text'
: 'Marked, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text';
final output = SubtitleWebVtt.convert('''[Script Info]
Title: Synthetic only
[V4+ Styles]
Format: Name, Fontname
Style: Default,https://unsafe.invalid/font
[Events]
Format: $fields
Dialogue: 0,0:00:01.25,0:00:03.00,Default,,0,0,0,,{\\b1}سلام\\Nworld, again{\\b0}
''');
expect(
output,
'WEBVTT\n\n00:00:01.250 --> 00:00:03.000\nسلام\nworld, again\n\n',
);
expect(output, isNot(contains('unsafe.invalid')));
});
}
test('synthetic SRT becomes actual WebVTT while preserving Persian text', () {
expect(
SubtitleWebVtt.convert(
'\ufeff1\r\n00:00:01,250 --> 00:00:03,000\r\nسلام دنیا\r\n',
),
'WEBVTT\n\n00:00:01.250 --> 00:00:03.000\nسلام دنیا\n\n',
);
});
}