Newer
Older
SmTIAS-Capture / test / providers / gallery_provider_test.dart
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';

/// テスト専用の GalleryProvider 実装.
///
/// プロダクションの GalleryProvider はパスがハードコードされており
/// サブクラスでオーバーライドできないため,同じロジックをテスト用パスで
/// 再実装する.仕様(SPEC_01)に準拠した振る舞いをテストする.
class _GalleryProviderTestable extends ChangeNotifier {
  _GalleryProviderTestable(this._basePath);

  final String _basePath;

  List<File> _images = [];
  bool _isLoading = false;

  List<File> get images => _images;
  bool get isLoading => _isLoading;

  /// Pictures/MiniTIAS/ 内の PNG と .preview.jpg を取得し,新しい順に並べる.
  Future<void> loadImages() async {
    _isLoading = true;
    notifyListeners();

    try {
      final directory = Directory(_basePath);
      if (!await directory.exists()) {
        _images = [];
      } else {
        final files = await directory
            .list()
            .where((entity) {
              final lower = entity.path.toLowerCase();
              return entity is File &&
                  (lower.endsWith('.png') || lower.endsWith('.preview.jpg'));
            })
            .cast<File>()
            .toList();

        // 更新日時の新しい順にソート
        files.sort((a, b) {
          final aStat = a.statSync();
          final bStat = b.statSync();
          return bStat.modified.compareTo(aStat.modified);
        });

        _images = files;
      }
    } catch (e) {
      debugPrint('画像一覧の取得に失敗: $e');
      _images = [];
    }

    _isLoading = false;
    notifyListeners();
  }

  /// 指定したファイルを削除し,一覧から除去する.
  ///
  /// QM プレビュー (*.preview.jpg) を削除する場合,同じ baseName の
  /// .dng ファイルと .meta.json ファイルも連動して削除する.
  /// .png ファイルは単独削除(後方互換).
  Future<bool> deleteImage(File file) async {
    final path = file.path;
    final lower = path.toLowerCase();

    final filesToDelete = <File>[file];
    if (lower.endsWith('.preview.jpg')) {
      final base = path.substring(0, path.length - '.preview.jpg'.length);
      filesToDelete.add(File('$base.dng'));
      filesToDelete.add(File('$base.meta.json'));
    }

    try {
      for (final f in filesToDelete) {
        if (await f.exists()) {
          await f.delete();
        }
      }
      if (!await file.exists()) {
        _images.remove(file);
        notifyListeners();
        return true;
      }
      return false;
    } catch (e) {
      debugPrint('画像の削除に失敗: $e');
      return false;
    }
  }
}

void main() {
  group('GalleryProvider.loadImages', () {
    late Directory tempDir;
    late _GalleryProviderTestable provider;

    setUp(() async {
      tempDir = await Directory.systemTemp.createTemp(
        'mini_tias_gallery_test_',
      );
      provider = _GalleryProviderTestable(tempDir.path);
    });

    tearDown(() async {
      if (await tempDir.exists()) {
        await tempDir.delete(recursive: true);
      }
    });

    test('.png のみある場合,全件取得する', () async {
      await File('${tempDir.path}/MiniTIAS_20260101_120000.png').create();
      await File('${tempDir.path}/MiniTIAS_20260101_120001.png').create();

      await provider.loadImages();

      final names = provider.images
          .map((f) => f.path.split(RegExp(r'[\\/]')).last)
          .toList();
      expect(names, contains('MiniTIAS_20260101_120000.png'));
      expect(names, contains('MiniTIAS_20260101_120001.png'));
      expect(provider.images, hasLength(2));
    });

    test('.preview.jpg のみある場合,全件取得する', () async {
      await File(
        '${tempDir.path}/MiniTIAS_QM_20260101_120000.preview.jpg',
      ).create();
      await File(
        '${tempDir.path}/MiniTIAS_QM_20260101_120001.preview.jpg',
      ).create();

      await provider.loadImages();

      final names = provider.images
          .map((f) => f.path.split(RegExp(r'[\\/]')).last)
          .toList();
      expect(names, contains('MiniTIAS_QM_20260101_120000.preview.jpg'));
      expect(names, contains('MiniTIAS_QM_20260101_120001.preview.jpg'));
      expect(provider.images, hasLength(2));
    });

    test('.png と .preview.jpg が混在する場合,両方取得する', () async {
      await File('${tempDir.path}/MiniTIAS_20260101_120000.png').create();
      await File(
        '${tempDir.path}/MiniTIAS_QM_20260101_120001.preview.jpg',
      ).create();

      await provider.loadImages();

      final names = provider.images
          .map((f) => f.path.split(RegExp(r'[\\/]')).last)
          .toList();
      expect(names, contains('MiniTIAS_20260101_120000.png'));
      expect(names, contains('MiniTIAS_QM_20260101_120001.preview.jpg'));
      expect(provider.images, hasLength(2));
    });

    test('.dng や .meta.json は取得しない', () async {
      await File('${tempDir.path}/MiniTIAS_QM_20260101_120000.dng').create();
      await File(
        '${tempDir.path}/MiniTIAS_QM_20260101_120000.meta.json',
      ).create();

      await provider.loadImages();

      expect(provider.images, isEmpty);
    });

    test('通常 .jpg(.preview.jpg 以外)は取得しない', () async {
      // .preview.jpg のみが対象,単純な .jpg は除外される
      await File('${tempDir.path}/photo.jpg').create();
      await File('${tempDir.path}/MiniTIAS_20260101_120000.jpg').create();
      // 比較用に .preview.jpg を 1 件追加
      await File(
        '${tempDir.path}/MiniTIAS_QM_20260101_120001.preview.jpg',
      ).create();

      await provider.loadImages();

      final names = provider.images
          .map((f) => f.path.split(RegExp(r'[\\/]')).last)
          .toList();
      expect(names, isNot(contains('photo.jpg')));
      expect(names, isNot(contains('MiniTIAS_20260101_120000.jpg')));
      expect(names, contains('MiniTIAS_QM_20260101_120001.preview.jpg'));
      expect(provider.images, hasLength(1));
    });

    test('ディレクトリが存在しない場合,空リストを返す', () async {
      final nonExistentPath = '${tempDir.path}/nonexistent';
      final providerNonExistent = _GalleryProviderTestable(nonExistentPath);

      await providerNonExistent.loadImages();

      expect(providerNonExistent.images, isEmpty);
    });

    test('ファイルが新しい順(更新日時降順)にソートされる', () async {
      // 先に古いファイルを作成し,十分な時間差を設けてから新しいファイルを作成する
      final olderPath = '${tempDir.path}/MiniTIAS_20260101_120000.png';
      final newerPath =
          '${tempDir.path}/MiniTIAS_QM_20260101_120001.preview.jpg';
      await File(olderPath).create();
      // Windows NTFS の mtime 精度 (100ns) より十分大きい 1 秒以上待機
      await Future<void>.delayed(const Duration(seconds: 2));
      await File(newerPath).create();

      await provider.loadImages();

      expect(provider.images, hasLength(2));
      // 新しい順なので newer が先頭
      final firstBasename = provider.images.first.path
          .split(RegExp(r'[\\/]'))
          .last;
      expect(firstBasename, 'MiniTIAS_QM_20260101_120001.preview.jpg');
    });
  });

  group('GalleryProvider.deleteImage', () {
    late Directory tempDir;
    late _GalleryProviderTestable provider;

    setUp(() async {
      tempDir = await Directory.systemTemp.createTemp('mini_tias_del_test_');
      provider = _GalleryProviderTestable(tempDir.path);
    });

    tearDown(() async {
      if (await tempDir.exists()) {
        await tempDir.delete(recursive: true);
      }
    });

    test('.preview.jpg を削除すると同 basename の .dng も削除される', () async {
      final previewPath =
          '${tempDir.path}/MiniTIAS_QM_20260101_120000.preview.jpg';
      final dngFile = File('${tempDir.path}/MiniTIAS_QM_20260101_120000.dng');
      final metaFile = File(
        '${tempDir.path}/MiniTIAS_QM_20260101_120000.meta.json',
      );
      await File(previewPath).create();
      await dngFile.create();
      await metaFile.create();

      await provider.loadImages();
      // loadImages で取得した同一インスタンスを使って削除する
      final fileToDelete = provider.images.first;
      final result = await provider.deleteImage(fileToDelete);

      expect(result, isTrue);
      expect(await dngFile.exists(), isFalse);
    });

    test('.preview.jpg を削除すると同 basename の .meta.json も削除される', () async {
      final previewPath =
          '${tempDir.path}/MiniTIAS_QM_20260101_120000.preview.jpg';
      final dngFile = File('${tempDir.path}/MiniTIAS_QM_20260101_120000.dng');
      final metaFile = File(
        '${tempDir.path}/MiniTIAS_QM_20260101_120000.meta.json',
      );
      await File(previewPath).create();
      await dngFile.create();
      await metaFile.create();

      await provider.loadImages();
      final fileToDelete = provider.images.first;
      final result = await provider.deleteImage(fileToDelete);

      expect(result, isTrue);
      expect(await metaFile.exists(), isFalse);
    });

    test('.png を削除しても他のファイルは削除されない', () async {
      final pngPath = '${tempDir.path}/MiniTIAS_20260101_120000.png';
      // 同名の .dng が仮に存在しても,連動削除の対象にならない
      final dngFile = File('${tempDir.path}/MiniTIAS_20260101_120000.dng');
      await File(pngPath).create();
      await dngFile.create();

      await provider.loadImages();
      final fileToDelete = provider.images.first;
      final result = await provider.deleteImage(fileToDelete);

      expect(result, isTrue);
      // .dng は削除されていない
      expect(await dngFile.exists(), isTrue);
    });

    test('連動削除対象の .dng が最初から存在しない場合でもエラーにならず .preview.jpg の削除は成功する', () async {
      final previewPath =
          '${tempDir.path}/MiniTIAS_QM_20260101_120000.preview.jpg';
      final metaFile = File(
        '${tempDir.path}/MiniTIAS_QM_20260101_120000.meta.json',
      );
      // .dng は作成しない
      await File(previewPath).create();
      await metaFile.create();

      await provider.loadImages();
      final fileToDelete = provider.images.first;
      // .dng が存在しなくても例外にならない
      final result = await provider.deleteImage(fileToDelete);

      expect(result, isTrue);
      expect(await File(previewPath).exists(), isFalse);
    });

    test('削除成功後,images リストから対象ファイルが除去される(.preview.jpg)', () async {
      final previewPath =
          '${tempDir.path}/MiniTIAS_QM_20260101_120000.preview.jpg';
      await File(previewPath).create();

      await provider.loadImages();
      expect(provider.images, hasLength(1));

      // loadImages で保持している同一インスタンスを使って削除する
      final fileToDelete = provider.images.first;
      final result = await provider.deleteImage(fileToDelete);

      expect(result, isTrue);
      expect(provider.images, isEmpty);
    });

    test('削除成功後,images リストから対象ファイルが除去される(.png)', () async {
      final pngPath = '${tempDir.path}/MiniTIAS_20260101_120000.png';
      await File(pngPath).create();

      await provider.loadImages();
      expect(provider.images, hasLength(1));

      final fileToDelete = provider.images.first;
      final result = await provider.deleteImage(fileToDelete);

      expect(result, isTrue);
      expect(provider.images, isEmpty);
    });
  });
}