JJ_MKVMaker/integration_test/windows_b6_shot_test.dart

185 lines
8.3 KiB
Dart

import 'dart:io';
import 'dart:ui' as ui;
import 'package:archive/archive_io.dart';
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/core/file_ops.dart';
import 'package:jj_mkvmaker/core/models.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/archive_ui.dart';
import 'package:jj_mkvmaker/ui/batch_rename_dialog.dart';
import 'package:jj_mkvmaker/ui/explorer_drag.dart';
import 'package:jj_mkvmaker/ui/explorer_page.dart' show TransferProgressChip;
import 'package:jj_mkvmaker/ui/explorer_search.dart';
import 'package:jj_mkvmaker/ui/quick_look.dart';
import 'package:jj_mkvmaker/ui/storage_page.dart';
import 'package:jj_mkvmaker/ui/theme.dart';
import 'support/temp_dir.dart';
import 'package:path/path.dart' as p;
/// 관리자 지시 5 (10/17): 기기 없이 15 가 볼 수 있게 묶음6 새 화면을 PNG 로 (JJ_SHOT_DIR, 없으면 임시 폴더).
/// 78 이름 일괄 창 · 79 찾기 결과 · 81 빠른 보기 · 82 놓기 묻기 창 · 85 작은 표시 (보통 · 멈춤) · 86 풀기 확인 창 (경고 줄) · 87 분석 화면.
/// 크기 412 (폰 세로) · 960, 언어 ko · en · ja. 시험 파일은 임시 폴더에만 (끝나면 지움), 설정은 메모리에만.
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
final out = Platform.environment['JJ_SHOT_DIR'] ?? p.join(Directory.systemTemp.path, 'jj_shots_b6');
for (final lang in ['ko', 'en', 'ja']) {
for (final (w, h) in [(412, 900), (960, 600)]) {
testWidgets('묶음6 화면 $lang ${w}x$h', (t) async {
t.view.physicalSize = Size(w.toDouble(), h.toDouble());
t.view.devicePixelRatio = 1;
addTearDown(t.view.reset);
await t.runAsync(() => i18n.apply(lang, save: false));
addTearDown(() => i18n.apply('ko', save: false));
// 관리자 지시 (10/24): 폴더 이름 · 시각을 실행마다 같게 (캡처가 흔들리지 않게 - 시험 쪽에서만)
final tmp = testFixedTempDir('jj_b6shot_fixed');
// 시험 파일
File(p.join(tmp.path, 'Show', 'ep01.mkv'))
..createSync(recursive: true)
..writeAsBytesSync(List.filled(4000, 0));
File(p.join(tmp.path, 'Show', 'ep02.mkv')).writeAsBytesSync(List.filled(3000, 0));
File(p.join(tmp.path, 'Show', 'Show 01.mkv')).writeAsBytesSync(List.filled(10, 0));
File(p.join(tmp.path, 'Show', 'ep01.srt')).writeAsStringSync('1\n00:00:01,000 --> 00:00:02,000\n안녕 hello\n');
File(p.join(tmp.path, 'docs', 'note.txt'))
..createSync(recursive: true)
..writeAsStringSync('메모 note');
pinTestTimes(tmp);
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
final navKey = GlobalKey<NavigatorState>();
final shotKey = GlobalKey();
await t.pumpWidget(RepaintBoundary(
key: shotKey,
// DEBUG 띠가 오른쪽 위 단추 (79 [모두 선택]) 를 가리지 않게 (15 지적)
child: MaterialApp(
theme: buildTheme(), debugShowCheckedModeBanner: false, navigatorKey: navKey, home: const Scaffold(body: SizedBox.expand())),
));
BuildContext ctx() => navKey.currentContext!;
Future<void> settle([int n = 8]) async {
for (var i = 0; i < n; i++) {
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 60)));
await t.pump();
}
}
Future<void> shot(String name) async {
await settle(3);
final b = shotKey.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, 'b6_${name}_${lang}_$w.png')).writeAsBytesSync(bytes!.buffer.asUint8List());
}
Future<void> closeAll() async {
navKey.currentState!.popUntil((r) => r.isFirst);
await settle(4);
}
// 78 이름 일괄 바꾸기 - 틀 "Show [C]" (첫 번째가 고르지 않은 "Show 01.mkv" 와 겹쳐 빨갛게)
final eps = [p.join(tmp.path, 'Show', 'ep01.mkv'), p.join(tmp.path, 'Show', 'ep02.mkv')];
showBatchRename(ctx(), eps, isDir: (_) => false);
await settle();
await t.enterText(find.byType(TextField).first, 'Show [C]');
await settle();
await shot('78_rename');
await closeAll();
// 79 찾기 결과 - 하나 골라 아래 할 일 줄까지
navKey.currentState!.push(MaterialPageRoute<void>(
builder: (_) => SearchResultsPage(
root: tmp.path,
cond: const SearchConditions(name: 'ep'),
showHidden: false,
otherDir: p.join(tmp.path, 'docs'),
onOpen: (_) async {},
),
));
await settle(12);
await t.tap(find.byType(Checkbox).first);
await settle();
await shot('79_search');
await closeAll();
// 81 빠른 보기 - 자막 (글 미리 보기)
final list = await t.runAsync(() => listEntries(p.join(tmp.path, 'Show')));
final srt = list!.indexWhere((e) => e.name == 'ep01.srt');
showDialog<void>(
context: ctx(),
builder: (_) => QuickLook(
entries: list,
index: srt,
probe: (_) async => const MediaInfo(streams: []),
thumbnail: (_) async => null,
imageExts: const ['jpg', 'png'],
onOpen: (_) {},
),
);
await settle(10);
await shot('81_quicklook');
await closeAll();
// 82 놓기 묻기 창
askDropAction(ctx(), eps, p.join(tmp.path, 'docs'));
await settle();
await shot('82_drop_ask');
await closeAll();
// 85 작은 표시 - 보통 · 멈춤
for (final (name, failed) in [('85_chip', false), ('85_chip_failed', true)]) {
navKey.currentState!.push(MaterialPageRoute<void>(
builder: (_) => Scaffold(
body: Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: const EdgeInsets.all(8),
child: TransferProgressChip(running: failed ? 0 : 2, percent: failed ? null : 45, failed: failed, onTap: () {}),
),
),
),
));
await settle();
await shot(name);
await closeAll();
}
// 86 풀기 확인 창 - 밖으로 나가는 경로 · 링크 · 쓸 수 없는 이름 · 덮어쓰기 경고
final a = Archive()
..addFile(ArchiveFile.string('ok/a.txt', 'a'))
..addFile(ArchiveFile.string('../evil.txt', 'x'))
..addFile(ArchiveFile.string('CON.txt', 'x'))
..addFile(ArchiveFile.string('lnk', '../x')..mode = 0xA1FF);
final zip = p.join(tmp.path, 'sample.zip');
File(zip).writeAsBytesSync(ZipEncoder().encode(a));
extractArchiveFlow(ctx(), c, zip);
await settle(15);
await shot('86_extract_confirm');
// 같은 이름: 덮어쓰기 → "되돌릴 수 없습니다"
await t.tap(find.byType(DropdownButton<NameConflict>));
await settle();
await t.tap(find.text(lang == 'ko' ? '덮어쓰기' : (lang == 'en' ? 'Overwrite' : '上書き')).last);
await settle();
await shot('86_extract_overwrite');
await closeAll();
// 87 분석 화면 - 큰 폴더 · 큰 파일
navKey.currentState!.push(MaterialPageRoute<void>(builder: (_) => StorageAnalysisPage(root: tmp.path)));
await settle(15);
await shot('87_storage_folders');
await t.tap(find.byType(ChoiceChip).last);
await settle();
await shot('87_storage_files');
await closeAll();
});
}
}
}