fix(convert): simplify output names and harden writes

This commit is contained in:
zarzet
2026-07-22 15:56:07 +07:00
parent 87376ee73b
commit f0988070bf
9 changed files with 553 additions and 152 deletions
+2 -4
View File
@@ -47,20 +47,18 @@ void main() {
});
group('kept conversion output identity', () {
test('uses a distinct filename when the original is retained', () {
test('keeps the original base name and only changes the extension', () {
expect(
convertedOutputFileName(
originalFileName: 'Track.flac',
targetFormat: 'FLAC',
keepOriginal: true,
),
'Track_converted.flac',
'Track.flac',
);
expect(
convertedOutputFileName(
originalFileName: 'Track.flac',
targetFormat: 'MP3',
keepOriginal: false,
),
'Track.mp3',
);
+94
View File
@@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:spotiflac_android/l10n/app_localizations.dart';
import 'package:spotiflac_android/models/unified_library_item.dart';
import 'package:spotiflac_android/services/batch_track_actions.dart';
void main() {
testWidgets(
'batch conversion keeps a valid context after its launcher is removed',
(tester) async {
await tester.pumpWidget(
const ProviderScope(
child: MaterialApp(
localizationsDelegates: [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
home: _BatchConvertHarness(),
),
),
);
await tester.tap(find.byKey(const Key('open-batch-convert')));
await tester.pumpAndSettle();
final convertButton = find.widgetWithText(
FilledButton,
'Convert 1 track',
);
await tester.scrollUntilVisible(
convertButton,
300,
scrollable: find.byType(Scrollable).last,
);
await tester.tap(convertButton);
await tester.pumpAndSettle();
expect(find.byType(AlertDialog), findsOneWidget);
expect(
find.textContaining('Original files will be deleted'),
findsOneWidget,
);
expect(tester.takeException(), isNull);
},
);
}
class _BatchConvertHarness extends ConsumerStatefulWidget {
const _BatchConvertHarness();
@override
ConsumerState<_BatchConvertHarness> createState() =>
_BatchConvertHarnessState();
}
class _BatchConvertHarnessState extends ConsumerState<_BatchConvertHarness> {
bool _showLauncher = true;
@override
Widget build(BuildContext context) {
return Scaffold(
body: _showLauncher
? Builder(
builder: (launcherContext) => FilledButton(
key: const Key('open-batch-convert'),
onPressed: () => showBatchConvertSheet(
launcherContext,
ref,
[
UnifiedLibraryItem(
id: 'track-1',
trackName: 'Track',
artistName: 'Artist',
albumName: 'Album',
filePath: '/music/track.flac',
addedAt: DateTime(2026),
source: LibraryItemSource.local,
),
],
onExitSelectionMode: () {},
onSheetOpen: () => setState(() => _showLauncher = false),
),
child: const Text('Open'),
),
)
: const SizedBox.shrink(),
);
}
}
+70
View File
@@ -0,0 +1,70 @@
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:spotiflac_android/services/conversion_library_service.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
const channel = MethodChannel('com.zarz.spotiflac/backend');
tearDown(() async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, null);
});
test(
'SAF conversion removes _converted and retries a transient write',
() async {
var attempts = 0;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (call) async {
expect(call.method, 'safCreateUniqueFromPath');
expect((call.arguments as Map)['file_name'], 'Song.mp3');
attempts++;
if (attempts == 1) {
throw PlatformException(code: 'temporary_io');
}
return jsonEncode({
'uri': 'content://music/Song%20(2).mp3',
'file_name': 'Song (2).mp3',
});
});
final published = await ConversionLibraryService.publishSafConversion(
treeUri: 'content://music/tree/root',
relativeDir: 'Album',
originalFileName: 'Song.flac',
targetFormat: 'mp3',
sourcePath: '/tmp/Song.mp3',
keepOriginal: true,
);
expect(attempts, 2);
expect(published?.fileName, 'Song (2).mp3');
},
);
test(
'same-format replacement reuses the original SAF document name',
() async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (call) async {
expect(call.method, 'safCreateFromPath');
expect((call.arguments as Map)['file_name'], 'Song.flac');
return 'content://music/Song.flac';
});
final published = await ConversionLibraryService.publishSafConversion(
treeUri: 'content://music/tree/root',
relativeDir: 'Album',
originalFileName: 'Song.flac',
targetFormat: 'flac',
sourcePath: '/tmp/Song.flac',
keepOriginal: false,
);
expect(published?.fileName, 'Song.flac');
},
);
}