WIP ⑦ 밝은 화면 캡처 시험 넓힘: 보관함 · DLNA · 앱 관리자 · Tor · 자막 편집 · Wi-Fi 공유 · 화면 보정 · 662 흔들림 고침 (검토 대기)

관리자 지시 10/21:
- 캡처가 없던 화면 캡처 시험 (windows_vault_shot_test · windows_misc_shot_test), Wi-Fi 공유 · 화면 보정 캡처에도 JJ_SHOT_THEME
- 662 캡처 흔들림: 크기마다 새 Navigator (앞 크기에서 밀어 연 화면이 남아 다음 첫 캡처가 다른 화면이 됨) · 밀기 그림이 끝날 때까지 기다림
- 확인: 밝은 화면 캡처를 보고 읽기 어려운 곳 없음, 어두운 화면은 0a9a2d0 과 픽셀로 견줘 흔들림 밖 차이 0, 662 는 두 번 찍어 같음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xplore_p3b
RyuJongJin 2026-10-21 17:44:20 +09:00
parent 0a9a2d0af5
commit 4ebc4f21cc
5 changed files with 224 additions and 0 deletions

View File

@ -18,6 +18,8 @@ import 'package:path/path.dart' as p;
/// 실제 FFmpeg: 가로 영상 → 화면 · 색 보정 창 미리보기 → 세로 9:16 + 채도 MKV 만들기 /// 실제 FFmpeg: 가로 영상 → 화면 · 색 보정 창 미리보기 → 세로 9:16 + 채도 MKV 만들기
void main() { void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized(); IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// ⑦ 밝은 화면 캡처: JJ_SHOT_THEME=light
if (Platform.environment['JJ_SHOT_THEME'] == 'light') JjColors.use(Brightness.light);
testWidgets('화면 · 색 보정: 미리보기와 세로 9:16 MKV', (tester) async { testWidgets('화면 · 색 보정: 미리보기와 세로 9:16 MKV', (tester) async {
final dir = testTempDir('jj_adj_e2e_'); final dir = testTempDir('jj_adj_e2e_');

View File

@ -53,7 +53,9 @@ void main() {
Future<void> showHome(WidgetTester t, AppController c, Size size) async { Future<void> showHome(WidgetTester t, AppController c, Size size) async {
t.view.physicalSize = size; t.view.physicalSize = size;
t.view.devicePixelRatio = 1; t.view.devicePixelRatio = 1;
// 새 Navigator 로 (앞 크기에서 밀어 연 화면이 남지 않게 - 남으면 다음 크기의 첫 캡처가 다른 화면이 됨)
await t.runAsync(() => t.pumpWidget(MaterialApp( await t.runAsync(() => t.pumpWidget(MaterialApp(
key: UniqueKey(),
theme: buildTheme(), theme: buildTheme(),
builder: (_, child) => AppScope(controller: c, child: RepaintBoundary(key: key, child: child!)), builder: (_, child) => AppScope(controller: c, child: RepaintBoundary(key: key, child: child!)),
home: KeyedSubtree(key: UniqueKey(), child: HomePage(c: c)), home: KeyedSubtree(key: UniqueKey(), child: HomePage(c: c)),
@ -64,6 +66,11 @@ void main() {
Future<void> swipe(WidgetTester t, Size size, double dx) async { 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 t.flingFrom(Offset(size.width / 2, size.height / 2), Offset(dx, 0), 2500);
await settle(t, 8); await settle(t, 8);
// 밀기 그림 (화면 전환) 이 다 끝날 때까지 - 그리기가 더 남아 있지 않을 때까지 (최대 5초)
for (var i = 0; i < 50 && t.binding.hasScheduledFrame; i++) {
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 100)));
await t.pump();
}
} }
testWidgets('66-2: 지금 화면 아이콘 (넓은 · 좁은 화면) · 처음으로 알림', (t) async { testWidgets('66-2: 지금 화면 아이콘 (넓은 · 좁은 화면) · 처음으로 알림', (t) async {

View File

@ -0,0 +1,127 @@
import 'dart:io';
import 'dart:typed_data';
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/ui/app_actions.dart';
import 'package:jj_mkvmaker/app/subtitle_editor_controller.dart';
import 'package:jj_mkvmaker/core/app_manager.dart';
import 'package:jj_mkvmaker/core/dlna.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/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_manager_page.dart';
import 'package:jj_mkvmaker/ui/dlna_page.dart';
import 'package:jj_mkvmaker/ui/subtitle_editor_page.dart';
import 'package:jj_mkvmaker/ui/theme.dart';
import 'package:jj_mkvmaker/ui/tor_page.dart';
import 'package:path/path.dart' as p;
/// ⑦ 밝은 화면 (관리자 지시 10/21): 캡처가 없던 화면 (DLNA · 앱 관리자 · Tor · 자막 편집) 을 PNG 로 (JJ_SHOT_DIR, JJ_SHOT_THEME=light).
/// 기기 · 네트워크 대신 가짜를 쓰고, 설정은 메모리에만
class _Apps implements AppManagerBackend {
@override
Future<List<InstalledApp>> list() async => [
for (final (i, n) in ['카메라', 'Chrome', 'JJ_MKVMaker', '메모'].indexed)
InstalledApp(
package: 'com.x.$i',
label: n,
version: '1.$i',
apk: '',
installed: DateTime(2026, 1, i + 1),
updated: DateTime(2026, 2, i + 1),
size: (i + 1) * 12000000,
system: i == 0,
splits: 0),
];
@override
Future<Uint8List?> icon(String package) async => null;
@override
Future<bool> details(String package) async => true;
@override
Future<bool> uninstall(String package) async => true;
@override
Future<String> backupDir() async => Directory.systemTemp.path;
}
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
if (Platform.environment['JJ_SHOT_THEME'] == 'light') JjColors.use(Brightness.light);
testWidgets('캡처 없던 화면 그림', (t) async {
final out = Platform.environment['JJ_SHOT_DIR'] ?? p.join(Directory.systemTemp.path, 'jj_shots_misc');
Directory(out).createSync(recursive: true);
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
c.settings
..torNoticeSeen = true
..components = [...c.settings.components, 'tor'];
final key = GlobalKey();
Future<void> shot(String name, Size size, Widget home, {Future<void> Function()? then}) async {
t.view.physicalSize = size;
t.view.devicePixelRatio = 1;
await t.runAsync(() => t.pumpWidget(MaterialApp(
key: UniqueKey(),
theme: buildTheme(),
debugShowCheckedModeBanner: false,
builder: (_, child) => AppScope(controller: c, child: RepaintBoundary(key: key, child: child!)),
home: home,
)));
for (var i = 0; i < 15; i++) {
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 100)));
await t.pump(const Duration(milliseconds: 50));
}
if (then != null) {
await then();
for (var i = 0; i < 10; i++) {
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 100)));
await t.pump(const Duration(milliseconds: 50));
}
}
final boundary = key.currentContext!.findRenderObject()! as RenderRepaintBoundary;
final image = await t.runAsync(() => boundary.toImage());
final bytes = await t.runAsync(() => image!.toByteData(format: ui.ImageByteFormat.png));
File(p.join(out, 'misc_$name.png')).writeAsBytesSync(bytes!.buffer.asUint8List());
await t.pumpWidget(const SizedBox());
}
const nas = DlnaServer('집 NAS', 'http://192.168.0.5/d.xml', 'http://192.168.0.5/ctl');
Widget dlna() => DlnaPage(
c: c,
discover: () async => [nas],
browse: (s, id, start) async => (
entries: [
const DlnaEntry('v', '동영상', isContainer: true, childCount: 2),
const DlnaEntry('x', '여행.mp4', upnpClass: 'object.item.videoItem'),
],
total: 2
),
);
await shot('dlna_servers', const Size(960, 600), dlna());
await shot('dlna_folder', const Size(960, 600), dlna(), then: () => t.tap(find.text('집 NAS')));
await shot('apps_412', const Size(412, 800), AppManagerPage(backend: _Apps()));
await shot('tor_960', const Size(960, 600), TorPage(
c: c,
startTor: () async {},
missingFeatures: () async => const [],
applyProxy: () async => true,
onLeft: () async {},
viewBuilder: (_, _) => const ColoredBox(color: Colors.white),
));
final v = VideoItem(r'D:\m\영화.mkv');
final s = SubtitleEntry.external(path: r'D:\m\영화.ko.srt', language: languageOf('ko'));
v.subtitles.add(s);
final editor = SubtitleEditorController(
parseSrt('1\n00:00:01,000 --> 00:00:03,000\n첫째\n\n2\n00:00:05,000 --> 00:00:07,000\n둘째\n\n3\n00:00:09,000 --> 00:00:11,000\n셋째\n'),
saveCharset: 'UTF-8');
await shot('subtitle_960', const Size(960, 600), SubtitleEditorPage(app: c, video: v, entry: s, editor: editor));
t.view.reset();
});
}

View File

@ -0,0 +1,86 @@
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/core/vault_store.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/theme.dart';
import 'package:jj_mkvmaker/ui/vault_page.dart';
import 'package:path/path.dart' as p;
/// 암호 보관함 화면 (만들기 · 목록 (폴더 · 파일)) 을 PNG 로 (JJ_SHOT_DIR, JJ_SHOT_THEME=light 면 밝은 화면).
/// 보관함 · 시험 파일은 임시 폴더에만 만들고 끝나면 지운다
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
if (Platform.environment['JJ_SHOT_THEME'] == 'light') JjColors.use(Brightness.light);
testWidgets('보관함 화면 그림', (t) async {
final out = Platform.environment['JJ_SHOT_DIR'] ?? p.join(Directory.systemTemp.path, 'jj_shots_vault');
Directory(out).createSync(recursive: true);
final tmp = Directory.systemTemp.createTempSync('jj_vaultshot_');
addTearDown(() => tmp.deleteSync(recursive: true));
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
c.settings.vaultEnabled = true;
final key = GlobalKey();
Future<void> shot(String name, Size size, Directory dir) async {
vaultDirOf = () async => dir;
t.view.physicalSize = size;
t.view.devicePixelRatio = 1;
await t.runAsync(() => t.pumpWidget(RepaintBoundary(
key: key, child: MaterialApp(theme: buildTheme(), debugShowCheckedModeBanner: false, home: VaultPage(c: c)))));
for (var i = 0; i < 15; i++) {
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 100)));
await t.pump(const Duration(milliseconds: 50));
}
final boundary = key.currentContext!.findRenderObject()! as RenderRepaintBoundary;
final image = await t.runAsync(() => boundary.toImage());
final bytes = await t.runAsync(() => image!.toByteData(format: ui.ImageByteFormat.png));
File(p.join(out, 'vault_$name.png')).writeAsBytesSync(bytes!.buffer.asUint8List());
await t.pumpWidget(const SizedBox());
}
// 만들기 화면
await shot('create_412', const Size(412, 800), Directory(p.join(tmp.path, 'new')));
// 열린 목록: 보관함을 미리 만들고 [열기] 까지
final dir = Directory(p.join(tmp.path, 'vault'));
final v = await t.runAsync(() => Vault.create(dir, 'pw', iterations: 1000));
final f = File(p.join(tmp.path, '사진.jpg'))..writeAsBytesSync(List.filled(2048, 7));
await t.runAsync(() async {
await v!.add(f.path);
await v.add(f.path, folder: '여행');
await v.mkdir('빈 폴더');
});
vaultIterationsForTest = 1000;
for (final (name, size) in [('open_412', const Size(412, 800)), ('open_960', const Size(960, 600))]) {
vaultDirOf = () async => dir;
t.view.physicalSize = size;
t.view.devicePixelRatio = 1;
await t.runAsync(() => t.pumpWidget(RepaintBoundary(
key: key, child: MaterialApp(theme: buildTheme(), debugShowCheckedModeBanner: false, home: VaultPage(c: c)))));
for (var i = 0; i < 10; i++) {
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 100)));
await t.pump(const Duration(milliseconds: 50));
}
await t.enterText(find.byType(TextField), 'pw');
await t.tap(find.byType(FilledButton));
for (var i = 0; i < 30 && find.text('사진.jpg').evaluate().isEmpty; i++) {
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 100)));
await t.pump(const Duration(milliseconds: 50));
}
await t.pump(const Duration(milliseconds: 300));
final boundary = key.currentContext!.findRenderObject()! as RenderRepaintBoundary;
final image = await t.runAsync(() => boundary.toImage());
final bytes = await t.runAsync(() => image!.toByteData(format: ui.ImageByteFormat.png));
File(p.join(out, 'vault_$name.png')).writeAsBytesSync(bytes!.buffer.asUint8List());
await t.pumpWidget(const SizedBox());
}
t.view.reset();
});
}

View File

@ -21,6 +21,8 @@ import 'package:path/path.dart' as p;
/// 크기 412 · 960, 언어 ko · en · ja. 실행: flutter test integration_test\windows_wifi_shot_test.dart -d windows /// 크기 412 · 960, 언어 ko · en · ja. 실행: flutter test integration_test\windows_wifi_shot_test.dart -d windows
void main() { void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized(); IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// ⑦ 밝은 화면 캡처: JJ_SHOT_THEME=light
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_wifi'); final out = Platform.environment['JJ_SHOT_DIR'] ?? p.join(Directory.systemTemp.path, 'jj_shots_wifi');
for (final lang in ['ko', 'en', 'ja']) { for (final lang in ['ko', 'en', 'ja']) {
for (final (w, h) in [(412, 900), (960, 600)]) { for (final (w, h) in [(412, 900), (960, 600)]) {