diff --git a/integration_test/windows_662_shot_test.dart b/integration_test/windows_662_shot_test.dart new file mode 100644 index 0000000..5980132 --- /dev/null +++ b/integration_test/windows_662_shot_test.dart @@ -0,0 +1,105 @@ +import 'dart:io'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:jj_mkvmaker/app/app_controller.dart'; +import 'package:jj_mkvmaker/app/bookmarks_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/platform_services.dart'; +import 'package:jj_mkvmaker/ui/app_actions.dart'; +import 'package:jj_mkvmaker/ui/browser_page.dart'; +import 'package:jj_mkvmaker/ui/home_page.dart'; +import 'package:jj_mkvmaker/ui/theme.dart'; +import 'package:path/path.dart' as p; + +/// 66-2 기기 감독 확인용 (Windows): 지금 화면 아이콘 (넓은 · 좁은 화면) · 처음으로 알림 · 브라우저 안내 한 줄을 PNG (JJ_SHOT_DIR) 로. +/// 설정은 메모리에만 (저장하지 않음), 즐겨찾기는 시험 폴더 (JJ_SHOT_TMP). +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + final out = Platform.environment['JJ_SHOT_DIR'] ?? p.join(Directory.systemTemp.path, 'jj_shots_662'); + final key = GlobalKey(); + + Future settle(WidgetTester t, [int rounds = 10]) async { + for (var i = 0; i < rounds; i++) { + await t.runAsync(() => Future.delayed(const Duration(milliseconds: 100))); + await t.pump(); + } + } + + Future shot(WidgetTester t, String name) async { + await settle(t, 3); + final b = key.currentContext!.findRenderObject()! as RenderRepaintBoundary; + final img = await t.runAsync(() => b.toImage()); + final bytes = await t.runAsync(() => img!.toByteData(format: ui.ImageByteFormat.png)); + Directory(out).createSync(recursive: true); + File(p.join(out, '$name.png')).writeAsBytesSync(bytes!.buffer.asUint8List()); + } + + AppController plain() { + final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); + c.settings + ..components = ['mkv', 'explorer', 'rsync'] + ..navOrder = ['mkv', 'explorer', 'rsync']; + return c; + } + + Future showHome(WidgetTester t, AppController c, Size size) async { + t.view.physicalSize = size; + t.view.devicePixelRatio = 1; + await t.runAsync(() => t.pumpWidget(MaterialApp( + theme: buildTheme(), + builder: (_, child) => AppScope(controller: c, child: RepaintBoundary(key: key, child: child!)), + home: KeyedSubtree(key: UniqueKey(), child: HomePage(c: c)), + ))); + await settle(t); + } + + Future swipe(WidgetTester t, Size size, double dx) async { + await t.flingFrom(Offset(size.width / 2, size.height / 2), Offset(dx, 0), 2500); + await settle(t, 8); + } + + testWidgets('66-2: 지금 화면 아이콘 (넓은 · 좁은 화면) · 처음으로 알림', (t) async { + for (final (name, size) in [('wide', const Size(1280, 800)), ('narrow', const Size(420, 860))]) { + final c = plain(); + await showHome(t, c, size); + await shot(t, 'b3_662_${name}_mkv_current'); + await swipe(t, size, -size.width * 0.6); // 다음 (파일 탐색기) + await shot(t, 'b3_662_${name}_explorer_current'); + await swipe(t, size, -size.width * 0.6); // Rsync + await swipe(t, size, -size.width * 0.6); // 끝 다음 = 처음 + await shot(t, 'b3_662_${name}_wrap_to_first'); + } + }); + + testWidgets('66-2: 웹 브라우저를 처음 열면 위쪽 막대 아래에 안내 한 줄 · 173 브라우저 아이콘도 지금 위치로 파랗게', (t) async { + final c = plain(); + // 실제 앱처럼 웹 브라우저도 화면 목록에 (그래야 위쪽 막대에 브라우저 아이콘이 있다) + c.settings + ..components = ['mkv', 'browser', 'explorer', 'rsync'] + ..navOrder = ['mkv', 'browser', 'explorer', 'rsync']; + final base = Platform.environment['JJ_SHOT_TMP']; + final dir = (base == null || base.isEmpty ? Directory.systemTemp : (Directory(base)..createSync(recursive: true))) + .createTempSync('jj_shot_662_'); + addTearDown(() => dir.deleteSync(recursive: true)); + final bm = BookmarksController(p.join(dir.path, 'bookmarks.json')); + t.view.physicalSize = const Size(1280, 800); + t.view.devicePixelRatio = 1; + await t.runAsync(() => t.pumpWidget(MaterialApp( + theme: buildTheme(), + builder: (_, child) => AppScope(controller: c, bookmarks: bm, child: RepaintBoundary(key: key, child: child!)), + home: BrowserPage( + c: c, + bookmarks: bm, + initialUrl: 'https://example.com/', + viewBuilder: (BrowserHost h, String url) => const ColoredBox(color: Colors.white), + ), + ))); + await settle(t); + await shot(t, 'b3_662_browser_hint'); + }); +} diff --git a/lib/ui/app_actions.dart b/lib/ui/app_actions.dart index 4fe3a6c..6bc291a 100644 --- a/lib/ui/app_actions.dart +++ b/lib/ui/app_actions.dart @@ -308,7 +308,8 @@ class AppNavButtons extends StatelessWidget { ), btn(const Icon(Icons.arrow_back), tr('뒤로'), // 브라우저 화면: 뒤로 키는 웹 페이지 뒤로 (PopScope) 이지만 이 버튼은 화면 이동이라 바로 닫는다 - atRoot ? null : () => onBrowserPage ? Navigator.pop(context) : Navigator.maybePop(context), + // 위쪽 ← 는 늘 화면 닫기 (브라우저 · 탐색기는 뒤로 키가 웹 뒤로 · 선택 풀기 · 상위 폴더를 먼저 하므로 그것을 거치지 않게 - 49) + atRoot ? null : () => onBrowserPage || onExplorerPage || onRsyncPage ? Navigator.pop(context) : Navigator.maybePop(context), label: tr('뒤로')), ]); } diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index fa2952d..604588b 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -2356,8 +2356,35 @@ class _ExplorerPageState extends State { // ───────── 화면 ───────── + /// 49: 뒤로 키가 화면을 닫기 전에 할 일이 있는지 (고른 것이 있거나 맨 위 폴더가 아님) + bool get _backHasWork => + _selecting || _panes.any((x) => x.marked.isNotEmpty) || (_ready && !samePath(_pane.current, _pane.root)); + + /// 49: 뒤로 키 - 먼저 고른 것 풀기, 그다음 상위 폴더, 맨 위면 화면 닫기 (위쪽 ← 는 늘 닫기) + Future _onBack() async { + if (_selecting || _panes.any((x) => x.marked.isNotEmpty)) { + _setSelecting(false); + return; + } + if (!samePath(_pane.current, _pane.root)) { + await _button(ExplorerButton.up); + return; + } + if (mounted) Navigator.of(context).pop(); + } + @override Widget build(BuildContext context) { + return PopScope( + canPop: !_backHasWork, + onPopInvokedWithResult: (didPop, _) { + if (!didPop) unawaited(_onBack()); + }, + child: _buildPage(context), + ); + } + + Widget _buildPage(BuildContext context) { return CallbackShortcuts( bindings: { const SingleActivator(LogicalKeyboardKey.f5): () => _button(ExplorerButton.refresh), diff --git a/test/explorer_conflict_test.dart b/test/explorer_conflict_test.dart index e8ab087..e85b5a2 100644 --- a/test/explorer_conflict_test.dart +++ b/test/explorer_conflict_test.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; @@ -208,6 +209,40 @@ void main() { expect(find.textContaining('1개를 영구 삭제했습니다.'), findsOneWidget); }, skip: !Platform.isWindows); + testWidgets('49: 뒤로 키 - 먼저 고른 것 풀기 → 상위 폴더 → 맨 위에서 화면 닫기', (tester) async { + tester.view.physicalSize = const Size(1600, 900); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); + // 시험 폴더를 저장 장치 맨 위로 (left 에서 한 번 올라가면 맨 위) + ExplorerPage.debugVolumes = () => [(tmp.path, 'T:')]; + final navKey = GlobalKey(); + await tester.runAsync(() => tester.pumpWidget(MaterialApp(navigatorKey: navKey, home: const Text('앞 화면')))); + unawaited(navKey.currentState!.push(MaterialPageRoute(builder: (_) => ExplorerPage(c: c)))); + await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty); + Future back() async { + await tester.runAsync(() => tester.binding.handlePopRoute()); + await settle(tester, () => false, rounds: 10); + } + + // 표시 (선택 모드) 를 켜고 doc.txt 를 고름 + await tester.runAsync(() => tester.tap(find.text('선택').first)); + await settle(tester, () => false, rounds: 5); + await tester.runAsync(() => tester.tap(find.text('doc.txt').first)); + await settle(tester, () => false, rounds: 5); + expect(find.textContaining('1개 표시함'), findsOneWidget); + await back(); // 1) 고른 것 풀기 (화면은 그대로) + expect(find.textContaining('표시함'), findsNothing); + expect(find.byType(ExplorerPage), findsOneWidget); + expect(find.textContaining(p.join(tmp.path, 'left')), findsWidgets, reason: '아직 left 폴더'); + await back(); // 2) 상위 폴더 (= 맨 위) + expect(find.byType(ExplorerPage), findsOneWidget); + expect(find.textContaining(p.join(tmp.path, 'left')), findsNothing); + await back(); // 3) 맨 위에서는 화면 닫기 + await tester.pumpAndSettle(); // 닫히는 전환 (가짜 시간을 흘려야 끝난다) + expect(find.byType(ExplorerPage), findsNothing); + expect(find.text('앞 화면'), findsOneWidget); + }); + testWidgets('48: 건너뛰기를 고르면 그대로 두고 건너뛴 것을 알린다', (tester) async { await startCopy(tester); await tester.tap(find.text('건너뛰기'));