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/i18n_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/explorer_page.dart'; import 'package:jj_mkvmaker/ui/theme.dart'; import 'package:path/path.dart' as p; import 'support/temp_dir.dart'; import '../test/support/test_explorer_device.dart'; /// 관리자 지시 (10/21, 28 지적): Windows · TC 스타일의 날짜 열이 시간까지 들어가지 않으면 "2026-10-2…" 로 잘리던 것 - /// 모자라면 짧은 꼴 (올해 "10-21 14:05" · 아니면 "2025-10-21"), 그래도 모자라면 숨김. 실제 글꼴로 312 조합 /// (폭 13 × 스타일 2 × 언어 3 × 글자 배율 4) 에서 날짜 칸이 잘리지 않는지 (RenderParagraph.didExceedMaxLines) 와 /// 날짜를 보일 수 있는 넓은 창에서는 다 쓴 꼴이 나오는지 본다. 좁은 몇 개는 PNG (JJ_SHOT_DIR). void main() { late TestExplorerDevice device; setUp(() => device = TestExplorerDevice()); IntegrationTestWidgetsFlutterBinding.ensureInitialized(); if (Platform.environment['JJ_SHOT_THEME'] == 'light') JjColors.use(Brightness.light); final out = Platform.environment['JJ_SHOT_DIR'] ?? p.join(Directory.systemTemp.path, 'jj_shots_datecol'); final dateLike = RegExp(r'^(\d{4}[-/])?\d{2}[-/]\d{2}( \d{2}:\d{2})?$'); testWidgets('날짜 열: 312 조합에서 잘림 0', (t) async { final tmp = testTempDir('jj_datecol_'); final dir = Directory(p.join(tmp.path, 'files'))..createSync(); final now = DateTime.now(); for (final (n, at) in [ ('올해 사진.jpg', DateTime(now.year, 12, 28, 23, 58).isAfter(now) ? now : DateTime(now.year, 12, 28, 23, 58)), ('작년 문서.pdf', DateTime(now.year - 1, 12, 28, 23, 58)), ]) { File(p.join(dir.path, n)) ..writeAsStringSync('x') ..setLastModifiedSync(at); } device.volumes = () => [(dir.path, 'T:')]; final key = GlobalKey(); var checked = 0, short = 0, hidden = 0; final emptyOnce = []; final widths = [300, 340, 360, 390, 412, 460, 520, 600, 700, 800, 960, 1280, 1600]; final scales = [(1.0, 80), (1.0, 100), (1.0, 150), (1.3, 150)]; for (final lang in ['ko', 'ja', 'zh-Hans']) { await t.runAsync(() => i18n.apply(lang, save: false)); for (final style in ['windows', 'totalcmd']) { for (final (dev, list) in scales) { for (final w in widths) { final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings ..explorerStyle = style ..explorerLayout = 'single' ..explorerTextScale = list ..explorerPaths = [dir.path, dir.path]; t.view.physicalSize = Size(w, 800); t.view.devicePixelRatio = 1; await t.runAsync(() => t.pumpWidget(RepaintBoundary( key: key, child: MaterialApp( theme: buildTheme(), debugShowCheckedModeBanner: false, builder: (ctx, child) => MediaQuery(data: MediaQuery.of(ctx).copyWith(textScaler: TextScaler.linear(dev)), child: child!), home: ExplorerPage(key: UniqueKey(), device: device, c: c), ), ))); for (var i = 0; i < 200 && find.textContaining('작년 문서').evaluate().isEmpty; i++) { await t.runAsync(() => Future.delayed(const Duration(milliseconds: 50))); await t.pump(); } await t.pump(const Duration(milliseconds: 100)); final name = '${lang}_${style}_${w.round()}_d${dev}_l$list'; if (find.textContaining('작년 문서').evaluate().isEmpty) { // 목록이 빈 채로 남는 일이 가끔 있다 (빙글이 없이 - 3a 에 알림). 그림을 남기고 [새로 고침] 으로 한 번 더 emptyOnce.add(name); final r = key.currentContext!.findRenderObject()! as RenderRepaintBoundary; final img = await t.runAsync(() => r.toImage()); final bytes = await t.runAsync(() => img!.toByteData(format: ui.ImageByteFormat.png)); Directory(out).createSync(recursive: true); File(p.join(out, 'datecol_EMPTY_$name.png')).writeAsBytesSync(bytes!.buffer.asUint8List()); await t.tap(find.byIcon(Icons.refresh).first); for (var i = 0; i < 200 && find.textContaining('작년 문서').evaluate().isEmpty; i++) { await t.runAsync(() => Future.delayed(const Duration(milliseconds: 50))); await t.pump(); } await t.pump(const Duration(milliseconds: 100)); } expect(find.textContaining('작년 문서'), findsWidgets, reason: '$name 목록'); final dates = find.byWidgetPredicate((x) => x is Text && x.data != null && dateLike.hasMatch(x.data!)); for (final e in dates.evaluate()) { final para = e.findRenderObject()! as RenderParagraph; expect(para.didExceedMaxLines, isFalse, reason: '$name: "${(e.widget as Text).data}" 잘림'); if (!(e.widget as Text).data!.contains(':') || !(e.widget as Text).data!.startsWith('20')) short++; } if (dates.evaluate().isEmpty) hidden++; // 넓은 창 (1280 이상) 에서는 다 쓴 꼴 (연도 + 시각) if (w >= 1280 && dev == 1.0 && list <= 100) { expect(dates.evaluate().map((e) => (e.widget as Text).data!).every((x) => x.length == 16), isTrue, reason: '$name 다 쓴 꼴'); } if (w == 360 || w == 412) { final r = key.currentContext!.findRenderObject()! as RenderRepaintBoundary; final img = await t.runAsync(() => r.toImage()); final bytes = await t.runAsync(() => img!.toByteData(format: ui.ImageByteFormat.png)); Directory(out).createSync(recursive: true); File(p.join(out, 'datecol_$name.png')).writeAsBytesSync(bytes!.buffer.asUint8List()); } checked++; await t.pumpWidget(const SizedBox()); } } } } await t.runAsync(() => i18n.apply('ko', save: false)); t.view.reset(); expect(checked, 312); // ignore: avoid_print print('날짜 열 조합 $checked · 짧은 꼴 칸 $short · 날짜 숨긴 조합 $hidden · 목록이 빈 채였던 조합 $emptyOnce'); }, timeout: const Timeout(Duration(minutes: 30))); }