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
parent
0a9a2d0af5
commit
4ebc4f21cc
|
|
@ -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_');
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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)]) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue