import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:jj_mkvmaker/app/app_controller.dart'; import 'package:jj_mkvmaker/app/settings.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_look.dart'; import 'package:jj_mkvmaker/ui/explorer_page.dart'; import 'package:path/path.dart' as p; /// X-plore 비교 40 (관리자 지시 10/20): 파일 탐색기 목록만의 글자 크기 (80~150%) · 줄 간격 (좁게 · 보통 · 넓게) void main() { late Directory tmp; setUp(() => tmp = Directory.systemTemp.createTempSync('jj_textscale_')); tearDown(() => tmp.deleteSync(recursive: true)); test('설정: 처음은 100% · 보통 (지금 그대로), 저장 · 읽기, 틀린 값은 그대로 쪽으로 · 범위 밖은 끝으로', () { final s = AppSettings(); expect((s.explorer.textScale, s.explorer.rowSpacing), (100, 'normal')); s ..explorer.textScale = 130 ..explorer.rowSpacing = 'wide'; final back = AppSettings.fromJson(s.toJson()); expect((back.explorer.textScale, back.explorer.rowSpacing), (130, 'wide')); expect(AppSettings.fromJson({}).explorer.textScale, 100); expect(AppSettings.fromJson({'explorerTextScale': 'x', 'explorerRowSpacing': 'huge'}).explorer.rowSpacing, 'normal'); expect(AppSettings.fromJson({'explorerTextScale': 300}).explorer.textScale, 150); expect(AppSettings.fromJson({'explorerTextScale': 10}).explorer.textScale, 80); }); /// [name] 글자의 실제 글자 크기 (기기 · 목록 글자 크기를 곱한 것) double shownSize(WidgetTester tester, String name) { final para = tester.renderObject(find.text(name).last); final size = para.text.style?.fontSize ?? 14; return para.textScaler.scale(size); } /// 이름 줄 높이 double rowHeight(WidgetTester tester, String name) => tester.getSize(find.ancestor(of: find.text(name).last, matching: find.byType(KeyedSubtree)).first).height; Future open(WidgetTester tester, String style, String wait, {double device = 1, int scale = 100, String spacing = 'normal'}) async { const pp = MethodChannel('plugins.flutter.io/path_provider'); tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(pp, (_) async => tmp.path); addTearDown(() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(pp, null)); File(p.join(tmp.path, '아주 긴 이름의 동영상 파일 - 글자 크기 시험용 2026 최종본.mp4')).writeAsStringSync('v' * 2000); File(p.join(tmp.path, 'a.txt')).writeAsStringSync('a'); Directory(p.join(tmp.path, 'sub')).createSync(); final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings ..explorer.paths = [tmp.path, tmp.path] // 폴더 + 파일 목록: 오른쪽 목록은 그 폴더의 것만이라 짧다 (트리는 Temp 의 다른 폴더가 많아 아래로 밀린다) ..explorer.layout = 'split' ..explorer.style = style // ✔ 칸 자리를 보는 시험: 표시를 늘 보이게 (설정 - 처음 값은 규칙 4 로 선택 모드에서만) ..explorer.alwaysMarks = true ..explorer.textScale = scale ..explorer.rowSpacing = spacing; await tester.runAsync(() => tester.pumpWidget(MaterialApp( builder: (ctx, child) => MediaQuery(data: MediaQuery.of(ctx).copyWith(textScaler: TextScaler.linear(device)), child: child!), home: ExplorerPage(c: c)))); // 목록을 다 읽을 때까지 (다른 시험과 함께 돌면 느리다 - 10초까지) for (var i = 0; i < 200 && (i < 15 || find.text(wait).evaluate().isEmpty); i++) { await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 50))); await tester.pump(); } return c; } Future close(WidgetTester tester) async { await tester.pumpWidget(const SizedBox()); await tester.pump(const Duration(seconds: 1)); } // 관리자 지시 10/20: 좁은 화면의 "폴더 + 파일 목록" - 트리 창 최소 폭 (120px 또는 38%), 트리에는 ✔ 칸 · 크기 줄 없이 이름이 보임 for (final width in [412.0, 360.0]) { for (final style in ['xplore', 'windows', 'totalcmd']) { testWidgets('좁은 폴더 + 파일 목록 ($width 폭, $style): 트리 창이 넓어 폴더 이름이 보이고 ✔ 칸은 오른쪽 목록에만', (tester) async { tester.view.physicalSize = Size(width, 900); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); await open(tester, style, 'sub'); expect(tester.takeException(), isNull); // 트리 (먼저 그려짐) 의 'sub' 이름이 다 보인다 final tree = find.text(style == 'totalcmd' ? '[sub]' : 'sub').first; final para = tester.renderObject(tree); // 시험 글꼴 (Ahem) 은 글자마다 글자 크기만큼 넓어 실제보다 훨씬 길다 - 이름 칸 폭으로 본다 (실제 글꼴은 캡처 시험) expect(para.constraints.maxWidth, greaterThanOrEqualTo(20), reason: '트리의 폴더 이름 칸이 남는다 (예전 412 폭은 0, 실제 글꼴로 360 폭에서 한 줄 5글자쯤)'); final treeRight = tester.getTopRight(tree).dx; // 관리자 결정 10/23 (E6 ③): 좁아 TC 의 확장자 칸이 없으면 이름에 확장자를 붙인다 - 이 폭에서는 TC 도 'a.txt' final listLeft = tester.getTopLeft(find.text('a.txt')).dx; expect(listLeft, greaterThan(treeRight), reason: '트리는 왼쪽, 목록은 오른쪽'); // 트리 줄에는 ✔ 단추가 없다 (오른쪽 목록 줄에만) - X-plore 는 늘 ✔ if (style == 'xplore') { final marks = find.byTooltip('표시 (여러 개 고르기)'); expect(marks, findsWidgets); for (final m in marks.evaluate()) { expect(tester.getTopLeft(find.byWidget(m.widget)).dx, greaterThan(treeRight), reason: '✔ 는 오른쪽 목록에만'); } } await close(tester); }); } } // 관리자 지시 10/20 (28 검토): 글자가 아주 크면 (기기 1.3 × 150%) 좁은 화면에 두 창이 다 들어가지 않는다 - 그동안만 한 창 (설정은 그대로) for (final style in ['xplore', 'windows', 'totalcmd']) { testWidgets('글자가 아주 크면 (기기 1.3 × 150% · 넓게, 412 폭, $style) 폴더 + 파일 목록 대신 한 창 - 설정은 split 그대로 · 넘침 없음', (tester) async { tester.view.physicalSize = const Size(412, 900); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); final sub = style == 'totalcmd' ? '[sub]' : 'sub'; // 관리자 결정 10/23 (E6 ③): 좁아 TC 의 확장자 칸이 없으면 이름에 확장자를 붙인다 - 이 폭에서는 TC 도 'a.txt' const a = 'a.txt'; final c = await open(tester, style, sub, device: 1.3, scale: 150, spacing: 'wide'); await tester.pump(); expect(tester.takeException(), isNull); expect(c.settings.explorer.layout, 'split', reason: '고른 배치는 바꾸지 않는다'); expect(find.text(sub), findsOneWidget, reason: '트리 · 목록 두 창이 아니라 한 창'); expect(find.text(a), findsOneWidget, reason: '한 창 트리에 파일도 보인다'); final para = tester.renderObject(find.text(sub)); // X-plore 는 이름이 줄 폭을 다 쓰고, Windows · TC 는 열 (날짜 · 유형 · 크기) 이 있어 이름 열이 6/13 쯤 expect(para.constraints.maxWidth, greaterThan(style == 'xplore' ? 100 : 55), reason: '이름이 보인다'); await close(tester); // 글자가 보통이면 같은 폭에서 두 창 await open(tester, style, sub); await tester.pump(); expect(find.text(sub), findsNWidgets(2), reason: '보통 글자면 폴더 + 파일 목록'); await close(tester); }); } // 관리자 지시 10/20: 좁은 창의 열 - 이름 열이 120 보다 좁으면 유형 (TC 확장자) → 날짜 순으로 숨김, 크기는 남김 test('숨길 열 정하기: 넓으면 0 · 좁아질수록 유형 → 날짜 · 열 없는 X-plore 는 0', () { expect(columnHideLevel(ExplorerStyle.windows, 1280), 0); expect(columnHideLevel(ExplorerStyle.windows, 300), 0, reason: '이름 열 (300-16)*6/13 = 131'); expect(columnHideLevel(ExplorerStyle.windows, 300, marks: true), 1, reason: '✔ 칸 (36) 이 있으면 114'); expect(columnHideLevel(ExplorerStyle.windows, 260), 1); expect(columnHideLevel(ExplorerStyle.windows, 180), 2); expect(columnHideLevel(ExplorerStyle.totalcmd, 1280), 0); expect(columnHideLevel(ExplorerStyle.totalcmd, 180), 2); expect(columnHideLevel(ExplorerStyle.xplore, 100), 0); }); for (final style in ['windows', 'totalcmd']) { testWidgets('좁은 한 창 ($style, 412 · 기기 글자 1.3): 이름 열이 좁으면 유형 → 날짜 열을 숨기고 크기는 남김 · 넓으면 다 보임', (tester) async { tester.view.physicalSize = const Size(412, 900); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); final type = style == 'windows' ? '유형' : '확장자'; final date = style == 'windows' ? '수정한 날짜' : '날짜'; final sub = style == 'totalcmd' ? '[sub]' : 'sub'; final c = await open(tester, style, sub, device: 1.3, scale: 150); await tester.pump(); expect(c.settings.explorer.layout, 'split'); expect(tester.takeException(), isNull); expect(find.text(type), findsNothing, reason: '좁아서 유형 열 숨김'); expect(find.text('크기'), findsOneWidget, reason: '크기는 남김'); await close(tester); tester.view.physicalSize = const Size(1600, 900); await open(tester, style, sub); await tester.pump(); expect(find.text(type), findsOneWidget); expect(find.text(date), findsOneWidget); expect(find.text('크기'), findsOneWidget); await close(tester); }); } testWidgets('넓은 화면 (1280) 의 폴더 + 파일 목록은 그대로 (2 : 3, 트리에도 ✔)', (tester) async { tester.view.physicalSize = const Size(1280, 900); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); await open(tester, 'xplore', 'sub'); final treeMarks = find.byTooltip('표시 (여러 개 고르기)').evaluate().where((m) => tester.getTopLeft(find.byWidget(m.widget)).dx < 500); expect(treeMarks, isNotEmpty, reason: '넓은 화면은 지금 그대로'); await close(tester); }); for (final style in ['xplore', 'windows', 'totalcmd']) { // 관리자 결정 10/23 (E6 ③): 좁아 TC 의 확장자 칸이 없으면 이름에 확장자를 붙인다 - 이 폭에서는 TC 도 'a.txt' const a = 'a.txt'; testWidgets('40: $style - 처음 값 (100% · 보통) 은 지금 그대로 · 150% · 넓게는 글자 1.5배 · 줄 높이 늘어남 · 412 폭에서 넘치지 않음', (tester) async { tester.view.physicalSize = const Size(412, 900); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); await open(tester, style, a); expect(tester.takeException(), isNull); final base = shownSize(tester, a), baseH = rowHeight(tester, a); expect(baseH, {'xplore': 52.0, 'windows': 34.0, 'totalcmd': 26.0}[style], reason: '처음 값은 예전 줄 높이 그대로'); await close(tester); await open(tester, style, a, scale: 150, spacing: 'wide'); expect(tester.takeException(), isNull, reason: '412 폭 · 150% · 넓게에서 넘치지 않는다'); expect(shownSize(tester, a), closeTo(base * 1.5, 0.01)); expect(rowHeight(tester, a), (baseH * 1.5 * 1.25).roundToDouble()); await close(tester); // 기기 글자 크기 1.3 과 곱해진다 · 좁게 await open(tester, style, a, device: 1.3, scale: 120, spacing: 'compact'); expect(tester.takeException(), isNull); expect(shownSize(tester, a), closeTo(base * 1.3 * 1.2, 0.01)); expect(rowHeight(tester, a), (baseH * 1.3 * 1.2 * 0.8).roundToDouble()); await close(tester); // 80% 는 글자만 작게 (줄은 예전 높이 - 누르기 크기를 줄이지 않음) await open(tester, style, a, scale: 80); expect(shownSize(tester, a), closeTo(base * 0.8, 0.01)); expect(rowHeight(tester, a), baseH); await close(tester); }); } }