Files
buzz/mobile/test/shared/widgets/masked_avatar_badge_test.dart
T
cls 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
feat: import Chinese-localized Buzz source snapshot
Signed-off-by: cls_宁波本机 <908705107@qq.com>
2026-08-13 18:34:25 +08:00

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),
);
});
});
}