import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:image/image.dart' as img; import 'package:jj_mkvmaker/core/batch_rename.dart'; import 'package:jj_mkvmaker/core/photo_date.dart'; import 'package:jj_mkvmaker/ui/batch_rename_dialog.dart'; import 'package:path/path.dart' as p; /// X-plore 비교 i: 일괄 이름 바꾸기의 날짜 · 시각 ([D] · [T], 사진은 찍은 때) · 마지막 규칙 기억 void main() { late Directory tmp; setUp(() => tmp = Directory.systemTemp.createTempSync('jj_rendate_')); tearDown(() => tmp.deleteSync(recursive: true)); test('[D] · [T]: 날짜 2026-10-18 · 시각 13-05-09 (":" 는 못 써서 "-"), 날짜가 없으면 빈칸', () { const r = RenameRule(template: '[D] [T] [N]'); expect(r.usesDate, isTrue); expect(r.apply('a.jpg', isDir: false, index: 0, date: DateTime(2026, 10, 18, 13, 5, 9)), '2026-10-18 13-05-09 a.jpg'); expect(const RenameRule(template: '[d]_[C]').apply('a.jpg', isDir: false, index: 0, date: DateTime(2026, 1, 2)), '2026-01-02_01.jpg'); expect(const RenameRule(template: 'x[D]').apply('a.jpg', isDir: false, index: 0), 'x.jpg'); expect(const RenameRule().usesDate, isFalse); }); test('규칙 기억: toJson → fromJson 그대로, 틀린 값은 처음 값으로', () { const r = RenameRule(template: '[D]_[C]', start: 3, step: 2, digits: 4, find: 'a', replace: 'b', ignoreCase: false, caseMode: 'upper', dateSource: 'modified'); expect(RenameRule.fromJson(r.toJson()).toJson(), r.toJson()); final bad = RenameRule.fromJson({'template': '', 'digits': 99, 'caseMode': '??', 'dateSource': 1, 'start': 'x'}); expect((bad.template, bad.digits, bad.caseMode, bad.dateSource, bad.start), ('[N]', 9, 'keep', 'photo', 1)); }); test('사진의 찍은 때 (JPEG Exif DateTimeOriginal) · 없거나 JPEG 이 아니면 null', () async { expect(parseExifDate('2024:05:01 12:34:56'), DateTime(2024, 5, 1, 12, 34, 56)); expect(parseExifDate('0000:00:00 00:00:00'), isNull); final image = img.Image(width: 8, height: 8); image.exif.exifIfd['DateTimeOriginal'] = img.IfdValueAscii('2024:05:01 12:34:56'); final withExif = File(p.join(tmp.path, 'a.jpg'))..writeAsBytesSync(img.encodeJpg(image)); expect(await photoTakenAt(withExif.path), DateTime(2024, 5, 1, 12, 34, 56)); final plain = File(p.join(tmp.path, 'b.jpg'))..writeAsBytesSync(img.encodeJpg(img.Image(width: 8, height: 8))); expect(await photoTakenAt(plain.path), isNull); expect(await photoTakenAt(p.join(tmp.path, 'none.png')), isNull); }); testWidgets('창: 마지막 규칙으로 열리고, [바꾸기] 때의 규칙을 돌려준다 · [D] 는 사진의 찍은 때', (tester) async { tester.view.physicalSize = const Size(1000, 900); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); final image = img.Image(width: 8, height: 8); image.exif.exifIfd['DateTimeOriginal'] = img.IfdValueAscii('2024:05:01 12:34:56'); final photo = File(p.join(tmp.path, 'IMG_1.jpg'))..writeAsBytesSync(img.encodeJpg(image)); RenameRule? used; final nav = GlobalKey(); await tester.pumpWidget(MaterialApp(navigatorKey: nav, home: const Scaffold())); showBatchRename(nav.currentContext!, [photo.path], isDir: (_) => false, initial: const RenameRule(template: '[D]_[N]'), onRun: (r) => used = r); bool ready() => find.text('2024-05-01_IMG_1.jpg').evaluate().isNotEmpty && tester.widget(find.widgetWithText(FilledButton, '바꾸기 (1개)')).onPressed != null; for (var i = 0; i < 60 && !ready(); i++) { await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 30))); await tester.pump(); } expect(find.widgetWithText(TextField, '[D]_[N]'), findsOneWidget, reason: '마지막 규칙으로 열림'); expect(find.text('2024-05-01_IMG_1.jpg'), findsOneWidget); expect(tester.widget(find.widgetWithText(FilledButton, '바꾸기 (1개)')).onPressed, isNotNull, reason: '찍은 때를 다 읽으면 켜짐'); await tester.tap(find.widgetWithText(FilledButton, '바꾸기 (1개)')); for (var i = 0; i < 100 && !File(p.join(tmp.path, '2024-05-01_IMG_1.jpg')).existsSync(); i++) { await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 30))); await tester.pump(); } expect(File(p.join(tmp.path, '2024-05-01_IMG_1.jpg')).existsSync(), isTrue); expect(used?.template, '[D]_[N]'); }); }