import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:jj_mkvmaker/app/app_controller.dart'; import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart'; import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart'; import 'package:jj_mkvmaker/services/app_shell.dart'; import 'package:jj_mkvmaker/services/platform_services.dart'; import 'package:jj_mkvmaker/ui/explorer_page.dart'; import 'package:path/path.dart' as p; import 'explorer_clock_keys.dart'; import 'fake_recycle_bin.dart'; import 'test_explorer_device.dart'; /// E5e: 탐색기 화면 시험의 공용 준비 (예전 explorer_test.dart 의 setUp · tearDown · open · 도우미). /// [useExplorerFixture] 를 main 맨 앞에서 부르면 시험마다 시험 폴더 (left: doc.txt · sub/inner.txt, right: 빈 폴더) 와 /// 앱 (다른 앱으로 열기를 적기만 하는 셸, 저장소 없음) 을 새로 만들고 끝나면 지운다. 휴지통은 가짜 (사용자 휴지통에 닿지 않게) /// 다른 앱으로 열기를 기록만 하는 셸 (실제로 프로그램을 띄우지 않게) class ExplorerShell extends NoopShell { final opened = <(String, bool)>[]; final scanned = >[]; @override Future mediaScan(List paths) async => scanned.add(paths); @override Future openWith(String path, {bool choose = false}) async { opened.add((path, choose)); return true; } } Directory? _tmp; String? _left, _right; ExplorerShell? _shell; TestExplorerDevice? _device; AppController? _c; /// 이 시험의 시험 폴더 · 왼쪽 · 오른쪽 창 폴더 · 셸 · 앱 Directory get tmp => _tmp!; String get left => _left!; String get right => _right!; ExplorerShell get shell => _shell!; AppController get c => _c!; /// 이 시험의 기기 (저장 장치 · 드라이브 · Android 갈래) - [open] 이 화면에 넘긴다 TestExplorerDevice get device => _device!; void useExplorerFixture() { setUp(() { // 휴지통: 기본은 가짜 (이 시험들은 영구 삭제를 고르지만 혹시라도 사용자 휴지통에 닿지 않게) useTestRecycleBin(); _tmp = Directory.systemTemp.createTempSync('jj_explorer_'); _left = p.join(tmp.path, 'left'); _right = p.join(tmp.path, 'right'); File(p.join(left, 'doc.txt')) ..createSync(recursive: true) ..writeAsStringSync('hello'); File(p.join(left, 'sub', 'inner.txt')) ..createSync(recursive: true) ..writeAsStringSync('inner'); Directory(right).createSync(); _shell = ExplorerShell(); _device = TestExplorerDevice(); _c = AppController(PlatformServices( mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService(), shell: shell)); c.settings.explorer.paths = [left, right]; c.settings.explorer.style = 'xplore'; // E6: 이 시험들은 X-plore 모양 (트리) 을 본다 - Windows 처음 값은 Total Commander }); // Windows: 방금 닫은 파일 (65 의 잠금 시험) 이나 색인 · 백신이 잠깐 잡고 있으면 지우기가 실패한다 - 될 때까지 기다린다 tearDown(() async { for (var i = 0;; i++) { try { if (tmp.existsSync()) tmp.deleteSync(recursive: true); return; } on FileSystemException { if (i >= 50) rethrow; await Future.delayed(const Duration(milliseconds: 100)); } } }); } /// 탐색기를 1600 × 900 으로 열고 왼쪽 창 목록이 보일 때까지 기다린다 (지금 시각은 정한 시각) Future open(WidgetTester tester) async { tester.view.physicalSize = const Size(1600, 900); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c, clock: () => testNow, device: device)))); await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty); } /// [name] 줄의 "표시" 동그라미 버튼 Finder markOf(String name) => find.descendant( of: find.ancestor(of: find.text(name), matching: find.byType(Material)).first, matching: find.byTooltip('표시 (여러 개 고르기)'), ); /// 누르기 등을 실제 비동기 구역에서 (그 때 시작한 파일 작업이 테스트의 가짜 시간에 묶이지 않게) Future act(WidgetTester tester, Future Function() f) async { await tester.runAsync(f); await tester.pump(); } /// [name] 트리 줄의 › (펼치기) 화살표 Finder chevronOf(String name) => find.descendant( of: find.ancestor(of: find.text(name), matching: find.byType(InkWell)).first, matching: find.byIcon(Icons.chevron_right), ); /// 두 번 누르기 (실제 비동기 구역에서: 그때 시작한 파일 작업이 테스트의 가짜 시간에 묶이지 않게) Future doubleTap(WidgetTester tester, Finder f) async { // 흔들림: 두 누름 사이를 실제 시간 (80ms) 으로 쉬면 무거울 때 두 번 누르기 판정 (300ms) 을 넘겨 한 번씩 두 번으로 됐다 - // 시험의 가짜 시간으로 50ms. 그 뒤 시작한 파일 작업은 settle (실제 시간) 이 기다린다 await tester.tap(f); await tester.pump(const Duration(milliseconds: 50)); await tester.tap(f); await tester.pump(); } /// 창들이 목록을 다시 읽는 중 (맨 위 진행 막대 · 도는 표시) 이 끝날 때까지 실제 시간으로 기다린다. /// 그사이 pumpAndSettle 을 하면 막대가 계속 움직여, 디스크 읽기가 늦을 때 (전체 시험이 무거울 때) 시간 초과가 난다. /// 값이 있는 막대 (디스크 용량 · 진행률) 는 움직이지 않으므로 세지 않는다 Future idle(WidgetTester tester) => settle( tester, () => find .byWidgetPredicate((w) => w is ProgressIndicator && w.value == null) .evaluate() .isEmpty); /// 탐색기 시험의 pumpAndSettle: 목록 읽기가 끝난 뒤에 (경로를 바꾸거나 다시 읽은 뒤 무거울 때 흔들리던 것을 한 번에) Future calm(WidgetTester tester) async { await idle(tester); await tester.pumpAndSettle(); } /// 실제 파일 작업 (비동기 IO) 이 끝나기를 기다리며 화면을 갱신 // 끝나면 바로 나온다 - 넉넉한 횟수는 전체 시험 중 PC 가 바쁠 때 (실제 파일 입출력이 느려짐) 흔들리지 않게 Future settle(WidgetTester tester, bool Function() done, {int rounds = 300}) async { for (var i = 0; i < rounds; i++) { await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 30))); await tester.pump(); if (done()) break; } await tester.pump(const Duration(milliseconds: 300)); }