import 'dart:io'; import 'package:archive/archive.dart' show Archive, ArchiveFile, ZipEncoder; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:jj_mkvmaker/ui/explorer_page.dart'; import 'package:jj_mkvmaker/ui/explorer_grid.dart'; import 'package:jj_mkvmaker/ui/text_viewer.dart'; import 'package:path/path.dart' as p; import 'support/explorer_clock_keys.dart'; import 'support/explorer_harness.dart'; /// E5e: 예전 explorer_test.dart 에서 나눈 탐색기 화면 시험 - 압축을 폴더처럼 · 격자 · 글 뷰어 · 앱 휴지통 · 원본 지우기 옵션 알림 · 바로 실행 void main() { useExplorerFixture(); testWidgets('X-plore f: 압축을 트리 안에서 폴더처럼 펼치기 · 안은 읽기 전용 · 복사는 꺼내기', (tester) async { final zip = p.join(left, 'pack.zip'); File(zip).writeAsBytesSync(ZipEncoder().encode(Archive() ..addFile(ArchiveFile.string('top.txt', 'top')) ..addFile(ArchiveFile.string('a/b.txt', 'bee')))); await open(tester); await settle(tester, () => find.text('pack.zip').evaluate().isNotEmpty); // › 로 펼치면 압축 안의 것이 폴더처럼 (목록에 폴더 이름이 없어도 경로에서 폴더를 만든다) await act(tester, () => tester.tap(chevronOf('pack.zip').first)); await settle(tester, () => find.text('top.txt').evaluate().isNotEmpty); expect(find.text('a'), findsOneWidget); await act(tester, () => tester.tap(chevronOf('a').first)); await settle(tester, () => find.text('b.txt').evaluate().isNotEmpty); expect(find.text('b.txt'), findsOneWidget); // 안의 것을 지우려 하면 막는다 (압축은 그대로) await act(tester, () => tester.tap(find.text('top.txt'))); await settle(tester, () => false, rounds: 10); await act(tester, () => tester.tap(find.text('삭제').first)); await settle(tester, () => find.textContaining('읽기 전용').evaluate().isNotEmpty); expect(find.textContaining('압축 안은 읽기 전용'), findsOneWidget); expect(File(zip).existsSync(), isTrue); expect(Directory(p.join(left, 'pack')).existsSync(), isFalse, reason: '풀지 않음'); ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars(); await tester.pump(const Duration(seconds: 1)); // 압축 안의 폴더를 펼쳐 지금 폴더가 압축 안이면 [새 폴더] 도 막는다 (이름을 묻지 않음) await act(tester, () => tester.tap(find.text('a'))); await settle(tester, () => false, rounds: 10); await act(tester, () => tester.tap(find.text('새 폴더').first)); await settle(tester, () => find.textContaining('읽기 전용').evaluate().isNotEmpty); expect(find.textContaining('압축 안은 읽기 전용'), findsOneWidget); expect(find.byType(TextField), findsNothing, reason: '이름 묻는 창 없음'); ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars(); await tester.pump(const Duration(seconds: 1)); // 데이터 검토 (10/18): 압축 안의 것 메뉴에는 [ZIP 으로 묶기] · [Wi-Fi 로 공유] 가 없다 (실제 파일이 아니라서). 압축 밖은 그대로 await tester.longPress(find.text('a')); await calm(tester); expect(find.text('ZIP 으로 묶기'), findsNothing); expect(find.text('Wi-Fi 로 공유'), findsNothing); await tester.tapAt(const Offset(5, 5)); await calm(tester); await tester.longPress(find.text('sub')); await calm(tester); expect(find.text('ZIP 으로 묶기'), findsOneWidget); await tester.tapAt(const Offset(5, 5)); await calm(tester); }); testWidgets('X-plore f 보충: 펼친 뒤 압축 파일이 바뀌면 새로 고칠 때 목록을 다시 읽는다 (관리자 지시 10/19)', (tester) async { final zip = p.join(left, 'pack.zip'); File(zip).writeAsBytesSync(ZipEncoder().encode(Archive()..addFile(ArchiveFile.string('old.txt', 'old')))); await open(tester); await settle(tester, () => find.text('pack.zip').evaluate().isNotEmpty); await act(tester, () => tester.tap(chevronOf('pack.zip').first)); await settle(tester, () => find.text('old.txt').evaluate().isNotEmpty); expect(find.text('old.txt'), findsOneWidget); // 다른 프로그램이 압축을 바꿈 (크기 · 바뀐 때가 달라짐) await tester.runAsync(() async { await Future.delayed(const Duration(milliseconds: 1100)); File(zip).writeAsBytesSync(ZipEncoder().encode(Archive() ..addFile(ArchiveFile.string('new.txt', 'new content')) ..addFile(ArchiveFile.string('more.txt', 'more')))); }); await act(tester, () => refreshKey(tester)); await settle(tester, () => find.text('new.txt').evaluate().isNotEmpty); expect(find.text('new.txt'), findsOneWidget); expect(find.text('more.txt'), findsOneWidget); expect(find.text('old.txt'), findsNothing); }); testWidgets('X-plore g · h: 폴더 메뉴 [격자로 보기] (길게 = 그 자리로) · 파일 메뉴 [글 뷰어로 보기]', (tester) async { File(p.join(left, 'sub', 'pic.png')).writeAsBytesSync([0x89, 0x50]); await open(tester); // g: 폴더를 길게 → 격자로 보기 → 그림만 (글 파일은 빠짐) await tester.longPress(find.text('sub')); await calm(tester); await act(tester, () => tester.tap(find.text('격자로 보기 (그림 · 동영상)'))); await settle(tester, () => find.byType(ExplorerGridPage).evaluate().isNotEmpty); expect(find.descendant(of: find.byType(ExplorerGridPage), matching: find.text('pic.png')), findsOneWidget); expect(find.descendant(of: find.byType(ExplorerGridPage), matching: find.text('inner.txt')), findsNothing); // 칸을 길게 누르면 탐색기에서 그 자리로 (격자 닫힘) await tester.longPress(find.descendant(of: find.byType(ExplorerGridPage), matching: find.text('pic.png'))); await settle(tester, () => find.byType(ExplorerGridPage).evaluate().isEmpty && find.descendant(of: find.byType(ExplorerPage), matching: find.text('pic.png')).evaluate().isNotEmpty); expect(find.descendant(of: find.byType(ExplorerPage), matching: find.text('pic.png')), findsOneWidget, reason: '탐색기에 그 폴더가 펼쳐져 보임 (길게 누른 칸의 이름 풍선은 빼고)'); // h: 파일을 길게 → 글 뷰어로 보기 (읽기 전용) await tester.longPress(find.text('doc.txt')); await calm(tester); await act(tester, () => tester.tap(find.text('글 뷰어로 보기'))); await settle(tester, () => find.text('hello').evaluate().isNotEmpty); expect(find.byType(TextViewerPage), findsOneWidget); expect(shell.opened, isEmpty, reason: '다른 앱으로 열지 않음'); await act(tester, () async => Navigator.of(tester.element(find.byType(TextViewerPage))).pop()); await settle(tester, () => find.byType(TextViewerPage).evaluate().isEmpty); }); testWidgets('X-plore e: Android 앱 휴지통 - 지우기 = 휴지통으로 · [되돌리기] · [휴지통] 화면에서 되살리기', (tester) async { // 저장 장치는 시험 폴더 (실제 C:\ 에 .jj_trash 를 만들지 않게) device.trash = true; device.volumes = () => [(tmp.path, 'T:')]; // [휴지통] 버튼이 줄에 보이게 (처음 구성은 좁으면 [더 보기] 로 간다) c.settings.explorer.buttons = ['trash', for (final b in ExplorerButton.defaultOrder(android: false)) if (b != ExplorerButton.trash) b.name]; addTearDown(() { }); await open(tester); final doc = p.join(left, 'doc.txt'); await act(tester, () => tester.tap(find.text('doc.txt'))); await settle(tester, () => false, rounds: 10); await act(tester, () => tester.tap(find.text('삭제').first)); await calm(tester); expect(find.textContaining('앱 휴지통에 14일'), findsOneWidget); await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '휴지통으로'))); await settle(tester, () => !File(doc).existsSync() && find.text('되돌리기').evaluate().isNotEmpty); expect(File(doc).existsSync(), isFalse); expect(Directory(p.join(tmp.path, '.jj_trash')).existsSync(), isTrue); // [되돌리기] await act(tester, () => tester.tap(find.text('되돌리기'))); await settle(tester, () => File(doc).existsSync()); expect(File(doc).readAsStringSync(), 'hello'); ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars(); await calm(tester); // 다시 지우고 [휴지통] 화면에서 되살리기 await act(tester, () => tester.tap(find.text('doc.txt'))); await settle(tester, () => false, rounds: 10); await act(tester, () => tester.tap(find.text('삭제').first)); await calm(tester); await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '휴지통으로'))); await settle(tester, () => !File(doc).existsSync()); ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars(); await calm(tester); // [휴지통] 버튼으로 휴지통 화면을 연다 await act(tester, () => tester.tap(find.text('휴지통').first)); await settle(tester, () => find.byType(CheckboxListTile).evaluate().isNotEmpty); await act(tester, () => tester.tap(find.byType(CheckboxListTile).first)); await settle(tester, () => find.text('되살리기').hitTestable().evaluate().isNotEmpty); // 화면 넘김이 끝나 눌릴 때까지 await act(tester, () => tester.tap(find.text('되살리기'))); await settle(tester, () => File(doc).existsSync()); expect(File(doc).readAsStringSync(), 'hello'); await tester.pageBack(); await calm(tester); ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars(); await tester.pump(const Duration(seconds: 2)); }); testWidgets('178 보충: 설정의 원본 지우기 옵션은 복사 창에서 빠진 까닭을 알린다 (이동 창에는 없음)', (tester) async { c.settings ..copy.methodFile = 'robocopy' ..copy.robocopyOptions = '/E /MOV'; await open(tester); await act(tester, () => tester.tap(find.text('doc.txt'))); await settle(tester, () => false, rounds: 15); await act(tester, () => tester.tap(find.text('복사').first)); await calm(tester); expect(find.textContaining('방법: robocopy'), findsOneWidget); expect(find.text('환경 설정의 /MOV 는 복사에서는 쓰지 않습니다 (이동은 [이동] 으로)'), findsOneWidget); await act(tester, () => tester.tap(find.text('취소').last)); await calm(tester); await act(tester, () => tester.tap(find.text('이동').first)); await calm(tester); expect(find.textContaining('옮길까요?'), findsOneWidget); expect(find.textContaining('복사에서는 쓰지 않습니다'), findsNothing); await act(tester, () => tester.tap(find.text('취소').last)); await calm(tester); expect(File(p.join(left, 'doc.txt')).existsSync(), isTrue); expect(File(p.join(right, 'doc.txt')).existsSync(), isFalse); }, skip: !Platform.isWindows); testWidgets('바로 실행 모드: 폴더를 누르면 펼치고, 파일을 누르면 바로 실행', (tester) async { c.settings.explorer.click = 'open'; await open(tester); await act(tester, () => tester.tap(find.text('sub'))); await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty); expect(find.text('inner.txt'), findsOneWidget); await act(tester, () => tester.tap(find.text('doc.txt'))); await settle(tester, () => shell.opened.isNotEmpty); expect(shell.opened.single, (p.join(left, 'doc.txt'), false)); }); }