49 탐색기 뒤로 키: 먼저 고른 것 풀기 → 상위 폴더 → 맨 위에서 화면 닫기 (위쪽 ← 는 늘 닫기)
- 탐색기 · Rsync 화면에 PopScope: 시스템 뒤로 (Android 뒤로 키 · 몸짓 · 마우스 뒤로 · Esc) 만 차례를 거친다 - 위쪽 ← 는 브라우저처럼 늘 바로 닫기 (Navigator.pop) - 시험: 선택 → 뒤로 (풀기) → 뒤로 (상위) → 뒤로 (닫기). 66-2 캡처 시험 (integration_test/windows_662_shot_test) 도 함께 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>묶음3
parent
01600ec69a
commit
de967d190a
|
|
@ -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<void> settle(WidgetTester t, [int rounds = 10]) async {
|
||||
for (var i = 0; i < rounds; i++) {
|
||||
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 100)));
|
||||
await t.pump();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> 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<void> 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<void> 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');
|
||||
});
|
||||
}
|
||||
|
|
@ -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('뒤로')),
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2356,8 +2356,35 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
|
||||
// ───────── 화면 ─────────
|
||||
|
||||
/// 49: 뒤로 키가 화면을 닫기 전에 할 일이 있는지 (고른 것이 있거나 맨 위 폴더가 아님)
|
||||
bool get _backHasWork =>
|
||||
_selecting || _panes.any((x) => x.marked.isNotEmpty) || (_ready && !samePath(_pane.current, _pane.root));
|
||||
|
||||
/// 49: 뒤로 키 - 먼저 고른 것 풀기, 그다음 상위 폴더, 맨 위면 화면 닫기 (위쪽 ← 는 늘 닫기)
|
||||
Future<void> _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),
|
||||
|
|
|
|||
|
|
@ -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<NavigatorState>();
|
||||
await tester.runAsync(() => tester.pumpWidget(MaterialApp(navigatorKey: navKey, home: const Text('앞 화면'))));
|
||||
unawaited(navKey.currentState!.push(MaterialPageRoute<void>(builder: (_) => ExplorerPage(c: c))));
|
||||
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
|
||||
Future<void> 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('건너뛰기'));
|
||||
|
|
|
|||
Loading…
Reference in New Issue