Add app source and tests

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
alvocool
2026-10-11 10:15:30 +03:00
parent 7e246255c2
commit 38881d1fe1
135 changed files with 21057 additions and 0 deletions

View File

@@ -0,0 +1,49 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:peach/Peach/about/internal/application/ServiceConfig.dart';
import 'package:peach/Peach/about/internal/file/BuildService.dart';
void main() {
test('runs custom build command and picks the runnable jar', () async {
// Fake Gradle project with several jars in build/libs.
final dir = Directory.systemTemp.createTempSync('peachdeploy_build_test');
addTearDown(() => dir.deleteSync(recursive: true));
File('${dir.path}/build.gradle').writeAsStringSync('// fake');
final libs = Directory('${dir.path}/build/libs')..createSync(recursive: true);
// Distractors that must be excluded, plus the real artifact.
File('${libs.path}/app-sources.jar').writeAsStringSync('x');
File('${libs.path}/app-plain.jar').writeAsStringSync('x');
File('${libs.path}/app.jar').writeAsStringSync('runnable');
final svc = ServiceConfig(
name: 'demo',
localProjectPath: dir.path,
// Trivial command so we don't need a real Gradle install.
buildCommand: Platform.isWindows ? 'echo building' : 'true',
);
final build = BuildService();
addTearDown(build.dispose);
final result = await build.build(svc);
expect(result.ok, isTrue, reason: result.message);
expect(result.jar, isNotNull);
expect(result.jar!.path.replaceAll('\\', '/'), endsWith('app.jar'));
});
test('reports a helpful error when no project is detected', () async {
final dir = Directory.systemTemp.createTempSync('peachdeploy_empty_test');
addTearDown(() => dir.deleteSync(recursive: true));
final svc = ServiceConfig(name: 'empty', localProjectPath: dir.path);
final build = BuildService();
addTearDown(build.dispose);
final result = await build.build(svc);
expect(result.ok, isFalse);
expect(result.message.toLowerCase(), contains('detect'));
});
}

View File

@@ -0,0 +1,197 @@
// Renders the Peach app icon to assets/icon/ using Flutter's software
// rasterizer (full anti-aliasing, no external image tools needed).
//
// flutter test test/tools/generate_icon_test.dart
//
// Then apply everywhere with: dart run flutter_launcher_icons
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
const _size = 1024.0;
// Brand gradient (matches PeachTheme.brandGradient / the splash tile).
const _g1 = Color(0xFFFF6A3D);
const _g2 = Color(0xFFE23C16);
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test('generate app icon', () async {
// Full-bleed source (square gradient, no alpha) — iOS-safe, universal.
await _write('assets/icon/app_icon.png', await _render(_Mode.bleed));
// Rounded-tile variant with transparent margin (macOS / marketing).
await _write('assets/icon/app_icon_tile.png', await _render(_Mode.tile));
// Android adaptive-icon foreground: rocket only, padded into the safe zone.
await _write('assets/icon/app_icon_fg.png', await _render(_Mode.foreground));
});
}
enum _Mode { bleed, tile, foreground }
Future<void> _write(String path, Uint8List bytes) async {
final f = File(path);
await f.create(recursive: true);
await f.writeAsBytes(bytes);
// ignore: avoid_print
print('wrote $path (${bytes.length} bytes)');
}
Future<Uint8List> _render(_Mode mode) async {
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
const rect = Rect.fromLTWH(0, 0, _size, _size);
// ---- background tile ----
final bg = Paint()
..shader = ui.Gradient.linear(
const Offset(0, 0),
const Offset(_size, _size),
[_g1, _g2],
);
switch (mode) {
case _Mode.bleed:
canvas.drawRect(rect, bg);
_sheen(canvas);
break;
case _Mode.tile:
const margin = 96.0;
final tileR = RRect.fromRectAndRadius(
const Rect.fromLTWH(
margin, margin, _size - 2 * margin, _size - 2 * margin),
const Radius.circular(224),
);
canvas.drawRRect(tileR, bg);
canvas.drawRRect(
tileR.deflate(3),
Paint()
..style = PaintingStyle.stroke
..strokeWidth = 3
..color = Colors.white.withValues(alpha: 0.16),
);
_sheen(canvas);
break;
case _Mode.foreground:
// transparent background — foreground carries the mark only
break;
}
// The mark: filled confidently. The adaptive foreground fills ~2/3 of the
// frame (the launcher XML then adds a 16% safe-zone inset on top).
final scale = mode == _Mode.foreground ? 1.15 : 1.14;
canvas.save();
canvas.translate(512, 524);
canvas.scale(scale);
canvas.translate(-512, -524);
_drawRocket(canvas);
canvas.restore();
final pic = recorder.endRecording();
final img = await pic.toImage(_size.toInt(), _size.toInt());
final data = await img.toByteData(format: ui.ImageByteFormat.png);
return data!.buffer.asUint8List();
}
/// Soft top-left highlight for depth.
void _sheen(Canvas canvas) {
canvas.drawCircle(
const Offset(300, 250),
620,
Paint()
..shader = ui.Gradient.radial(
const Offset(300, 250),
620,
[Colors.white.withValues(alpha: 0.16), Colors.white.withValues(alpha: 0)],
),
);
}
/// A clean, upright rocket — the Peach mark — in white with an accent window,
/// fins and an exhaust flame. Centered on the 1024 canvas.
void _drawRocket(Canvas canvas) {
const cx = 512.0;
// subtle drop shadow behind the whole mark
canvas.save();
canvas.translate(0, 16);
_rocketBodyAndFins(canvas, Paint()
..color = Colors.black.withValues(alpha: 0.16)
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 22));
canvas.restore();
// flame first (behind body base)
final flame = Path()
..moveTo(cx, 828)
..cubicTo(cx - 62, 748, cx - 46, 712, cx - 30, 706)
..lineTo(cx + 30, 706)
..cubicTo(cx + 46, 712, cx + 62, 748, cx, 828)
..close();
canvas.drawPath(
flame,
Paint()
..shader = ui.Gradient.linear(
const Offset(cx, 700),
const Offset(cx, 828),
[const Color(0xFFFFD48A), const Color(0xFFF5A623)],
));
// inner flame
final flame2 = Path()
..moveTo(cx, 790)
..cubicTo(cx - 26, 742, cx - 20, 720, cx - 12, 716)
..lineTo(cx + 12, 716)
..cubicTo(cx + 20, 720, cx + 26, 742, cx, 790)
..close();
canvas.drawPath(flame2, Paint()..color = Colors.white.withValues(alpha: 0.85));
// white body + fins
_rocketBodyAndFins(canvas, Paint()..color = Colors.white);
// window (accent blue with a soft rim)
canvas.drawCircle(const Offset(cx, 432), 70,
Paint()..color = const Color(0xFF1F2937).withValues(alpha: 0.10));
canvas.drawCircle(const Offset(cx, 430), 66,
Paint()
..shader = ui.Gradient.linear(
const Offset(cx - 60, 370),
const Offset(cx + 60, 490),
[const Color(0xFF5AA0FF), const Color(0xFF2F6FE0)],
));
// window glint
canvas.drawCircle(const Offset(cx - 20, 408), 18,
Paint()..color = Colors.white.withValues(alpha: 0.55));
}
void _rocketBodyAndFins(Canvas canvas, Paint paint) {
const cx = 512.0;
// fins (drawn under the body edges)
final rightFin = Path()
..moveTo(cx + 96, 556)
..lineTo(cx + 208, 712)
..lineTo(cx + 78, 676)
..close();
final leftFin = Path()
..moveTo(cx - 96, 556)
..lineTo(cx - 208, 712)
..lineTo(cx - 78, 676)
..close();
canvas.drawPath(rightFin, paint);
canvas.drawPath(leftFin, paint);
// body (symmetric capsule with a pointed nose)
final body = Path()
..moveTo(cx, 244) // nose tip
..cubicTo(cx + 92, 300, cx + 116, 372, cx + 116, 452)
..cubicTo(cx + 116, 566, cx + 96, 646, cx + 60, 700)
..cubicTo(cx + 34, 714, cx - 34, 714, cx - 60, 700)
..cubicTo(cx - 96, 646, cx - 116, 566, cx - 116, 452)
..cubicTo(cx - 116, 372, cx - 92, 300, cx, 244)
..close();
canvas.drawPath(body, paint);
}

74
test/web_build_test.dart Normal file
View File

@@ -0,0 +1,74 @@
import 'dart:io';
import 'package:archive/archive.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:peach/Peach/about/internal/application/ServiceConfig.dart';
import 'package:peach/Peach/about/internal/file/BuildService.dart';
import 'package:peach/Peach/about/internal/file/WebArchive.dart';
void main() {
test('buildWeb locates the React build/ output', () async {
final dir = Directory.systemTemp.createTempSync('peachdeploy_web_test');
addTearDown(() => dir.deleteSync(recursive: true));
Directory('${dir.path}/build').createSync(recursive: true);
File('${dir.path}/build/index.html').writeAsStringSync('<html></html>');
File('${dir.path}/build/main.js').writeAsStringSync('console.log(1)');
final svc = ServiceConfig(
name: 'site',
kind: ServiceKind.webApp,
framework: WebFramework.react,
localProjectPath: dir.path,
buildCommand: Platform.isWindows ? 'echo building' : 'true',
);
final build = BuildService();
addTearDown(build.dispose);
final result = await build.buildWeb(svc);
expect(result.ok, isTrue, reason: result.message);
expect(result.dir, isNotNull);
expect(result.dir!.path.replaceAll('\\', '/'), endsWith('build'));
});
test('static framework needs no build and publishes the folder', () async {
final dir = Directory.systemTemp.createTempSync('peachdeploy_static_test');
addTearDown(() => dir.deleteSync(recursive: true));
File('${dir.path}/index.html').writeAsStringSync('<html></html>');
final svc = ServiceConfig(
name: 'static',
kind: ServiceKind.webApp,
framework: WebFramework.staticFiles,
localProjectPath: dir.path,
);
final build = BuildService();
addTearDown(build.dispose);
final result = await build.buildWeb(svc);
expect(result.ok, isTrue, reason: result.message);
expect(result.dir, isNotNull);
});
test('WebArchive tar.gz round-trips files at the archive root', () {
final dir = Directory.systemTemp.createTempSync('peachdeploy_arch_test');
addTearDown(() => dir.deleteSync(recursive: true));
File('${dir.path}/index.html').writeAsStringSync('hello');
Directory('${dir.path}/assets').createSync();
File('${dir.path}/assets/app.js').writeAsStringSync('x = 1;');
final (bytes, count) = WebArchive.tarGz(dir);
expect(count, 2);
final tar = GZipDecoder().decodeBytes(bytes);
final archive = TarDecoder().decodeBytes(tar);
final names = archive.files.map((f) => f.name).toSet();
// Paths must be POSIX (forward slashes) so `tar` on Linux rebuilds the
// directory tree instead of creating literal `assets\app.js` files.
expect(names.any((n) => n.contains('\\')), isFalse,
reason: 'archive entries must use forward slashes');
expect(names, contains('index.html'));
expect(names, contains('assets/app.js'));
});
}

13
test/widget_test.dart Normal file
View File

@@ -0,0 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:peach/main.dart';
void main() {
testWidgets('Peach shows the branded splash on boot', (tester) async {
await tester.pumpWidget(const Peach());
// While storage loads, the splash screen is shown.
expect(find.text('Peach'), findsOneWidget);
expect(find.byType(LinearProgressIndicator), findsWidgets);
});
}