import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:jj_mkvmaker/app/settings.dart'; import 'package:jj_mkvmaker/core/file_ops.dart' show formatSize; import 'package:jj_mkvmaker/ui/explorer_page.dart'; import 'package:path/path.dart' as p; import 'support/explorer_harness.dart'; /// E5e: 예전 explorer_test.dart 에서 나눈 탐색기 화면 시험 - 폴더 + 파일 목록 · 스타일 · 선택 모드 · 지우기 실패 · 즐겨찾기 · 고르기 도구 · 길게 누르기 메뉴 · 뒤로 · 앞으로 · 트리 화살표 폭 void main() { useExplorerFixture(); testWidgets('폴더 + 파일 목록 배치 · Windows 탐색기 / Total Commander 스타일', (tester) async { c.settings ..explorer.layout = 'split' ..explorer.style = 'windows'; await open(tester); // 왼쪽 트리는 폴더만, 오른쪽 목록은 고른 폴더 (left) 의 내용 expect(find.text('sub'), findsNWidgets(2)); // 트리 + 목록 expect(find.text('doc.txt'), findsOneWidget); // 목록에만 expect(find.text('수정한 날짜'), findsOneWidget); // Windows 탐색기 열 머리 expect(find.text('TXT 파일'), findsOneWidget); expect(find.text('상위 폴더'), findsOneWidget); // 버튼 줄은 두 창 사이 // 왼쪽 트리에서 sub 를 고르면 오른쪽 목록이 그 폴더로 await act(tester, () => tester.tap(find.text('sub').first)); await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty); expect(find.text('inner.txt'), findsOneWidget); expect(find.text('doc.txt'), findsNothing); // 목록의 ".." 을 누르면 상위 폴더로 await act(tester, () => tester.tap(find.text('..'))); await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty); expect(find.text('doc.txt'), findsOneWidget); // 목록의 폴더를 두 번 누르면 들어간다 await doubleTap(tester, find.text('sub').last); await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty); expect(find.text('inner.txt'), findsOneWidget); // 이 배치에도 복사 · 이동: 길게 눌러 [복사] 로 담고 → 다른 폴더를 골라 [붙여넣기] await tester.longPress(find.text('inner.txt')); await calm(tester); expect(find.byTooltip('다른 창으로 복사'), findsNothing); await tester.tap(find.text('복사').last); await calm(tester); await act(tester, () => tester.tap(find.text('..'))); await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty); // 73 (관리자): 담아 둔 것이 있으면 [붙여넣기] 는 좁아도 [더 보기] 로 가지 않고 줄에 남는다 expect(find.text('더 보기'), findsOneWidget, reason: '이 너비에서는 줄이 넘친다'); expect(find.text('붙여넣기'), findsOneWidget); await act(tester, () => tester.tap(find.text('붙여넣기'))); await calm(tester); await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '복사'))); await settle(tester, () => File(p.join(left, 'inner.txt')).existsSync() && find.byType(LinearProgressIndicator).evaluate().isEmpty); expect(File(p.join(left, 'inner.txt')).readAsStringSync(), 'inner'); expect(File(p.join(left, 'sub', 'inner.txt')).existsSync(), isTrue); // 복사이므로 원본은 그대로 await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty); expect(find.text('inner.txt'), findsOneWidget); // 목록에 바로 보임 // Total Commander: [폴더] · · 확장자 칸 await c.updateSettings((s) => s.explorer.style = 'totalcmd'); await settle(tester, () => find.text('[sub]').evaluate().length == 2); expect(find.text('[sub]'), findsNWidgets(2)); expect(find.text(''), findsOneWidget); expect(find.text('doc'), findsOneWidget); // 확장자를 뺀 이름 expect(find.text('txt'), findsNWidgets(2)); // 확장자 칸 (doc.txt · 붙여넣은 inner.txt) expect(find.text('확장자'), findsOneWidget); // 알림 (복사 끝 등) 이 시험이 끝난 뒤에 닫히지 않게 정리 ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars(); await calm(tester); }); testWidgets('선택 모드: 파일 누르기 = 선택 · 취소, 폴더 누르기 = 자기 자신과 안의 것 모두 선택 · 다시 누르면 모두 취소', (tester) async { File(p.join(left, 'sub', 'second.txt')).writeAsStringSync('2'); await open(tester); await act(tester, () => tester.tap(find.text('선택').first)); // 가운데 [선택] 버튼 expect(find.byTooltip('표시 (여러 개 고르기)'), findsWidgets); // 동그라미가 보인다 // 파일: 누르면 선택, 다시 누르면 취소 (실행하지 않음) await act(tester, () => tester.tap(find.text('doc.txt'))); expect(find.textContaining('1개 표시함'), findsOneWidget); await act(tester, () => tester.tap(find.text('doc.txt'))); expect(find.textContaining('0개 표시함'), findsNWidgets(2)); // 두 창 모두 (각 창의 선택 수) expect(shell.opened, isEmpty); // 줄 끝 표시 단추: 규칙 4 - 모양은 V (Icons.check), 고른 것은 "표시 지우기" IconButton markOf(String name) => tester.widget(find.descendant( of: find.ancestor(of: find.text(name), matching: find.byType(Material)).first, matching: find.byType(IconButton))); bool markIcon(String name) { expect((markOf(name).icon as Icon).icon, Icons.check, reason: '규칙 4: V 모양'); return markOf(name).tooltip == '표시 지우기'; } // 폴더: 누르면 자기 자신과 안의 것 모두 선택 (펼쳐서 ✔ 로 보여 줌) await act(tester, () => tester.tap(find.text('sub'))); await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty); expect(find.textContaining('1개 표시함'), findsOneWidget); // 폴더 하나로 안의 것까지 expect([markIcon('sub'), markIcon('inner.txt'), markIcon('second.txt')], everyElement(isTrue)); // 안의 것 하나를 누르면 그것만 빠지고 나머지는 고른 채로 await act(tester, () => tester.tap(find.text('inner.txt'))); await settle(tester, () => false, rounds: 5); expect(markIcon('inner.txt'), isFalse); expect(markIcon('second.txt'), isTrue); expect(markIcon('sub'), isFalse); // 하나를 뺀 폴더를 다시 누르면 그 폴더와 안의 것 모두 취소 (관리자 결정 2, 10/22 - 규칙 6 "다시 누르면 전체 취소") await act(tester, () => tester.tap(find.text('sub'))); await settle(tester, () => false, rounds: 5); expect(find.textContaining('0개 표시함'), findsNWidgets(2)); expect([markIcon('sub'), markIcon('inner.txt'), markIcon('second.txt')], everyElement(isFalse)); // [선택 끝] → 표시가 모두 풀리고 표시 단추도 사라진다 (규칙 4: 선택 모드에서만) await act(tester, () => tester.tap(find.text('선택 끝').first)); // 두 창 모두에 있음 expect(find.byTooltip('표시 지우기'), findsNothing); expect(find.byTooltip('표시 (여러 개 고르기)'), findsNothing); // 설정 "늘 보이기" 를 켜면 X-plore 스타일은 선택 모드 밖에서도 보임 await tester.runAsync(() => c.updateSettings((x) => x.explorer.alwaysMarks = true)); await settle(tester, () => false, rounds: 5); expect(find.byTooltip('표시 (여러 개 고르기)'), findsWidgets); }); testWidgets('65: 여러 개를 지울 때 하나가 실패해도 나머지는 지우고, 못 지운 것을 알린다', (tester) async { final locked = File(p.join(left, 'locked.txt'))..writeAsStringSync('l'); final h = locked.openSync(mode: FileMode.append); // Windows: 열려 있는 파일은 지울 수 없다 addTearDown(h.closeSync); await open(tester); await act(tester, () => tester.tap(find.text('선택').first)); await act(tester, () => tester.tap(markOf('doc.txt'))); await act(tester, () => tester.tap(markOf('locked.txt'))); await act(tester, () => tester.tap(markOf('sub'))); await settle(tester, () => find.textContaining('3개 표시함').evaluate().isNotEmpty); await act(tester, () => tester.tap(find.text('삭제').first)); await calm(tester); await tester.tap(find.text('휴지통을 거치지 않고 영구 삭제 (Shift+Delete)')); await calm(tester); await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '영구 삭제'))); await settle(tester, () => find.textContaining('지우지 못했습니다').evaluate().isNotEmpty); expect(find.text('3개 중 1개를 지우지 못했습니다'), findsOneWidget); expect(find.text('locked.txt'), findsWidgets); expect(File(p.join(left, 'doc.txt')).existsSync(), isFalse); expect(Directory(p.join(left, 'sub')).existsSync(), isFalse); expect(locked.existsSync(), isTrue); await tester.tap(find.widgetWithText(FilledButton, '확인')); await calm(tester); }, skip: !Platform.isWindows); testWidgets('73: 즐겨찾기 - 지금 폴더 넣기 (이름 고침) · 한 번에 가기 · 빼기 (폴더는 그대로)', (tester) async { await open(tester); // 창 위의 ★ (왼쪽 창) await act(tester, () => tester.tap(find.byTooltip('즐겨찾기').first)); await calm(tester); expect(find.text('아직 없습니다. 아래에서 지금 폴더를 넣으세요.'), findsOneWidget); await tester.tap(find.text('지금 폴더 추가')); await calm(tester); await tester.enterText(find.byType(TextField).last, '왼쪽 폴더'); await tester.tap(find.widgetWithText(FilledButton, '확인')); await calm(tester); expect([for (final b in c.settings.folderBookmarks) (b.name, b.path)], [('왼쪽 폴더', left)]); expect(find.text('왼쪽 폴더'), findsOneWidget); await tester.tap(find.text('닫기')); await calm(tester); expect(find.byIcon(Icons.star), findsOneWidget, reason: '즐겨찾기한 폴더를 보는 창의 ★ 는 채움'); // 오른쪽 폴더를 즐겨찾기에 두고 왼쪽 창에서 한 번에 가기 await c.updateSettings((x) => x.folderBookmarks = [...x.folderBookmarks, FolderBookmark(name: 'R', path: right)]); await act(tester, () => tester.tap(find.byTooltip('즐겨찾기').first)); await calm(tester); await act(tester, () => tester.tap(find.text('R'))); await settle(tester, () => false, rounds: 20); expect(find.byIcon(Icons.star), findsNWidgets(2), reason: '두 창 모두 즐겨찾기한 폴더 (왼쪽 창이 R 로 감)'); // 빼기: 즐겨찾기에서만 (폴더는 그대로) await act(tester, () => tester.tap(find.byTooltip('즐겨찾기').first)); await calm(tester); await tester.tap(find.byTooltip('즐겨찾기에서 빼기 (폴더는 그대로)').first); await calm(tester); expect([for (final b in c.settings.folderBookmarks) b.name], ['R']); expect(Directory(left).existsSync(), isTrue); await tester.tap(find.text('닫기')); await calm(tester); }); testWidgets('73: Rsync 화면 - "왼쪽 + 오른쪽" 짝을 넣고 한 번에 연다', (tester) async { tester.view.physicalSize = const Size(1600, 900); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); c.settings.copy.rsyncPaths = [left, right]; await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c, rsync: true)))); await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty); await act(tester, () => tester.tap(find.byTooltip('즐겨찾기').first)); await calm(tester); await tester.tap(find.text('지금 짝 추가')); await calm(tester); await tester.tap(find.widgetWithText(FilledButton, '확인')); await calm(tester); final pair = c.settings.folderBookmarks.single; expect((pair.path, pair.right, pair.name), (left, right, 'left ⇄ right')); await tester.tap(find.text('닫기')); await calm(tester); // 반대 짝을 넣어 두고 열면 두 창이 함께 바뀐다 await c.updateSettings((x) => x.folderBookmarks = [...x.folderBookmarks, FolderBookmark(name: '거꾸로', path: right, right: left)]); await act(tester, () => tester.tap(find.byTooltip('즐겨찾기').first)); await calm(tester); await act(tester, () => tester.tap(find.text('거꾸로'))); await settle(tester, () => c.settings.copy.rsyncPaths.length == 2 && c.settings.copy.rsyncPaths[0] == right && c.settings.copy.rsyncPaths[1] == left, rounds: 100); expect(c.settings.copy.rsyncPaths, [right, left]); }); testWidgets('74: 고르기 도구 - 개수 · 합계 크기, 반전, 패턴, 같은 확장자, 범위 (Shift · 길게 누르기)', (tester) async { for (final n in ['a.srt', 'b.srt', 'c.mkv']) { File(p.join(left, n)).writeAsStringSync('12345678'); // 8B } await open(tester); await settle(tester, () => find.text('c.mkv').evaluate().isNotEmpty); await act(tester, () => tester.tap(find.text('선택').first)); await act(tester, () => tester.tap(markOf('a.srt'))); await settle(tester, () => find.textContaining('1개 표시함').evaluate().isNotEmpty); expect(find.text('1개 표시함 · 8B'), findsOneWidget); Future tool(String label) async { await tester.tap(find.text('고르기 도구').first); await calm(tester); await tester.tap(find.textContaining(label).last); await calm(tester); } // 같은 확장자 모두 (.srt) - 마지막으로 누른 a.srt 기준 await tool('같은 확장자 모두'); expect(find.text('2개 표시함 · 16B'), findsOneWidget); // 반전: 이 폴더의 나머지 (doc.txt · c.mkv · sub) await tool('반전'); expect(find.textContaining('3개 표시함'), findsOneWidget); expect(find.textContaining('폴더 1개 (크기 빼고)'), findsOneWidget); // 패턴 await tester.tap(find.text('표시 지우기').first); await tester.pump(); await tool('패턴으로 고르기'); await tester.enterText(find.byType(TextField).last, '*.MKV; doc.*'); await tester.tap(find.widgetWithText(FilledButton, '고르기')); await calm(tester); expect(find.text('2개 표시함 · 13B'), findsOneWidget); // 범위: a.srt 를 표시한 뒤 Shift + c.mkv → a.srt · b.srt · c.mkv (보이는 순서) await tester.tap(find.text('표시 지우기').first); await tester.pump(); await act(tester, () => tester.tap(markOf('a.srt'))); await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); await act(tester, () => tester.tap(find.text('c.mkv').first)); await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft); await tester.pump(); expect(find.text('3개 표시함 · 24B'), findsOneWidget); // 폰: 첫 항목을 표시한 뒤 마지막 항목을 길게 눌러 [여기까지 범위 고르기] await tester.tap(find.text('표시 지우기').first); await tester.pump(); await act(tester, () => tester.tap(markOf('b.srt'))); await tester.longPress(find.text('doc.txt').first); await calm(tester); await tester.tap(find.text('여기까지 범위 고르기')); await calm(tester); expect(find.textContaining('3개 표시함'), findsOneWidget, reason: 'b.srt · c.mkv · doc.txt'); }); testWidgets('77: 길게 누르기 메뉴 - 위 아이콘 줄 · 여럿이면 "n개 항목" (이름 변경은 하나일 때만) · 경로 복사 · 즐겨찾기에 추가 · 삭제는 표시한 것 모두', (tester) async { File(p.join(left, 'b.txt')).writeAsStringSync('b'); String? clip; tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (call) async { if (call.method == 'Clipboard.setData') clip = (call.arguments as Map)['text'] as String?; return null; }); addTearDown(() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, null)); await open(tester); await settle(tester, () => find.text('b.txt').evaluate().isNotEmpty); // 하나: 머리 없음 · 아이콘 줄 · 경로 복사 (실제 경로) await tester.longPress(find.text('doc.txt').first); await calm(tester); expect(find.textContaining('개 항목'), findsNothing); for (final t in ['다른 창으로 복사', '다른 창으로 이동', '이름 변경', '삭제']) { expect(find.byTooltip(t), findsWidgets, reason: t); // 이름 변경 · 삭제는 아래 버튼 줄에도 } await tester.tap(find.text('경로 복사')); await calm(tester); expect(clip, p.join(left, 'doc.txt')); // 여럿: "2개 항목" · 이름 변경은 일괄 이름 바꾸기 (78) · 경로 복사는 둘 다 await act(tester, () => tester.tap(find.text('선택').first)); await act(tester, () => tester.tap(markOf('doc.txt'))); await act(tester, () => tester.tap(markOf('b.txt'))); await tester.longPress(find.text('b.txt').first); await calm(tester); expect(find.text('2개 항목'), findsOneWidget); expect(find.byTooltip('2개 삭제'), findsOneWidget); await tester.tap(find.byTooltip('2개 이름 일괄 바꾸기')); await calm(tester); expect(find.text('이름 일괄 바꾸기 · 2개'), findsOneWidget, reason: '78: 여럿이면 일괄 이름 바꾸기 창'); await tester.tap(find.text('취소').last); await calm(tester); await tester.longPress(find.text('b.txt').first); await calm(tester); await tester.tap(find.text('경로 복사 (2개)')); await calm(tester); expect(clip!.split('\n'), unorderedEquals([p.join(left, 'doc.txt'), p.join(left, 'b.txt')])); // 삭제 (아이콘 줄) 는 표시한 것 모두 - 확인 창에서 취소 await tester.longPress(find.text('b.txt').first); await calm(tester); await tester.tap(find.byTooltip('2개 삭제')); await calm(tester); expect(find.textContaining('2개 항목을'), findsOneWidget); await tester.tap(find.text('취소').last); await calm(tester); expect(File(p.join(left, 'b.txt')).existsSync(), isTrue); // 선택 끝 (아래 알림이 [선택 끝] 을 가릴 수 있어 Esc) await tester.sendKeyEvent(LogicalKeyboardKey.escape); await calm(tester); // 폴더: 즐겨찾기에 추가 (넣은 뒤에는 메뉴에서 빠짐) await tester.longPress(find.text('sub').first); await calm(tester); await tester.tap(find.text('즐겨찾기에 추가')); await calm(tester); expect(c.settings.folderBookmarks.map((x) => x.path), contains(p.join(left, 'sub'))); await tester.longPress(find.text('sub').first); await calm(tester); expect(find.text('즐겨찾기에 추가'), findsNothing); await tester.tapAt(const Offset(5, 5)); await calm(tester); }); testWidgets('75: 창마다 뒤로 · 앞으로 - 버튼 · Alt+← / → · 마우스 4 · 5번', (tester) async { await open(tester); final sub = p.join(left, 'sub'); String now() => c.settings.explorer.paths.first; await act(tester, () => tester.tap(find.text('sub').first)); await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty); // 뒤로 (버튼): 왼쪽 창은 left 로 await act(tester, () => tester.tap(find.byTooltip('뒤로 (Alt+←)').first)); await settle(tester, () => now() == left, rounds: 50); expect(now(), left); // 앞으로 (버튼) await act(tester, () => tester.tap(find.byTooltip('앞으로 (Alt+→)').first)); await settle(tester, () => now() == sub, rounds: 50); expect(now(), sub); // Alt+← · Alt+→ await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft); await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft); await settle(tester, () => now() == left, rounds: 50); expect(now(), left); // 마우스 5번 (앞으로) · 4번 (뒤로) Future mouse(int button) async { final g = await tester.createGesture(kind: PointerDeviceKind.mouse, buttons: button); await g.addPointer(location: tester.getCenter(find.text('doc.txt').first)); await g.down(tester.getCenter(find.text('doc.txt').first)); await g.up(); await g.removePointer(); } await mouse(kForwardMouseButton); await settle(tester, () => now() == sub, rounds: 50); expect(now(), sub); await mouse(kBackMouseButton); await settle(tester, () => now() == left, rounds: 50); expect(now(), left); }); testWidgets('75: 두 창 - 마우스 4 · 5번은 마우스가 올라간 창에 (그 창이 활성으로)', (tester) async { File(p.join(right, 'r1', 'x.txt')) ..createSync(recursive: true) ..writeAsStringSync('x'); await open(tester); final r1 = p.join(right, 'r1'); final sub = p.join(left, 'sub'); String at(int i) => c.settings.explorer.paths[i]; await act(tester, () => tester.tap(find.text('r1'))); await settle(tester, () => at(1) == r1 && find.text('x.txt').evaluate().isNotEmpty, rounds: 50); // 왼쪽 창이 활성 await act(tester, () => tester.tap(find.text('sub'))); await settle(tester, () => at(0) == sub, rounds: 50); // 오른쪽 창 위에서 4번 (뒤로) - 오른쪽만 뒤로 final g = await tester.createGesture(kind: PointerDeviceKind.mouse, buttons: kBackMouseButton); await g.addPointer(location: tester.getCenter(find.text('x.txt'))); await g.down(tester.getCenter(find.text('x.txt'))); await g.up(); await g.removePointer(); await settle(tester, () => at(1) == right, rounds: 50); expect([at(0), at(1)], [sub, right]); // 오른쪽 창이 활성이 되었으므로 Alt+→ 도 오른쪽 await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft); await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft); await settle(tester, () => at(1) == r1, rounds: 50); expect([at(0), at(1)], [sub, r1]); }); testWidgets('75: 지워진 폴더로 앞으로 - 그 항목은 버리고 지금 폴더 그대로 · 이유를 알림', (tester) async { await open(tester); final sub = p.join(left, 'sub'); String now() => c.settings.explorer.paths.first; await act(tester, () => tester.tap(find.text('sub'))); await settle(tester, () => now() == sub, rounds: 50); await act(tester, () => tester.tap(find.byTooltip('뒤로 (Alt+←)').first)); await settle(tester, () => now() == left, rounds: 50); await tester.runAsync(() => Directory(sub).delete(recursive: true)); expect(find.text('sub'), findsOneWidget, reason: '밖에서 지운 것은 아직 모름'); await act(tester, () => tester.tap(find.byTooltip('앞으로 (Alt+→)').first)); await settle(tester, () => find.textContaining('내역에서 뺐습니다').evaluate().isNotEmpty, rounds: 50); expect(find.textContaining('내역에서 뺐습니다'), findsOneWidget); expect(now(), left); await settle(tester, () => find.text('sub').evaluate().isEmpty, rounds: 50); expect(find.text('sub'), findsNothing, reason: '트리에도 남지 않음 (상위 폴더를 다시 읽음)'); // 버린 항목은 다시 가지 않는다 (앞으로 버튼이 꺼짐) final fwd = tester.widget(find.ancestor(of: find.byTooltip('앞으로 (Alt+→)').first, matching: find.byType(IconButton)).first); expect(fwd.onPressed, isNull); ScaffoldMessenger.of(tester.element(find.byType(Scaffold).first)).clearSnackBars(); await tester.pump(); }); testWidgets('189: Android 는 트리 화살표를 손가락으로 누를 수 있게 넓게 (줄 높이 전체 · 폭 44)', (tester) async { debugDefaultTargetPlatformOverride = TargetPlatform.android; addTearDown(() => debugDefaultTargetPlatformOverride = null); await open(tester); final area = find.ancestor(of: find.descendant( of: find.ancestor(of: find.text('sub'), matching: find.byType(InkWell)).first, matching: find.byIcon(Icons.chevron_right)), matching: find.byType(InkResponse)).first; final size = tester.getSize(area); expect(size.width, greaterThanOrEqualTo(44)); expect(size.height, greaterThanOrEqualTo(34)); // 아이콘이 아니라 누르는 영역의 가장자리를 눌러도 펼친다 final r = tester.getRect(area); await act(tester, () => tester.tapAt(Offset(r.left + 3, r.top + 3))); await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty); expect(find.text('inner.txt'), findsOneWidget); debugDefaultTargetPlatformOverride = null; }); test('203: 크기 · 바뀐 날을 모르면 (잠긴 시스템 파일) "-1B · 1970-01-01" 대신 "—"', () { expect(formatSize(-1), '—'); expect(formatSize(0), '0B'); expect(fileDateText(DateTime(1970)), '—'); expect(fileDateText(DateTime.fromMillisecondsSinceEpoch(0)), '—'); expect(fileDateText(DateTime(2026, 10, 11, 9, 5)), '2026-10-11 09:05'); }); }