9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Signed-off-by: cls_宁波本机 <908705107@qq.com>
148 lines
5.3 KiB
Dart
148 lines
5.3 KiB
Dart
import 'dart:math' as math;
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:buzz/shared/theme/theme.dart';
|
|
import 'package:buzz/shared/widgets/masked_avatar_badge.dart';
|
|
|
|
void main() {
|
|
group('avatarBadgeMaskPath', () {
|
|
const size = 128.0;
|
|
const geometry = AvatarBadgeMaskGeometry.badge;
|
|
|
|
test('keeps the avatar but cuts the notch out of it', () {
|
|
final path = avatarBadgeMaskPath(size: size, geometry: geometry);
|
|
|
|
expect(path.contains(const Offset(size / 2, size / 2)), isTrue);
|
|
expect(path.contains(geometry.cutoutCenter(size)), isFalse);
|
|
// The far edge is untouched — the notch only bites the bottom-right.
|
|
expect(path.contains(const Offset(4, size / 2)), isTrue);
|
|
});
|
|
|
|
test('stays inside the avatar circle on the unnotched sides', () {
|
|
// Checked with contains() rather than getBounds(), which reports
|
|
// control-point bounds and so overshoots the near-full-circle arc.
|
|
final path = avatarBadgeMaskPath(size: size, geometry: geometry);
|
|
|
|
expect(path.contains(const Offset(-1, size / 2)), isFalse);
|
|
expect(path.contains(const Offset(size / 2, -1)), isFalse);
|
|
expect(path.contains(const Offset(2, 2)), isFalse);
|
|
});
|
|
|
|
test('fillets the notch rather than subtracting a bare circle', () {
|
|
// A plain difference meets the cutout at two cusps. The fillets round
|
|
// those off, which necessarily removes extra material just outside the
|
|
// cutout circle — so the masked path must be a strict subset there.
|
|
final masked = avatarBadgeMaskPath(size: size, geometry: geometry);
|
|
final cutoutCenter = geometry.cutoutCenter(size);
|
|
final cutoutRadius = geometry.cutoutRadius(size);
|
|
final bare = Path.combine(
|
|
PathOperation.difference,
|
|
Path()..addOval(
|
|
Rect.fromCircle(
|
|
center: const Offset(size / 2, size / 2),
|
|
radius: size / 2,
|
|
),
|
|
),
|
|
Path()..addOval(
|
|
Rect.fromCircle(center: cutoutCenter, radius: cutoutRadius),
|
|
),
|
|
);
|
|
|
|
// Sample the ring just outside the cutout, where the fillets live.
|
|
var filleted = 0;
|
|
for (var step = 0; step < 360; step++) {
|
|
final angle = step * math.pi / 180;
|
|
final point =
|
|
cutoutCenter +
|
|
Offset(math.cos(angle), math.sin(angle)) * (cutoutRadius + 2);
|
|
if (bare.contains(point) && !masked.contains(point)) filleted++;
|
|
// The fillet only adds material to the cut, never puts it back.
|
|
expect(
|
|
masked.contains(point) && !bare.contains(point),
|
|
isFalse,
|
|
reason: 'mask should not extend beyond the plain difference',
|
|
);
|
|
}
|
|
expect(filleted, greaterThan(0));
|
|
});
|
|
|
|
test('falls back to a plain circle when the notch misses the avatar', () {
|
|
// A notch tucked at the centre never crosses the avatar's edge, so there
|
|
// is no crossing to fillet and nothing should be cut.
|
|
const detached = AvatarBadgeMaskGeometry(
|
|
cutoutCenterRatio: 0.5,
|
|
cutoutRadiusRatio: 0.05,
|
|
badgeSizeRatio: 0.08,
|
|
curve: AvatarBadgeCurve.standard,
|
|
);
|
|
final path = avatarBadgeMaskPath(size: size, geometry: detached);
|
|
|
|
expect(path.contains(detached.cutoutCenter(size)), isTrue);
|
|
expect(path.getBounds().size, const Size(size, size));
|
|
});
|
|
|
|
test(
|
|
'scales geometry with the avatar, matching desktop at its own size',
|
|
() {
|
|
// Desktop's sidebar avatar is 32px with the cutout at (28, 28) r 7.5.
|
|
const presence = AvatarBadgeMaskGeometry.presenceDot;
|
|
|
|
expect(presence.cutoutCenter(32), const Offset(28, 28));
|
|
expect(presence.cutoutRadius(32), 7.5);
|
|
expect(presence.badgeSize(32), 14);
|
|
// Twice the avatar, twice the notch.
|
|
expect(presence.cutoutCenter(64), const Offset(56, 56));
|
|
expect(presence.cutoutRadius(64), 15);
|
|
},
|
|
);
|
|
});
|
|
|
|
group('MaskedAvatarBadge', () {
|
|
Widget harness({Widget? badge}) => MaterialApp(
|
|
theme: AppTheme.light(),
|
|
home: Center(
|
|
child: MaskedAvatarBadge(
|
|
size: 128,
|
|
geometry: AvatarBadgeMaskGeometry.badge,
|
|
avatar: const ColoredBox(color: Color(0xFF123456)),
|
|
badge: badge,
|
|
),
|
|
),
|
|
);
|
|
|
|
testWidgets('masks the avatar and centres the badge in the notch', (
|
|
tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
harness(badge: const ColoredBox(color: Color(0xFFABCDEF))),
|
|
);
|
|
|
|
final clip = tester.widget<ClipPath>(find.byType(ClipPath));
|
|
expect(clip.clipper, isA<AvatarBadgeMaskClipper>());
|
|
|
|
const geometry = AvatarBadgeMaskGeometry.badge;
|
|
final frame = tester.getRect(find.byType(MaskedAvatarBadge));
|
|
final badgeRect = tester.getRect(find.byType(ColoredBox).last);
|
|
|
|
expect(badgeRect.width, closeTo(geometry.badgeSize(128), 0.01));
|
|
expect(
|
|
badgeRect.center - frame.topLeft,
|
|
within(distance: 0.01, from: geometry.cutoutCenter(128)),
|
|
);
|
|
});
|
|
|
|
testWidgets('leaves the avatar whole when there is no badge', (
|
|
tester,
|
|
) async {
|
|
await tester.pumpWidget(harness());
|
|
|
|
expect(find.byType(ClipPath), findsNothing);
|
|
expect(
|
|
tester.getSize(find.byType(MaskedAvatarBadge)),
|
|
const Size(128, 128),
|
|
);
|
|
});
|
|
});
|
|
}
|