JJ_MKVMaker/integration_test/windows_ui2_shot_test.dart

128 lines
7.3 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import 'dart:async';
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/app/subtitle_editor_controller.dart';
import 'package:jj_mkvmaker/core/encode_options.dart';
import 'package:jj_mkvmaker/core/languages.dart';
import 'package:jj_mkvmaker/core/models.dart';
import 'package:jj_mkvmaker/core/srt.dart';
import 'package:jj_mkvmaker/l10n/tr.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/subtitle_editor_page.dart';
import 'package:jj_mkvmaker/ui/theme.dart';
import 'package:jj_mkvmaker/ui/video_adjust_dialog.dart';
import 'package:path/path.dart' as p;
import 'support/temp_dir.dart';
/// 관리자 지시 (10/21, 28 찾음): ① 화면 · 색 보정 창 - 고른 비율 칩 ("세로 9:16") 은 ✓ 만 (그림 위에 체크가 겹치지 않게)
/// ② 자막 편집 화면 - 제목은 "자막 편집", 파일 이름은 그 아래 한 줄 (가운데를 줄여 끝 .srt 가 보이게). ko · ja × 360 · 412 · 960
/// F11 (7247aeb, after_1020 에서 옮김): 360 에서도 [✕] 는 화면 안 · [모두 초기화] 는 도움말 이름 그대로 · 안내 글은 숨기지 않음
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
final out = Platform.environment['JJ_SHOT_DIR'] ?? '';
for (final lang in ['ko', 'ja']) {
for (final (w, h) in [(360, 780), (412, 900), (960, 600)]) {
testWidgets('화면 · 색 보정 칩 · 자막 편집 제목 $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));
final tmp = testTempDir('jj_ui2shot_');
appTempInto(tmp);
final video = p.join(tmp.path, '아주 긴 이름의 시험용 동영상 파일 - 제1화 고화질 버전.mp4');
final ffmpeg = p.join(Directory.current.path, 'third_party', 'ffmpeg', 'windows', 'ffmpeg.exe');
final r = await t.runAsync(() => Process.run(ffmpeg, [
'-y', '-v', 'error', '-f', 'lavfi', '-i', 'testsrc2=size=640x360:rate=10:duration=2', //
'-c:v', 'libx264', '-pix_fmt', 'yuv420p', video,
]));
expect(r!.exitCode, 0, reason: '${r.stderr}');
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool.locate(), storage: DesktopStorageService()));
await t.runAsync(() async {
await c.init();
await c.addVideos([video]);
});
final shotKey = GlobalKey();
final navKey = GlobalKey<NavigatorState>();
await t.pumpWidget(MaterialApp(
theme: buildTheme(),
debugShowCheckedModeBanner: false,
navigatorKey: navKey,
builder: (context, child) => RepaintBoundary(key: shotKey, child: child),
home: const Scaffold(body: SizedBox.expand()),
));
Future<void> settle([int n = 10]) async {
for (var i = 0; i < n; i++) {
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 80)));
await t.pump();
}
}
Future<void> shot(String name) async {
await settle(3);
// 넘쳐도 무엇이 넘쳤는지 볼 수 있게 먼저 찍는다
if (out.isNotEmpty) {
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, 'ui2_${name}_${lang}_$w.png')).writeAsBytesSync(bytes!.buffer.asUint8List());
}
expect(t.takeException(), isNull, reason: '$name 넘침');
}
// ① 화면 · 색 보정 - "세로 9:16" 을 고른다
unawaited(showVideoAdjust(navKey.currentContext!, c));
await settle(20);
final portrait = find.text(FrameChoice.portrait.label);
await t.ensureVisible(portrait);
await t.tap(portrait);
await settle();
final chip = find.ancestor(of: portrait, matching: find.byType(ChoiceChip));
expect(t.widget<ChoiceChip>(chip).selected, isTrue);
expect(t.widget<ChoiceChip>(chip).showCheckmark, isFalse, reason: '그림 위에 체크가 겹치지 않게');
expect(find.descendant(of: chip, matching: find.byIcon(Icons.check)), findsOneWidget, reason: '✓ 만');
expect(find.descendant(of: chip, matching: find.byIcon(Icons.crop_portrait)), findsNothing);
await shot('1_adjust');
// F11 (관리자 10/21): 좁아도 [✕] 는 화면 안, [모두 초기화] 는 (좁으면 아이콘이라도) 도움말 이름 그대로
final close = t.getRect(find.byTooltip(tr('닫기')));
expect(close.right <= w + 0.5, isTrue, reason: '✕ 가 화면 안 ($close)');
expect(find.byTooltip(tr('모두 초기화')).evaluate().isNotEmpty || find.text(tr('모두 초기화')).evaluate().isNotEmpty, isTrue);
expect(find.textContaining(tr('MKV 만들기에 적용됩니다 (영상을 다시 인코딩)')), findsOneWidget, reason: '안내 글은 숨기지 않는다');
navKey.currentState!.popUntil((r) => r.isFirst);
await settle();
// ② 자막 편집 - 긴 파일 이름
final v = c.videos.single;
final s = SubtitleEntry.external(path: p.join(tmp.path, '아주 긴 이름의 시험용 동영상 파일 - 제1화 고화질 버전.ko.srt'), language: languageOf('ko'));
v.subtitles.add(s);
final editor = SubtitleEditorController(parseSrt('1\n00:00:01,000 --> 00:00:02,000\n안녕\n'), saveCharset: 'UTF-8');
unawaited(navKey.currentState!.push(MaterialPageRoute<void>(builder: (_) => SubtitleEditorPage(app: c, video: v, entry: s, editor: editor))));
await settle(12);
expect(find.text(tr('자막 편집')), findsOneWidget, reason: '제목은 "자막 편집" 만');
final name = find.textContaining('.srt');
expect(name, findsOneWidget, reason: '파일 이름은 아래 한 줄 - 끝 (.srt) 이 보임');
await shot('2_editor');
// F9 (관리자 10/21, 7247aeb 에서 옮김): 줄의 단추는 줄이지 않는다 - 기본 누를 자리 48 (+ 기기의 밀도 조정: Windows 는 compact 라 40, Android 는 48)
final density = Theme.of(t.element(find.byType(SubtitleEditorPage))).visualDensity.baseSizeAdjustment.dx;
for (final tip in [tr('아래에 줄 추가'), tr('줄 삭제')]) {
final r = t.getRect(find.byTooltip(tip).first);
expect(r.width >= 48 + density - 0.5 && r.height >= 48 + density - 0.5, isTrue, reason: '$tip 누를 자리 $r (밀도 조정 $density)');
}
expect(t.widget<Text>(name).data!.endsWith('.srt'), isTrue, reason: '${t.widget<Text>(name).data}');
});
}
}
}