Files
SpotiFLAC-Mobile/test/native_lyrics_parser_test.dart
T
zarzet 514e229fc8 perf(metadata): parse lyrics and encode ID3 artwork in native workers
Port LRC, enhanced LRC, and TTML parsing plus ID3v2.3 lyrics writing and picture encoding. Keep legacy contracts and deep XML fallback; guard asynchronous player updates against stale track results.
2026-10-06 23:28:20 +07:00

127 lines
4.2 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:spotiflac_android/utils/lyrics_parser.dart';
Map<String, dynamic> _word(LyricWord value) => {
'timeMs': value.time.inMilliseconds,
'endMs': value.end?.inMilliseconds,
'text': value.text,
};
Map<String, dynamic> _dto(ParsedLyrics value) => {
'synced': value.synced,
'wordSynced': value.wordSynced,
'plainText': value.plainText,
'writers': value.writers,
'provider': value.provider,
'lines': [
for (final line in value.lines)
{
'timeMs': line.time.inMilliseconds,
'endMs': line.end?.inMilliseconds,
'text': line.text,
'words': line.words.map(_word).toList(),
'romanization': line.romanization,
'romanizationWords': line.romanizationWords.map(_word).toList(),
'translation': line.translation,
'voice': line.voice == null
? null
: {
'id': line.voice!.id,
'index': line.voice!.index,
'isGroup': line.voice!.isGroup,
},
'isBackground': line.isBackground,
'vocalGroup': line.vocalGroup,
},
],
};
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final fixtures =
(jsonDecode(
File(
'test/fixtures/native_lyrics_parsers.json',
).readAsStringSync(),
)
as List)
.cast<Map<String, dynamic>>();
const channel = MethodChannel('com.zarz.spotiflac/backend');
final messenger =
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
for (final fixture in fixtures) {
test('native DTO and fallback preserve ${fixture['name']}', () async {
final request = fixture['request'] as Map<String, dynamic>;
final raw = request['raw'] as String;
final expected = fixture['expected'] as Map;
expect(_dto(LyricsParser.parse(raw)), expected);
// With no native host, the production async caller uses its isolated
// compatibility parser and preserves the same public models.
expect(_dto(await LyricsParser.parseAsyncNative(raw)), expected);
var calls = 0;
messenger.setMockMethodCallHandler(channel, (call) async {
expect(call.method, 'runNativeDataJob');
final args = call.arguments as Map;
final request = jsonDecode(args['request_json'] as String) as Map;
expect(request['operation'], 'parse_lyrics');
expect(args['bytes'] as Uint8List, utf8.encode(raw));
calls++;
return jsonEncode(expected);
});
try {
expect(_dto(await LyricsParser.parseAsyncNative(raw)), expected);
expect(calls, raw.isEmpty ? 0 : 1);
} finally {
messenger.setMockMethodCallHandler(channel, null);
}
});
}
test('deep valid TTML uses the isolated compatibility parser', () async {
final raw =
'<tt xmlns="http://www.w3.org/ns/ttml"><body><p begin="1s" end="3s">'
'${List.filled(18, '<span>').join()}'
'<span begin="1s">First </span><span begin="2s">second</span>'
'${List.filled(18, '</span>').join()}</p></body></tt>';
final expected = LyricsParser.parse(raw);
expect(expected.lines.single.text, 'First second');
var calls = 0;
messenger.setMockMethodCallHandler(channel, (call) async {
expect(call.method, 'runNativeDataJob');
calls++;
throw PlatformException(
code: 'data_job_error',
message: 'XML nesting exceeds 16 elements',
);
});
try {
expect(_dto(await LyricsParser.parseAsyncNative(raw)), _dto(expected));
expect(calls, 1);
} finally {
messenger.setMockMethodCallHandler(channel, null);
}
});
test('a real native parser failure is not retried as Dart success', () async {
messenger.setMockMethodCallHandler(channel, (_) async {
throw PlatformException(
code: 'data_job_error',
message: 'Invalid lyric document',
);
});
try {
await expectLater(
LyricsParser.parseAsyncNative('[00:01]Line'),
throwsA(isA<PlatformException>()),
);
} finally {
messenger.setMockMethodCallHandler(channel, null);
}
});
}