import 'dart:io'; import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:mini_tias/services/file_service.dart'; void main() { group('FileService.generateFileNameSync', () { test('同秒のファイルが存在しない場合,サフィックスなしのファイル名を返す', () { final result = FileService.generateFileNameSync('20260404_120000', []); expect(result, 'MiniTIAS_20260404_120000.png'); }); test('同秒のファイルが存在する場合,_1 サフィックスを付与する', () { final result = FileService.generateFileNameSync('20260404_120000', [ 'MiniTIAS_20260404_120000.png', ]); expect(result, 'MiniTIAS_20260404_120000_1.png'); }); test('_1 も存在する場合,_2 サフィックスを付与する', () { final result = FileService.generateFileNameSync('20260404_120000', [ 'MiniTIAS_20260404_120000.png', 'MiniTIAS_20260404_120000_1.png', ]); expect(result, 'MiniTIAS_20260404_120000_2.png'); }); test('異なるタイムスタンプのファイルが存在しても影響しない', () { final result = FileService.generateFileNameSync('20260404_120000', [ 'MiniTIAS_20260404_120001.png', ]); expect(result, 'MiniTIAS_20260404_120000.png'); }); // --- 追加テスト: ファイル命名規則の詳細検証 --- test('返り値が MiniTIAS_ プレフィックスで始まる', () { final result = FileService.generateFileNameSync('20260404_120000', []); expect(result, startsWith('MiniTIAS_')); }); test('返り値が .png 拡張子で終わる', () { final result = FileService.generateFileNameSync('20260404_120000', []); expect(result, endsWith('.png')); }); test('連番が連続して欠けている場合(_1 が欠落),_2 ではなく _1 を返す', () { // 仕様: suffix=1 から順に探すため _1 が存在しなければ _1 を返す final result = FileService.generateFileNameSync('20260404_120000', [ 'MiniTIAS_20260404_120000.png', 'MiniTIAS_20260404_120000_2.png', // _1 が存在しない状態 ]); expect(result, 'MiniTIAS_20260404_120000_1.png'); }); test('連番が多数存在する場合,次の番号を付与する', () { final existing = [ 'MiniTIAS_20260404_120000.png', 'MiniTIAS_20260404_120000_1.png', 'MiniTIAS_20260404_120000_2.png', 'MiniTIAS_20260404_120000_3.png', 'MiniTIAS_20260404_120000_4.png', ]; final result = FileService.generateFileNameSync( '20260404_120000', existing, ); expect(result, 'MiniTIAS_20260404_120000_5.png'); }); test('空のタイムスタンプでも形式を維持する', () { final result = FileService.generateFileNameSync('', []); expect(result, 'MiniTIAS_.png'); }); }); group('FileService.saveImage', () { late Directory tempDir; late FileService service; setUp(() async { tempDir = await Directory.systemTemp.createTemp('mini_tias_test_'); service = _FileServiceTestable(tempDir.path); }); tearDown(() async { if (await tempDir.exists()) { await tempDir.delete(recursive: true); } }); test('PNG バイト列を渡すと,ファイルパスを返す', () async { final pngBytes = Uint8List.fromList([0, 1, 2, 3]); final path = await service.saveImage(pngBytes); expect(path, isNotEmpty); }); test('PNG バイト列を渡すと,.png 拡張子のファイルが生成される', () async { final pngBytes = Uint8List.fromList([0, 1, 2, 3]); final path = await service.saveImage(pngBytes); expect(path, endsWith('.png')); }); test('PNG バイト列を渡すと,ファイル名が MiniTIAS_ プレフィックスを持つ', () async { final pngBytes = Uint8List.fromList([0, 1, 2, 3]); final path = await service.saveImage(pngBytes); final fileName = path.split('/').last; expect(fileName, startsWith('MiniTIAS_')); }); test('渡したバイト列がそのままファイルに書き込まれる', () async { final pngBytes = Uint8List.fromList([137, 80, 78, 71, 13, 10, 26, 10]); final path = await service.saveImage(pngBytes); final written = await File(path).readAsBytes(); expect(written, equals(pngBytes)); }); test('空のバイト列でもファイルを生成できる', () async { final pngBytes = Uint8List(0); final path = await service.saveImage(pngBytes); expect(await File(path).exists(), isTrue); final written = await File(path).readAsBytes(); expect(written.length, 0); }); test('同秒に 2 回呼び出すと,2 つ目のファイル名に _1 サフィックスが付く', () async { final pngBytes = Uint8List.fromList([0, 1, 2, 3]); // 最初の saveImage でファイルを作成 final path1 = await service.saveImage(pngBytes); // 同秒内に再度呼び出す(ファイルが既に存在する状態を擬似的に作る) // generateFileName はファイルシステムを参照するため,1 ファイル作成後に呼ぶ final path2 = await service.saveImage(pngBytes); final fileName1 = path1.split('/').last; final fileName2 = path2.split('/').last; // 2 つのファイル名は異なるはず // (同秒の場合 _1 サフィックスが付く,異なる秒の場合も名前が異なる) if (fileName1.replaceAll(RegExp(r'_\d+\.png$'), '.png') == fileName2.replaceAll(RegExp(r'_\d+\.png$'), '.png')) { // 同秒の場合: 2 つ目は _1 サフィックスを持つはず expect(fileName2, contains('_1.png')); } else { // 異なる秒の場合: それぞれ独立したタイムスタンプ付きファイル名 expect(fileName1, isNot(equals(fileName2))); } }); test('ディレクトリが存在しない場合でも自動作成してファイルを保存する', () async { final nonExistentSubDir = Directory('${tempDir.path}/sub/dir'); final serviceWithSubDir = _FileServiceTestable(nonExistentSubDir.path); expect(await nonExistentSubDir.exists(), isFalse); final pngBytes = Uint8List.fromList([0, 1, 2, 3]); final path = await serviceWithSubDir.saveImage(pngBytes); expect(await File(path).exists(), isTrue); }); }); group('FileService.directoryPath', () { test('保存先ディレクトリのパスを返す', () { final service = FileService(); expect(service.directoryPath, '/storage/emulated/0/Pictures/MiniTIAS'); }); }); } /// テスト用: 保存先ディレクトリをオーバーライドできる FileService サブクラス. class _FileServiceTestable extends FileService { _FileServiceTestable(this._testBasePath); final String _testBasePath; @override String get directoryPath => _testBasePath; @override Future saveImage(Uint8List pngBytes) async { final directory = Directory(_testBasePath); if (!await directory.exists()) { await directory.create(recursive: true); } final fileName = await _generateFileNameForTest(); final filePath = '$_testBasePath/$fileName'; await File(filePath).writeAsBytes(pngBytes); return filePath; } Future _generateFileNameForTest() async { final now = DateTime.now(); final timestamp = '${now.year}' '${now.month.toString().padLeft(2, '0')}' '${now.day.toString().padLeft(2, '0')}' '_' '${now.hour.toString().padLeft(2, '0')}' '${now.minute.toString().padLeft(2, '0')}' '${now.second.toString().padLeft(2, '0')}'; final baseName = 'MiniTIAS_$timestamp'; final candidate = '$baseName.png'; if (!await File('$_testBasePath/$candidate').exists()) { return candidate; } var suffix = 1; while (await File('$_testBasePath/${baseName}_$suffix.png').exists()) { suffix++; } return '${baseName}_$suffix.png'; } }