import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:jj_mkvmaker/app/settings.dart'; import 'package:jj_mkvmaker/ui/explorer_page.dart'; import 'package:jj_mkvmaker/ui/hidden_items_dialog.dart'; import 'package:jj_mkvmaker/ui/explorer_search.dart'; import 'package:path/path.dart' as p; import 'support/explorer_clock_keys.dart'; import 'support/explorer_harness.dart'; /// E5e: 예전 explorer_test.dart 에서 나눈 탐색기 화면 시험 - 목록에서 숨기기 · .nomedia · 폴더 크기 · 찾기 결과 void main() { useExplorerFixture(); testWidgets('X-plore 숨기기: 길게 누르기 [목록에서 숨기기] → 목록에서만 빠짐 (파일 그대로) · 맨 아래 [숨긴 항목 보이기] · [숨기기 풀기]', (tester) async { // 폴더 + 파일 목록 배치: 오른쪽 목록은 그 폴더의 것만이라 짧다 (트리는 Temp 폴더 줄이 많아 맨 아래가 멀다) c.settings.explorerLayout = 'split'; await open(tester); final doc = p.join(left, 'doc.txt'); await tester.longPress(find.text('doc.txt')); await calm(tester); await act(tester, () => tester.tap(find.text('목록에서 숨기기'))); await calm(tester); // X-plore 맞추기 (관리자 지시 10/20): 무엇이 바뀌는지 확인 창 expect(find.textContaining('파일 탐색기 목록에서만 빠집니다'), findsOneWidget); await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '숨기기'))); await settle(tester, () => find.text('doc.txt').evaluate().isEmpty); expect(find.text('doc.txt'), findsNothing); expect(File(doc).existsSync(), isTrue, reason: '파일은 바꾸지 않는다'); expect(c.settings.explorerHiddenItems, [doc]); // 목록 맨 아래에 [숨긴 항목 1개 보이기] expect(find.text('숨긴 항목 1개 보이기'), findsOneWidget); await idle(tester); // 보이기 → 흐리게 보이고, 메뉴는 [숨기기 풀기] await act(tester, () => tester.tap(find.text('숨긴 항목 1개 보이기'))); await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty); expect(find.text('숨긴 항목 1개 다시 숨기기'), findsOneWidget); expect(find.ancestor(of: find.text('doc.txt'), matching: find.byType(Opacity)), findsOneWidget); await tester.longPress(find.text('doc.txt')); await calm(tester); expect(find.text('목록에서 숨기기'), findsNothing); await act(tester, () => tester.tap(find.text('숨기기 풀기'))); await settle(tester, () => find.textContaining('숨긴 항목').evaluate().isEmpty); expect(c.settings.explorerHiddenItems, isEmpty); expect(find.text('doc.txt'), findsOneWidget); expect(find.ancestor(of: find.text('doc.txt'), matching: find.byType(Opacity)), findsNothing); }); testWidgets('X-plore 숨기기 확인 창: [취소] 면 그대로 · [다음부터 묻지 않기] 를 고르면 다음엔 묻지 않고 바로 숨김 (설정 저장)', (tester) async { c.settings.explorerLayout = 'split'; expect(AppSettings().explorerHideConfirm, isTrue, reason: '처음은 묻기'); File(p.join(left, 'two.txt')).writeAsStringSync('2'); await open(tester); await tester.longPress(find.text('doc.txt')); await calm(tester); await act(tester, () => tester.tap(find.text('목록에서 숨기기'))); await calm(tester); await act(tester, () => tester.tap(find.text('취소'))); await calm(tester); expect(c.settings.explorerHiddenItems, isEmpty, reason: '취소하면 숨기지 않는다'); expect(find.text('doc.txt'), findsOneWidget); // 다음부터 묻지 않기 + 숨기기 await tester.longPress(find.text('doc.txt')); await calm(tester); await act(tester, () => tester.tap(find.text('목록에서 숨기기'))); await calm(tester); await act(tester, () => tester.tap(find.text('다음부터 묻지 않기'))); await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '숨기기'))); await settle(tester, () => find.text('doc.txt').evaluate().isEmpty); expect(c.settings.explorerHideConfirm, isFalse); expect(AppSettings.fromJson(c.settings.toJson()).explorerHideConfirm, isFalse); expect(AppSettings.fromJson({}).explorerHideConfirm, isTrue, reason: '예전 설정 (값 없음) 은 묻기'); // 이제는 창 없이 바로 await tester.longPress(find.text('two.txt')); await calm(tester); await act(tester, () => tester.tap(find.text('목록에서 숨기기'))); await settle(tester, () => find.text('two.txt').evaluate().isEmpty); expect(find.textContaining('파일 탐색기 목록에서만 빠집니다'), findsNothing); expect(c.settings.explorerHiddenItems, [p.join(left, 'doc.txt'), p.join(left, 'two.txt')]); }); testWidgets('X-plore 맞추기: 폴더 길게 누르기 [갤러리 · 음악 앱에서 숨기기 (.nomedia)] - 확인 창 뒤 빈 .nomedia, 다시 누르면 지우기 · 안의 파일은 그대로', (tester) async { device.nomediaOn = true; final mark = File(p.join(left, 'sub', '.nomedia')); await open(tester); expect(find.text('갤러리 · 음악 앱에서 숨기기 (.nomedia)'), findsNothing); await tester.longPress(find.text('sub')); await calm(tester); await act(tester, () => tester.tap(find.text('갤러리 · 음악 앱에서 숨기기 (.nomedia)'))); await calm(tester); expect(find.textContaining('빈 .nomedia 파일을 만듭니다'), findsOneWidget); // 취소하면 아무것도 안 바뀜 await act(tester, () => tester.tap(find.text('취소'))); await calm(tester); expect(mark.existsSync(), isFalse); await tester.longPress(find.text('sub')); await calm(tester); await act(tester, () => tester.tap(find.text('갤러리 · 음악 앱에서 숨기기 (.nomedia)'))); await calm(tester); await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '숨기기'))); await settle(tester, () => mark.existsSync() && shell.scanned.isNotEmpty); expect(mark.lengthSync(), 0); expect(shell.scanned, [ [mark.path, p.join(left, 'sub')] ], reason: '갤러리가 다시 읽게 (MediaScanner)'); expect(File(p.join(left, 'sub', 'inner.txt')).readAsStringSync(), 'inner', reason: '안의 파일은 그대로'); // 이제 메뉴는 [다시 보이기] await idle(tester); await tester.longPress(find.text('sub')); await calm(tester); expect(find.text('갤러리 · 음악 앱에서 숨기기 (.nomedia)'), findsNothing); await act(tester, () => tester.tap(find.text('갤러리 · 음악 앱에 다시 보이기 (.nomedia 지우기)'))); await calm(tester); expect(find.textContaining('.nomedia 파일을 지웁니다'), findsOneWidget); await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '다시 보이기'))); await settle(tester, () => !mark.existsSync()); await settle(tester, () => shell.scanned.length == 2); expect(shell.scanned.last, [mark.path, p.join(left, 'sub')], reason: '지울 때도 다시 읽게'); expect(File(p.join(left, 'sub', 'inner.txt')).existsSync(), isTrue); // 파일에는 없음 await idle(tester); await tester.longPress(find.text('doc.txt')); await calm(tester); expect(find.text('갤러리 · 음악 앱에서 숨기기 (.nomedia)'), findsNothing); expect(find.text('갤러리 · 음악 앱에 다시 보이기 (.nomedia 지우기)'), findsNothing); }); testWidgets('X-plore 숨기기: 숨긴 것이 든 폴더를 지울 때 확인 창에 알림 (숨긴 것도 함께 지워진다)', (tester) async { c.settings.explorerHiddenItems = [p.join(left, 'sub', 'inner.txt')]; await open(tester); await tester.longPress(find.text('sub')); await calm(tester); await act(tester, () => tester.tap(find.byTooltip('삭제').last)); await calm(tester); expect(find.text('지우는 폴더 안에 목록에서 숨긴 항목 1개가 있습니다 - 함께 지워집니다'), findsOneWidget); await act(tester, () => tester.tap(find.text('취소').last)); await calm(tester); expect(File(p.join(left, 'sub', 'inner.txt')).existsSync(), isTrue); // 다른 파일만 지울 때는 알리지 않는다 await tester.longPress(find.text('doc.txt')); await calm(tester); await act(tester, () => tester.tap(find.byTooltip('삭제').last)); await calm(tester); expect(find.textContaining('목록에서 숨긴 항목'), findsNothing); await act(tester, () => tester.tap(find.text('취소').last)); await calm(tester); }); testWidgets('X-plore 숨기기: 이름을 바꾸면 숨긴 목록도 새 경로로 · 영구로 지우면 목록에서 뺀다 (관리자 결정)', (tester) async { c.settings.explorerHiddenItems = [p.join(left, 'sub', 'inner.txt')]; await open(tester); await tester.longPress(find.text('sub')); await calm(tester); await act(tester, () => tester.tap(find.byTooltip('이름 변경').last)); await calm(tester); await act(tester, () => tester.enterText(find.byType(TextField), 'sub2')); await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '확인'))); await settle(tester, () => find.text('sub2').evaluate().isNotEmpty); expect(c.settings.explorerHiddenItems, [p.join(left, 'sub2', 'inner.txt')]); await idle(tester); await tester.longPress(find.text('sub2')); await calm(tester); await act(tester, () => tester.tap(find.byTooltip('삭제').last)); 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.text('sub2').evaluate().isEmpty); await settle(tester, () => c.settings.explorerHiddenItems.isEmpty); expect(c.settings.explorerHiddenItems, isEmpty); }); testWidgets('X-plore 숨기기: 다른 창으로 옮기면 숨긴 목록도 새 자리로 (그 안의 것도)', (tester) async { c.settings.explorerHiddenItems = [p.join(left, 'sub', 'inner.txt')]; await open(tester); await tester.longPress(find.text('sub')); await calm(tester); await act(tester, () => tester.tap(find.byTooltip('다른 창으로 이동').last)); await settle(tester, () => find.widgetWithText(FilledButton, '이동').evaluate().isNotEmpty); await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '이동'))); await settle(tester, () => c.settings.explorerHiddenItems.first != p.join(left, 'sub', 'inner.txt')); expect(File(p.join(right, 'sub', 'inner.txt')).existsSync(), isTrue); expect(c.settings.explorerHiddenItems, [p.join(right, 'sub', 'inner.txt')]); await idle(tester); }); testWidgets('X-plore 숨기기: 위 막대 ⋮ [목록에서 숨긴 항목 보이기] · 환경 설정의 [목록에서 숨긴 항목] 에서 풀기', (tester) async { final doc = p.join(left, 'doc.txt'); c.settings ..explorerLayout = 'split' ..explorerHiddenItems = [doc]; tester.view.physicalSize = const Size(1600, 900); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(device: device, c: c)))); await settle(tester, () => find.text('숨긴 항목 1개 보이기').evaluate().isNotEmpty); expect(find.text('doc.txt'), findsNothing); await act(tester, () => tester.tap(find.byTooltip('창 배치 · 버튼 구성'))); await calm(tester); await act(tester, () => tester.tap(find.text('목록에서 숨긴 항목 보이기 (1개)'))); await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty); expect(find.text('숨긴 항목 1개 다시 숨기기'), findsOneWidget); await act(tester, () => tester.tap(find.byTooltip('창 배치 · 버튼 구성'))); await calm(tester); await act(tester, () => tester.tap(find.text('목록에서 숨긴 항목 다시 숨기기'))); await settle(tester, () => find.text('doc.txt').evaluate().isEmpty); // 설정 화면의 목록에서 풀면 탐색기 목록에 바로 보인다 final ctx = tester.element(find.byType(ExplorerPage)); unawaited(showHiddenItems(ctx, c)); await calm(tester); expect(find.text('목록에서 숨긴 항목 (1개)'), findsOneWidget); await act(tester, () => tester.tap(find.byTooltip('숨기기 풀기'))); await calm(tester); expect(c.settings.explorerHiddenItems, isEmpty); expect(find.text('숨긴 항목이 없습니다'), findsOneWidget); await act(tester, () => tester.tap(find.text('닫기'))); await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty); expect(find.textContaining('숨긴 항목'), findsNothing); }); testWidgets('80: 크기순 정렬이면 폴더 크기를 계산해 보이고, "안 함" 이어도 길게 누르기 [폴더 크기 계산]', (tester) async { File(p.join(left, 'sub', 'big.bin')).writeAsBytesSync(List.filled(3000, 1)); c.settings ..explorerSort = 'size' ..explorerSortDesc = true; await open(tester); // sub = inner.txt (5) + big.bin (3000) await settle(tester, () => find.text('3KB').evaluate().isNotEmpty); expect(find.text('3KB'), findsWidgets); // 바뀌면 (새로 고침) 다시 계산 File(p.join(left, 'sub', 'more.bin')).writeAsBytesSync(List.filled(3000, 1)); await act(tester, () => refreshKey(tester)); await settle(tester, () => find.text('6KB').evaluate().isNotEmpty); expect(find.text('6KB'), findsWidgets); }); testWidgets('80: "계산 안 함" - 크기를 보이지 않다가 [폴더 크기 계산] 을 누르면', (tester) async { File(p.join(left, 'sub', 'big.bin')).writeAsBytesSync(List.filled(3000, 1)); c.settings ..explorerSort = 'size' ..explorerFolderSize = 'off'; await open(tester); await settle(tester, () => false, rounds: 15); expect(find.text('3KB'), findsNothing); await tester.longPress(find.text('sub').first); await calm(tester); await act(tester, () => tester.tap(find.text('폴더 크기 계산'))); await settle(tester, () => find.text('3KB').evaluate().isNotEmpty); expect(find.text('3KB'), findsWidgets); }); testWidgets('79-a: [그 자리로] 로 나가도 [찾기 결과로 돌아가기] 가 남는다 (관리자 판단) · ✕ 로 닫음', (tester) async { await open(tester); await act(tester, () => tester.tap(find.text('찾기').first)); await calm(tester); await tester.enterText(find.widgetWithText(TextField, '이름 (비워도 됨)'), 'inner'); await tester.pump(); await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '찾기'))); await settle(tester, () => find.text('찾기 결과 · 1개').evaluate().isNotEmpty); await act(tester, () => tester.tap(find.byTooltip('그 자리로'))); await settle(tester, () => find.text('찾기 결과로 돌아가기').evaluate().isNotEmpty); await calm(tester); // 결과 화면이 닫히는 움직임까지 expect(find.byType(SearchResultsPage), findsNothing); await act(tester, () => tester.tap(find.text('찾기 결과로 돌아가기'))); await settle(tester, () => find.text('찾기 결과 · 1개').evaluate().isNotEmpty); // 결과 화면을 그냥 닫으면 없어진다 await act(tester, () => tester.pageBack()); await calm(tester); expect(find.text('찾기 결과로 돌아가기'), findsNothing); }); testWidgets('79: 찾기 결과를 목록으로 - 이름 없이 종류 조건으로 찾고, 골라서 그대로 다른 창으로 복사', (tester) async { await open(tester); File(p.join(left, 'sub', 'big.mkv')) ..createSync(recursive: true) ..writeAsBytesSync(List.filled(2048, 1)); await act(tester, () => tester.tap(find.text('찾기').first)); await calm(tester); // 이름은 비우고 종류만 (동영상) - 이름이 비면 [찾기] 가 꺼져 있다가 조건을 넣으면 켜진다 expect(tester.widget(find.widgetWithText(FilledButton, '찾기')).onPressed, isNull); await act(tester, () => tester.tap(find.widgetWithText(ChoiceChip, '동영상'))); await calm(tester); await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '찾기'))); await settle(tester, () => find.text('big.mkv').evaluate().isNotEmpty); expect(find.text('찾기 결과 · 1개'), findsOneWidget); // (뒤의 탐색기 목록에는 doc.txt 가 있다 - 결과 화면 안만 본다) expect(find.descendant(of: find.byType(SearchResultsPage), matching: find.text('doc.txt')), findsNothing, reason: '종류가 맞지 않음'); // 고르면 아래에 할 일 줄 - 다른 창 (right) 으로 복사 await act(tester, () => tester.tap(find.descendant(of: find.byType(SearchResultsPage), matching: find.byType(Checkbox)).first)); await calm(tester); await act(tester, () => tester.tap(find.text('복사 → right'))); await calm(tester); // 탐색기의 복사 확인 창 그대로 await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '복사'))); await settle(tester, () => File(p.join(right, 'big.mkv')).existsSync() && find.byType(LinearProgressIndicator).evaluate().isEmpty); expect(File(p.join(right, 'big.mkv')).lengthSync(), 2048); expect(File(p.join(left, 'sub', 'big.mkv')).existsSync(), isTrue); }); }