import 'dart:async'; import 'dart:io'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:jj_mkvmaker/app/app_controller.dart'; import 'package:jj_mkvmaker/core/file_ops.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/platform_services.dart'; import 'package:jj_mkvmaker/ui/explorer_page.dart'; import 'package:path/path.dart' as p; import 'support/explorer_harness.dart' show calm; import 'support/test_explorer_device.dart'; void main() { late TestExplorerDevice device; setUp(() => device = TestExplorerDevice()); late Directory tmp; late AppController c; late Completer stuck; var dead = {}; setUp(() { tmp = Directory.systemTemp.createTempSync('jj_drives_'); File(p.join(tmp.path, 'left', 'doc.txt')) ..createSync(recursive: true) ..writeAsStringSync('x'); Directory(p.join(tmp.path, 'right')).createSync(); c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings.explorerPaths = [p.join(tmp.path, 'left'), p.join(tmp.path, 'right')]; stuck = Completer(); dead = {r'R:\'}; // T: 시험 폴더 (정상) · Q: 응답 없는 네트워크 드라이브 · R: 연결 안 된 드라이브 device.volumes = () => [(tmp.path, 'T:'), (r'Q:\', 'Q:'), (r'R:\', 'R:')]; device.probe = (path) => path == r'Q:\' ? stuck.future : Future.value(!dead.contains(path)); }); tearDown(() { if (!stuck.isCompleted) stuck.complete(false); tmp.deleteSync(recursive: true); }); Future settle(WidgetTester tester, bool Function() done) async { for (var i = 0; i < 50 && !done(); i++) { await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 50))); await tester.pump(); } } testWidgets('165: 응답 없는 드라이브가 있어도 첫 화면이 바로 뜨고, 연결 안 된 드라이브는 숨기지 않고 "연결 안 됨" · 누르면 다시 연결', (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(device: device, c: c)))); await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty); // Q: 는 끝내 대답하지 않지만 목록 · 두 창은 바로 (Q: 를 기다리지 않음) expect(find.text('doc.txt'), findsOneWidget); // 걸린 시간 (전체 시험 중 바쁘면 늘어난다) 대신: Q: 가 끝내 대답하지 않은 채로 목록이 떴다 expect(stuck.isCompleted, isFalse); expect(find.text('Q:'), findsWidgets, reason: '응답 없는 드라이브도 목록에 (확인 중)'); // R: 는 숨기지 않고 "연결 안 됨" await settle(tester, () => find.textContaining('연결 안 됨').evaluate().isNotEmpty); expect(find.textContaining('R: · 연결 안 됨'), findsWidgets); // 누르면 다시 연결: 아직 안 되면 알림 await tester.tap(find.textContaining('R: · 연결 안 됨').first); await settle(tester, () => find.textContaining('R:\\ 에 연결할 수 없습니다').evaluate().isNotEmpty); expect(find.textContaining(r'R:\ 에 연결할 수 없습니다'), findsOneWidget); // 연결되면 "연결 안 됨" 이 사라진다 dead = {}; await tester.tap(find.textContaining('R: · 연결 안 됨').first); await settle(tester, () => find.textContaining('R: · 연결 안 됨').evaluate().isEmpty); expect(find.textContaining('R: · 연결 안 됨'), findsNothing); }); testWidgets('202: 드라이브가 많아 탭이 넘쳐도 [경로 입력] · WebDAV 를 누를 수 있고, 휠로 탭이 옆으로 · 빈 드라이브는 뒤로', (tester) async { // 빈 (연결 안 된) 드라이브가 앞쪽에 많은 PC final empties = [for (final l in 'GHIJKLNOPS'.split('')) ('$l:\\', '$l:')]; dead = {for (final e in empties) e.$1}; device.volumes = () => [...empties, (tmp.path, 'T:')]; device.probe = (path) => Future.value(!dead.contains(path)); c.settings ..explorerLayout = 'dual' ..explorerOrientation = 'side'; tester.view.physicalSize = const Size(1280, 800); 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('doc.txt').evaluate().isNotEmpty); await settle(tester, () => find.textContaining('S: ·').evaluate().isNotEmpty); // 넓은 창: 끝 버튼이 화면 안에 (눌러서 창이 뜬다) final pathBtn = find.byTooltip('경로 입력'); expect(pathBtn, findsNWidgets(2)); for (final e in pathBtn.evaluate()) { expect(tester.getRect(find.byWidget(e.widget).first).right, lessThanOrEqualTo(1280)); } await tester.tap(pathBtn.first); await calm(tester); expect(find.byType(AlertDialog), findsOneWidget); await tester.tap(find.text('취소').last); await calm(tester); // 정상 드라이브 (T:) 가 빈 드라이브들보다 앞 expect(tester.getTopLeft(find.text('T:').first).dx, lessThan(tester.getTopLeft(find.textContaining('G: ·').first).dx)); // 휠 (위 · 아래) 로 탭 줄이 옆으로 (왼쪽 창의 T: 탭이 왼쪽으로 밀린다) final before = tester.getTopLeft(find.text('T:').first).dx; final pointer = TestPointer(1, PointerDeviceKind.mouse); await tester.sendEventToBinding(pointer.hover(tester.getCenter(find.text('T:').first))); await tester.sendEventToBinding(pointer.scroll(const Offset(0, 120))); await tester.pump(); expect(tester.getTopLeft(find.text('T:').first).dx, lessThan(before - 10)); // 좁은 창 (위아래 두 창 · 폰): 끝 버튼은 [⋮] 하나로 늘 보임 → 경로 입력 tester.view.physicalSize = const Size(560, 900); await calm(tester); expect(find.byTooltip('경로 입력 · 네트워크 서버'), findsWidgets); await tester.tap(find.byTooltip('경로 입력 · 네트워크 서버').first); await calm(tester); await tester.tap(find.text('경로 입력').last); await calm(tester); expect(find.byType(AlertDialog), findsOneWidget); await tester.tap(find.text('취소').last); await calm(tester); expect(tester.takeException(), isNull, reason: '넘침 (RenderFlex overflow) 없음'); }); test('165: 드라이브 목록은 열어 보지 않고 Windows 에게 받는다 (빠름)', () { final sw = Stopwatch()..start(); final d = windowsDrives(); expect(sw.elapsedMilliseconds, lessThan(500)); expect(d.map((e) => e.$2), contains('C:')); }, skip: !Platform.isWindows); }