feat: import Chinese-localized Buzz source snapshot
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
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>
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:buzz/features/profile/presence_cache_provider.dart';
|
||||
import 'package:buzz/shared/relay/relay.dart';
|
||||
|
||||
/// Tests for [PresenceCacheNotifier] in the pure-Nostr world.
|
||||
///
|
||||
/// The cache is now purely WS-driven: the notifier subscribes to kind:20001
|
||||
/// (presence updates) over the relay session and only mutates state for
|
||||
/// pubkeys that have been registered via [PresenceCacheNotifier.track].
|
||||
/// There is no longer a REST backstop — the previous test seeded state via
|
||||
/// a `GET /api/presence` call which has been removed.
|
||||
void main() {
|
||||
test('WS presence event updates cache for tracked pubkey', () async {
|
||||
final relaySession = _RecordingRelaySessionNotifier();
|
||||
final container = _buildContainer(relaySession: relaySession);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
// Initialize the notifier (triggers build → subscribes to WS).
|
||||
container.read(presenceCacheProvider);
|
||||
await _pumpEventQueue();
|
||||
|
||||
// Track alice, then emit her initial 'online' status.
|
||||
container.read(presenceCacheProvider.notifier).track(['alice']);
|
||||
relaySession.emit(_presence('alice', 'online'));
|
||||
expect(container.read(presenceCacheProvider)['alice'], 'online');
|
||||
|
||||
// Simulate a WS presence event: alice goes away.
|
||||
relaySession.emit(_presence('alice', 'away'));
|
||||
expect(container.read(presenceCacheProvider)['alice'], 'away');
|
||||
});
|
||||
|
||||
test('WS presence event ignores untracked pubkeys', () async {
|
||||
final relaySession = _RecordingRelaySessionNotifier();
|
||||
final container = _buildContainer(relaySession: relaySession);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
container.read(presenceCacheProvider);
|
||||
await _pumpEventQueue();
|
||||
|
||||
// Track only alice.
|
||||
container.read(presenceCacheProvider.notifier).track(['alice']);
|
||||
|
||||
// Emit event for bob (untracked).
|
||||
relaySession.emit(_presence('bob', 'online'));
|
||||
|
||||
// Bob should NOT appear in the cache.
|
||||
expect(container.read(presenceCacheProvider).containsKey('bob'), isFalse);
|
||||
});
|
||||
|
||||
test('WS presence event ignores invalid status values', () async {
|
||||
final relaySession = _RecordingRelaySessionNotifier();
|
||||
final container = _buildContainer(relaySession: relaySession);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
container.read(presenceCacheProvider);
|
||||
await _pumpEventQueue();
|
||||
|
||||
container.read(presenceCacheProvider.notifier).track(['alice']);
|
||||
relaySession.emit(_presence('alice', 'online'));
|
||||
expect(container.read(presenceCacheProvider)['alice'], 'online');
|
||||
|
||||
// Emit event with garbage status — should be rejected.
|
||||
relaySession.emit(_presence('alice', 'garbage-status'));
|
||||
|
||||
// Status should remain 'online'.
|
||||
expect(container.read(presenceCacheProvider)['alice'], 'online');
|
||||
});
|
||||
|
||||
test('WS presence event skips no-op updates', () async {
|
||||
final relaySession = _RecordingRelaySessionNotifier();
|
||||
final container = _buildContainer(relaySession: relaySession);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
container.read(presenceCacheProvider);
|
||||
await _pumpEventQueue();
|
||||
|
||||
container.read(presenceCacheProvider.notifier).track(['alice']);
|
||||
relaySession.emit(_presence('alice', 'online'));
|
||||
|
||||
// Listen for state changes after initial setup.
|
||||
var stateChangeCount = 0;
|
||||
container.listen(presenceCacheProvider, (prev, next) => stateChangeCount++);
|
||||
|
||||
// Emit event with same status as current.
|
||||
relaySession.emit(_presence('alice', 'online'));
|
||||
|
||||
// No state change should occur — it's a no-op.
|
||||
expect(stateChangeCount, 0);
|
||||
});
|
||||
|
||||
test('subscribes to kind:20001 with limit 0', () async {
|
||||
final relaySession = _RecordingRelaySessionNotifier();
|
||||
final container = _buildContainer(relaySession: relaySession);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
container.read(presenceCacheProvider);
|
||||
await _pumpEventQueue();
|
||||
|
||||
// Should have subscribed with the correct filter.
|
||||
expect(relaySession.filters, hasLength(1));
|
||||
expect(relaySession.filters.single.kinds, [EventKind.presenceUpdate]);
|
||||
expect(relaySession.filters.single.limit, 0);
|
||||
});
|
||||
|
||||
test('WS event uses pubkey variable, not literal string', () async {
|
||||
// Regression test for the map key bug where `{...state, pubkey: status}`
|
||||
// used the literal string "pubkey" instead of the variable's value.
|
||||
final relaySession = _RecordingRelaySessionNotifier();
|
||||
final container = _buildContainer(relaySession: relaySession);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
container.read(presenceCacheProvider);
|
||||
await _pumpEventQueue();
|
||||
|
||||
container.read(presenceCacheProvider.notifier).track([
|
||||
'deadbeef',
|
||||
'cafebabe',
|
||||
]);
|
||||
|
||||
// Seed cafebabe -> offline, then set deadbeef online.
|
||||
relaySession.emit(_presence('cafebabe', 'offline'));
|
||||
relaySession.emit(_presence('deadbeef', 'online'));
|
||||
|
||||
final cache = container.read(presenceCacheProvider);
|
||||
// deadbeef should be online (the actual pubkey, not a literal "pubkey" key).
|
||||
expect(cache['deadbeef'], 'online');
|
||||
// cafebabe should still be offline (not clobbered).
|
||||
expect(cache['cafebabe'], 'offline');
|
||||
// There should be no literal "pubkey" key in the map.
|
||||
expect(cache.containsKey('pubkey'), isFalse);
|
||||
});
|
||||
}
|
||||
|
||||
NostrEvent _presence(String pubkey, String status) => NostrEvent(
|
||||
id: 'evt-$pubkey-$status',
|
||||
pubkey: pubkey,
|
||||
createdAt: 1000,
|
||||
kind: EventKind.presenceUpdate,
|
||||
tags: const [],
|
||||
content: status,
|
||||
sig: 'sig',
|
||||
);
|
||||
|
||||
Future<void> _pumpEventQueue() async {
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
}
|
||||
|
||||
ProviderContainer _buildContainer({
|
||||
required _RecordingRelaySessionNotifier relaySession,
|
||||
}) {
|
||||
return ProviderContainer(
|
||||
overrides: [
|
||||
appLifecycleProvider.overrideWith(() => _FakeAppLifecycleNotifier()),
|
||||
relaySessionProvider.overrideWith(() => relaySession),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class _RecordingRelaySessionNotifier extends RelaySessionNotifier {
|
||||
final List<NostrFilter> filters = [];
|
||||
final List<void Function(NostrEvent)> _listeners = [];
|
||||
|
||||
@override
|
||||
SessionState build() => const SessionState(status: SessionStatus.connected);
|
||||
|
||||
@override
|
||||
Future<void Function()> subscribe(
|
||||
NostrFilter filter,
|
||||
void Function(NostrEvent) onEvent, {
|
||||
void Function(String message)? onClosed,
|
||||
}) async {
|
||||
filters.add(filter);
|
||||
_listeners.add(onEvent);
|
||||
return () {
|
||||
filters.remove(filter);
|
||||
_listeners.remove(onEvent);
|
||||
};
|
||||
}
|
||||
|
||||
/// Emit an event synchronously to all live subscribers.
|
||||
void emit(NostrEvent event) {
|
||||
for (final listener in List.of(_listeners)) {
|
||||
listener(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _FakeAppLifecycleNotifier extends AppLifecycleNotifier {
|
||||
@override
|
||||
AppLifecycleState build() => AppLifecycleState.resumed;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:buzz/features/profile/profile_avatar.dart';
|
||||
import 'package:buzz/features/profile/profile_provider.dart';
|
||||
import 'package:buzz/features/profile/user_profile.dart';
|
||||
import 'package:buzz/shared/theme/theme.dart';
|
||||
import 'package:buzz/shared/widgets/masked_avatar_badge.dart';
|
||||
|
||||
void main() {
|
||||
Widget harness({bool showPresence = true, String presence = 'online'}) {
|
||||
return ProviderScope(
|
||||
overrides: [
|
||||
profileProvider.overrideWith(_FakeProfileNotifier.new),
|
||||
presenceProvider.overrideWith(() => _FakePresenceNotifier(presence)),
|
||||
],
|
||||
child: MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: Center(child: ProfileAvatar(showPresence: showPresence)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('seats the presence dot in a notch instead of over the avatar', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(harness());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final badge = tester.widget<MaskedAvatarBadge>(
|
||||
find.byType(MaskedAvatarBadge),
|
||||
);
|
||||
expect(badge.geometry, AvatarBadgeMaskGeometry.presenceDot);
|
||||
expect(
|
||||
tester.widget<ClipPath>(find.byType(ClipPath).last).clipper,
|
||||
isA<AvatarBadgeMaskClipper>(),
|
||||
);
|
||||
|
||||
// The notch supplies the separation, so the dot carries no border ring.
|
||||
final dot = tester
|
||||
.widgetList<DecoratedBox>(find.byType(DecoratedBox))
|
||||
.map((box) => box.decoration)
|
||||
.whereType<BoxDecoration>()
|
||||
.firstWhere((decoration) => decoration.shape == BoxShape.circle);
|
||||
expect(dot.border, isNull);
|
||||
});
|
||||
|
||||
Color dotColor(WidgetTester tester) => tester
|
||||
.widgetList<DecoratedBox>(find.byType(DecoratedBox))
|
||||
.map((box) => box.decoration)
|
||||
.whereType<BoxDecoration>()
|
||||
.firstWhere((decoration) => decoration.shape == BoxShape.circle)
|
||||
.color!;
|
||||
|
||||
final theme = AppTheme.light();
|
||||
|
||||
testWidgets('paints the dot with the presence color', (tester) async {
|
||||
await tester.pumpWidget(harness());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(dotColor(tester), theme.extension<AppColors>()!.success);
|
||||
});
|
||||
|
||||
testWidgets('mutes the dot when offline', (tester) async {
|
||||
await tester.pumpWidget(harness(presence: 'offline'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(dotColor(tester), theme.colorScheme.outline);
|
||||
});
|
||||
|
||||
testWidgets('leaves the avatar unmasked when presence is hidden', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(harness(showPresence: false));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final badge = tester.widget<MaskedAvatarBadge>(
|
||||
find.byType(MaskedAvatarBadge),
|
||||
);
|
||||
expect(badge.badge, isNull);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(MaskedAvatarBadge),
|
||||
matching: find.byType(ClipPath),
|
||||
),
|
||||
findsNothing,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
class _FakeProfileNotifier extends ProfileNotifier {
|
||||
@override
|
||||
Future<UserProfile?> build() async =>
|
||||
const UserProfile(pubkey: 'aabb', displayName: 'Test');
|
||||
}
|
||||
|
||||
class _FakePresenceNotifier extends PresenceNotifier {
|
||||
_FakePresenceNotifier(this._presence);
|
||||
|
||||
final String _presence;
|
||||
|
||||
@override
|
||||
Future<String> build() async => _presence;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import 'package:buzz/features/profile/profile_provider.dart';
|
||||
import 'package:buzz/features/profile/user_profile.dart';
|
||||
import 'package:buzz/shared/relay/relay.dart';
|
||||
import 'package:buzz/shared/theme/theme.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
void main() {
|
||||
test(
|
||||
'manual presence persists until Online restores automatic mode',
|
||||
() async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
var container = _buildContainer(prefs);
|
||||
|
||||
expect(
|
||||
await container
|
||||
.read(presenceProvider.future)
|
||||
.timeout(
|
||||
const Duration(seconds: 2),
|
||||
onTimeout: () =>
|
||||
throw StateError('initial presence did not resolve'),
|
||||
),
|
||||
'online',
|
||||
);
|
||||
await container
|
||||
.read(presenceProvider.notifier)
|
||||
.setPresence('away')
|
||||
.timeout(
|
||||
const Duration(seconds: 2),
|
||||
onTimeout: () => throw StateError('setting Away did not resolve'),
|
||||
);
|
||||
expect(container.read(presenceProvider).value, 'away');
|
||||
expect(prefs.getString('buzz_presence_preference_aabb'), 'away');
|
||||
|
||||
container.dispose();
|
||||
container = _buildContainer(prefs);
|
||||
addTearDown(container.dispose);
|
||||
expect(
|
||||
await container
|
||||
.read(presenceProvider.future)
|
||||
.timeout(
|
||||
const Duration(seconds: 2),
|
||||
onTimeout: () =>
|
||||
throw StateError('stored presence did not resolve'),
|
||||
),
|
||||
'away',
|
||||
);
|
||||
|
||||
await container
|
||||
.read(presenceProvider.notifier)
|
||||
.setPresence('online')
|
||||
.timeout(
|
||||
const Duration(seconds: 2),
|
||||
onTimeout: () => throw StateError('setting Online did not resolve'),
|
||||
);
|
||||
expect(container.read(presenceProvider).value, 'online');
|
||||
expect(prefs.getString('buzz_presence_preference_aabb'), 'auto');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
ProviderContainer _buildContainer(SharedPreferences prefs) => ProviderContainer(
|
||||
overrides: [
|
||||
savedPrefsProvider.overrideWithValue(prefs),
|
||||
myPubkeyProvider.overrideWithValue('aabb'),
|
||||
profileProvider.overrideWith(_FakeProfileNotifier.new),
|
||||
relaySessionProvider.overrideWith(_DisconnectedRelaySession.new),
|
||||
appLifecycleProvider.overrideWith(_ResumedLifecycle.new),
|
||||
],
|
||||
);
|
||||
|
||||
class _FakeProfileNotifier extends ProfileNotifier {
|
||||
@override
|
||||
Future<UserProfile?> build() async =>
|
||||
const UserProfile(pubkey: 'aabb', displayName: 'Test');
|
||||
}
|
||||
|
||||
class _DisconnectedRelaySession extends RelaySessionNotifier {
|
||||
@override
|
||||
SessionState build() =>
|
||||
const SessionState(status: SessionStatus.disconnected);
|
||||
}
|
||||
|
||||
class _ResumedLifecycle extends AppLifecycleNotifier {
|
||||
@override
|
||||
AppLifecycleState build() => AppLifecycleState.resumed;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import 'package:buzz/features/profile/set_status_sheet.dart';
|
||||
import 'package:buzz/features/profile/user_status.dart';
|
||||
import 'package:buzz/features/profile/user_status_provider.dart';
|
||||
import 'package:buzz/shared/custom_emoji/custom_emoji_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
|
||||
import '../../helpers/widget_helpers.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('removes only the emoji from an existing status', (tester) async {
|
||||
final statusNotifier = _RecordingUserStatusNotifier();
|
||||
await tester.pumpWidget(
|
||||
WidgetHelpers.testable(
|
||||
overrides: [
|
||||
customEmojiListProvider.overrideWithValue(const []),
|
||||
userStatusProvider.overrideWith(() => statusNotifier),
|
||||
],
|
||||
child: Builder(
|
||||
builder: (context) => FilledButton(
|
||||
onPressed: () => showSetStatusSheet(
|
||||
context,
|
||||
currentStatus: const UserStatus(
|
||||
text: 'Focusing',
|
||||
emoji: '\u{1F3AF}',
|
||||
updatedAt: 1,
|
||||
),
|
||||
),
|
||||
child: const Text('Open status editor'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Open status editor'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('\u{1F3AF}'), findsOneWidget);
|
||||
expect(find.byTooltip('Remove status emoji'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byTooltip('Remove status emoji'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('\u{1F3AF}'), findsNothing);
|
||||
expect(find.byIcon(LucideIcons.smilePlus), findsOneWidget);
|
||||
expect(
|
||||
tester.widget<TextField>(find.byType(TextField)).controller?.text,
|
||||
'Focusing',
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Save'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(statusNotifier.savedText, 'Focusing');
|
||||
expect(statusNotifier.savedEmoji, isEmpty);
|
||||
});
|
||||
}
|
||||
|
||||
class _RecordingUserStatusNotifier extends UserStatusNotifier {
|
||||
String? savedText;
|
||||
String? savedEmoji;
|
||||
|
||||
@override
|
||||
Future<UserStatus?> build() async => null;
|
||||
|
||||
@override
|
||||
Future<void> setStatus(String text, String emoji) async {
|
||||
savedText = text;
|
||||
savedEmoji = emoji;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import 'package:buzz/features/profile/profile_provider.dart';
|
||||
import 'package:buzz/features/profile/settings_profile_header.dart';
|
||||
import 'package:buzz/features/profile/user_profile.dart';
|
||||
import 'package:buzz/features/profile/user_status.dart';
|
||||
import 'package:buzz/features/profile/user_status_provider.dart';
|
||||
import 'package:buzz/shared/custom_emoji/custom_emoji_provider.dart';
|
||||
import 'package:buzz/shared/theme/theme.dart';
|
||||
import 'package:buzz/shared/widgets/masked_avatar_badge.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
|
||||
import '../../helpers/widget_helpers.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('uses a bounded icon for an unresolved status shortcode', (
|
||||
tester,
|
||||
) async {
|
||||
const missingShortcode = ':very_long_missing_custom_emoji:';
|
||||
await tester.pumpWidget(
|
||||
WidgetHelpers.testable(
|
||||
overrides: [
|
||||
profileProvider.overrideWith(_FakeProfileNotifier.new),
|
||||
presenceProvider.overrideWith(() => _FakePresenceNotifier('online')),
|
||||
userStatusProvider.overrideWith(
|
||||
() => _FakeUserStatusNotifier(
|
||||
const UserStatus(
|
||||
text: 'Focusing',
|
||||
emoji: missingShortcode,
|
||||
updatedAt: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
customEmojiListProvider.overrideWithValue(const []),
|
||||
],
|
||||
child: const SettingsProfileHeader(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(Hero), findsNothing);
|
||||
final badge = find.byType(MaskedAvatarBadge);
|
||||
expect(
|
||||
find.descendant(of: badge, matching: find.text(missingShortcode)),
|
||||
findsNothing,
|
||||
);
|
||||
expect(
|
||||
find.descendant(of: badge, matching: find.byIcon(LucideIcons.smile)),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'keeps text-only status visible beside a changeable presence pill',
|
||||
(tester) async {
|
||||
final presenceNotifier = _FakePresenceNotifier('away');
|
||||
await tester.pumpWidget(
|
||||
WidgetHelpers.testable(
|
||||
overrides: [
|
||||
profileProvider.overrideWith(_FakeProfileNotifier.new),
|
||||
presenceProvider.overrideWith(() => presenceNotifier),
|
||||
userStatusProvider.overrideWith(
|
||||
() => _FakeUserStatusNotifier(
|
||||
const UserStatus(text: 'Focusing', emoji: '', updatedAt: 1),
|
||||
),
|
||||
),
|
||||
customEmojiListProvider.overrideWithValue(const []),
|
||||
],
|
||||
child: const SettingsProfileHeader(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Focusing'), findsOneWidget);
|
||||
await tester.tap(find.text('Focusing'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Set a status'), findsOneWidget);
|
||||
expect(
|
||||
tester.widget<TextField>(find.byType(TextField)).controller?.text,
|
||||
'Focusing',
|
||||
);
|
||||
await tester.binding.handlePopRoute();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.byKey(const ValueKey('settings-presence-label')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('Away'), findsOneWidget);
|
||||
expect(
|
||||
tester
|
||||
.widget<Text>(find.byKey(const ValueKey('settings-presence-label')))
|
||||
.style
|
||||
?.fontSize,
|
||||
filterChipTextStyle.fontSize,
|
||||
);
|
||||
expect(
|
||||
tester
|
||||
.getSize(find.byKey(const ValueKey('settings-presence-target')))
|
||||
.height,
|
||||
48,
|
||||
);
|
||||
expect(
|
||||
tester
|
||||
.getSize(find.byKey(const ValueKey('settings-presence-pill')))
|
||||
.height,
|
||||
greaterThanOrEqualTo(31),
|
||||
);
|
||||
|
||||
final presenceTarget = find.byKey(
|
||||
const ValueKey('settings-presence-target'),
|
||||
);
|
||||
final targetRect = tester.getRect(presenceTarget);
|
||||
await tester.tapAt(Offset(targetRect.center.dx, targetRect.bottom - 1));
|
||||
await tester.pump();
|
||||
|
||||
final scale = tester.widget<ScaleTransition>(
|
||||
find.byKey(const ValueKey('activity-popover-scale')),
|
||||
);
|
||||
expect(scale.alignment, Alignment.topCenter);
|
||||
expect(
|
||||
find.byKey(const ValueKey('settings-presence-popover')),
|
||||
findsOneWidget,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.byKey(const ValueKey('settings-presence-online')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const ValueKey('settings-presence-away')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const ValueKey('settings-presence-offline')),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
await tester.tap(find.byKey(const ValueKey('settings-presence-offline')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(presenceNotifier.selected, ['offline']);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class _FakeProfileNotifier extends ProfileNotifier {
|
||||
@override
|
||||
Future<UserProfile?> build() async =>
|
||||
const UserProfile(pubkey: 'aabb', displayName: 'Test');
|
||||
}
|
||||
|
||||
class _FakeUserStatusNotifier extends UserStatusNotifier {
|
||||
_FakeUserStatusNotifier(this._status);
|
||||
|
||||
final UserStatus _status;
|
||||
|
||||
@override
|
||||
Future<UserStatus?> build() async => _status;
|
||||
}
|
||||
|
||||
class _FakePresenceNotifier extends PresenceNotifier {
|
||||
_FakePresenceNotifier(this._presence);
|
||||
|
||||
final String _presence;
|
||||
final List<String> selected = [];
|
||||
|
||||
@override
|
||||
Future<String> build() async => _presence;
|
||||
|
||||
@override
|
||||
Future<void> setPresence(String status) async {
|
||||
selected.add(status);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user