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,377 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:buzz/features/channels/channel_sort/channel_sort_manager.dart';
|
||||
import 'package:buzz/features/channels/channel_sort/channel_sort_storage.dart';
|
||||
import 'package:buzz/shared/relay/relay.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:nostr/nostr.dart' as nostr;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
void main() {
|
||||
late SharedPreferences prefs;
|
||||
late nostr.Keys keys;
|
||||
late ChannelSortCrypto crypto;
|
||||
|
||||
setUp(() async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
prefs = await SharedPreferences.getInstance();
|
||||
keys = nostr.Keys.generate();
|
||||
crypto = ChannelSortCrypto(keys.nsec, keys.public);
|
||||
});
|
||||
|
||||
NostrEvent event(
|
||||
Map<String, String> groups,
|
||||
int createdAt, {
|
||||
String id = 'e',
|
||||
}) => NostrEvent(
|
||||
id: id,
|
||||
pubkey: keys.public,
|
||||
createdAt: createdAt,
|
||||
kind: EventKind.readState,
|
||||
tags: const [
|
||||
['d', 'channel-sort'],
|
||||
],
|
||||
content: crypto.encrypt(jsonEncode({'version': 1, 'groups': groups})),
|
||||
sig: 'sig',
|
||||
);
|
||||
|
||||
ChannelSortManager manager(
|
||||
_FakeRelaySession relay,
|
||||
_RecordingSignedEventRelay signed, {
|
||||
Duration retry = const Duration(milliseconds: 5),
|
||||
}) => ChannelSortManager(
|
||||
pubkey: keys.public,
|
||||
relayUrl: 'wss://relay.example',
|
||||
prefs: prefs,
|
||||
crypto: crypto,
|
||||
relaySession: relay,
|
||||
signedEventRelay: signed,
|
||||
remoteEnabled: true,
|
||||
onChanged: () {},
|
||||
startupRetryBaseDelay: retry,
|
||||
publishDelay: const Duration(milliseconds: 5),
|
||||
);
|
||||
|
||||
test(
|
||||
'adopts desktop payload and defaults unspecified groups to alpha',
|
||||
() async {
|
||||
final relay = _FakeRelaySession()
|
||||
..historyEvents = [
|
||||
event({'channels': 'recent', 'section:abc': 'recent'}, 100),
|
||||
];
|
||||
final subject = manager(relay, _RecordingSignedEventRelay());
|
||||
await subject.initialize();
|
||||
expect(subject.sortModeFor('channels'), ChannelSortMode.recent);
|
||||
expect(subject.sortModeFor('section:abc'), ChannelSortMode.recent);
|
||||
expect(subject.sortModeFor('dms'), ChannelSortMode.alpha);
|
||||
subject.dispose();
|
||||
},
|
||||
);
|
||||
|
||||
test('publishes local edit using desktop encrypted wire format', () async {
|
||||
final relay = _FakeRelaySession();
|
||||
final signed = _RecordingSignedEventRelay();
|
||||
final subject = manager(relay, signed);
|
||||
await subject.initialize();
|
||||
subject.setSortModeFor('dms', ChannelSortMode.recent);
|
||||
final submitted = await signed.submitted.future.timeout(
|
||||
const Duration(seconds: 1),
|
||||
);
|
||||
expect(
|
||||
submitted.tags.any((tag) => tag[0] == 'd' && tag[1] == 'channel-sort'),
|
||||
isTrue,
|
||||
);
|
||||
final payload =
|
||||
jsonDecode(crypto.decrypt(submitted.content)) as Map<String, dynamic>;
|
||||
expect(payload, {
|
||||
'version': 1,
|
||||
'groups': {'dms': 'recent'},
|
||||
});
|
||||
subject.dispose();
|
||||
});
|
||||
|
||||
test(
|
||||
'failed publish preflight retries without submitting stale state',
|
||||
() async {
|
||||
final relay = _FakeRelaySession();
|
||||
final signed = _RecordingSignedEventRelay();
|
||||
final subject = manager(relay, signed);
|
||||
await subject.initialize();
|
||||
|
||||
relay.fetchFailures = 1;
|
||||
subject.setSortModeFor('dms', ChannelSortMode.recent);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 8));
|
||||
expect(signed.submitCount, 0);
|
||||
|
||||
final submitted = await signed.submitted.future.timeout(
|
||||
const Duration(seconds: 1),
|
||||
);
|
||||
expect(relay.fetchCount, greaterThanOrEqualTo(4));
|
||||
final payload =
|
||||
jsonDecode(crypto.decrypt(submitted.content)) as Map<String, dynamic>;
|
||||
expect(payload['groups'], {'dms': 'recent'});
|
||||
subject.dispose();
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'pending local edit survives manager rebuild and older relay state',
|
||||
() async {
|
||||
final firstRelay = _FakeRelaySession();
|
||||
final first = manager(firstRelay, _RecordingSignedEventRelay());
|
||||
await first.initialize();
|
||||
first.setSortModeFor('channels', ChannelSortMode.recent);
|
||||
first.dispose();
|
||||
|
||||
final secondRelay = _FakeRelaySession()
|
||||
..historyEvents = [
|
||||
event({'dms': 'recent'}, 100),
|
||||
];
|
||||
final signed = _RecordingSignedEventRelay();
|
||||
final second = manager(secondRelay, signed);
|
||||
await second.initialize();
|
||||
|
||||
expect(second.store.groups, {'channels': ChannelSortMode.recent});
|
||||
final submitted = await signed.submitted.future.timeout(
|
||||
const Duration(seconds: 1),
|
||||
);
|
||||
final payload =
|
||||
jsonDecode(crypto.decrypt(submitted.content)) as Map<String, dynamic>;
|
||||
expect(payload['groups'], {'channels': 'recent'});
|
||||
second.dispose();
|
||||
},
|
||||
);
|
||||
|
||||
test('newer remote event cancels pending local whole-blob write', () async {
|
||||
final relay = _FakeRelaySession();
|
||||
final signed = _RecordingSignedEventRelay();
|
||||
final subject = manager(relay, signed);
|
||||
await subject.initialize();
|
||||
subject.setSortModeFor('channels', ChannelSortMode.recent);
|
||||
relay.emit(
|
||||
event({
|
||||
'dms': 'recent',
|
||||
}, DateTime.now().millisecondsSinceEpoch ~/ 1000 + 1),
|
||||
);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 30));
|
||||
expect(subject.store.groups, {'dms': ChannelSortMode.recent});
|
||||
expect(signed.submitted.isCompleted, isFalse);
|
||||
subject.dispose();
|
||||
});
|
||||
|
||||
test('same-second remote does not erase a pending local edit', () async {
|
||||
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
ChannelSortStorage(prefs).writeSyncState(
|
||||
keys.public,
|
||||
'wss://relay.example',
|
||||
ChannelSortSyncState(updatedAt: now - 1, eventId: 'remote'),
|
||||
);
|
||||
final relay = _FakeRelaySession();
|
||||
final signed = _RecordingSignedEventRelay();
|
||||
final subject = manager(relay, signed);
|
||||
await subject.initialize();
|
||||
|
||||
subject.setSortModeFor('channels', ChannelSortMode.recent);
|
||||
relay.emit(event({'dms': 'recent'}, now, id: 'a'));
|
||||
expect(subject.store.groups, {'channels': ChannelSortMode.recent});
|
||||
await signed.submitted.future.timeout(const Duration(seconds: 1));
|
||||
subject.dispose();
|
||||
});
|
||||
|
||||
test('publishing does not advance the persisted remote cursor', () async {
|
||||
final storage = ChannelSortStorage(prefs);
|
||||
storage.writeSyncState(
|
||||
keys.public,
|
||||
'wss://relay.example',
|
||||
const ChannelSortSyncState(updatedAt: 100, eventId: 'remote'),
|
||||
);
|
||||
final signed = _RecordingSignedEventRelay();
|
||||
final subject = manager(_FakeRelaySession(), signed);
|
||||
await subject.initialize();
|
||||
|
||||
subject.setSortModeFor('channels', ChannelSortMode.recent);
|
||||
await signed.submitted.future.timeout(const Duration(seconds: 1));
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
final syncState = storage.readSyncState(keys.public, 'wss://relay.example');
|
||||
expect(syncState.updatedAt, 100);
|
||||
expect(syncState.eventId, 'remote');
|
||||
expect(syncState.hasPendingLocalChanges, isFalse);
|
||||
expect(syncState.pendingUpdatedAt, 0);
|
||||
subject.dispose();
|
||||
});
|
||||
|
||||
test('legacy pending state resets its ambiguous remote cursor', () {
|
||||
final storage = ChannelSortStorage(prefs);
|
||||
// Simulate the pre-migration JSON, which had no pendingUpdatedAt field.
|
||||
prefs.setString(
|
||||
'${channelSortKey(keys.public, 'wss://relay.example')}:sync',
|
||||
jsonEncode({
|
||||
'updatedAt': 4102444800,
|
||||
'eventId': 'local-publication',
|
||||
'hasPendingLocalChanges': true,
|
||||
}),
|
||||
);
|
||||
|
||||
final syncState = storage.readSyncState(keys.public, 'wss://relay.example');
|
||||
expect(syncState.updatedAt, 0);
|
||||
expect(syncState.eventId, isEmpty);
|
||||
expect(syncState.pendingUpdatedAt, 4102444800);
|
||||
});
|
||||
|
||||
test('lower event ID wins when remote timestamps tie', () async {
|
||||
final relay = _FakeRelaySession()
|
||||
..historyEvents = [
|
||||
event({'channels': 'recent'}, 200, id: 'f'),
|
||||
];
|
||||
final subject = manager(relay, _RecordingSignedEventRelay());
|
||||
await subject.initialize();
|
||||
|
||||
relay.emit(event({'dms': 'recent'}, 200, id: 'a'));
|
||||
expect(subject.store.groups, {'dms': ChannelSortMode.recent});
|
||||
|
||||
relay.emit(event({'starred': 'recent'}, 200, id: 'z'));
|
||||
expect(subject.store.groups, {'dms': ChannelSortMode.recent});
|
||||
subject.dispose();
|
||||
});
|
||||
|
||||
test('adopts a newer relay replacement after publishing', () async {
|
||||
final relay = _FakeRelaySession();
|
||||
final signed = _RecordingSignedEventRelay();
|
||||
final subject = manager(relay, signed);
|
||||
await subject.initialize();
|
||||
|
||||
subject.setSortModeFor('channels', ChannelSortMode.recent);
|
||||
final submitted = await signed.submitted.future.timeout(
|
||||
const Duration(seconds: 1),
|
||||
);
|
||||
relay.emit(event({'dms': 'recent'}, submitted.createdAt! + 1, id: 'a'));
|
||||
expect(subject.store.groups, {'dms': ChannelSortMode.recent});
|
||||
subject.dispose();
|
||||
});
|
||||
|
||||
test(
|
||||
'future-dated remote event is ignored and cannot wedge publishing',
|
||||
() async {
|
||||
final relay = _FakeRelaySession()
|
||||
..historyEvents = [
|
||||
event({'channels': 'recent'}, 4102444800),
|
||||
];
|
||||
final signed = _RecordingSignedEventRelay();
|
||||
final subject = manager(relay, signed);
|
||||
await subject.initialize();
|
||||
expect(subject.sortModeFor('channels'), ChannelSortMode.alpha);
|
||||
subject.setSortModeFor('dms', ChannelSortMode.recent);
|
||||
await signed.submitted.future.timeout(const Duration(seconds: 1));
|
||||
subject.dispose();
|
||||
},
|
||||
);
|
||||
|
||||
test('retries failed startup and closes fetch-subscribe gap', () async {
|
||||
final relay = _FakeRelaySession()..fetchFailures = 1;
|
||||
final subject = manager(relay, _RecordingSignedEventRelay());
|
||||
await subject.initialize();
|
||||
relay.historyEvents = [
|
||||
event({'starred': 'recent'}, 300),
|
||||
];
|
||||
await Future<void>.delayed(const Duration(milliseconds: 40));
|
||||
expect(subject.sortModeFor('starred'), ChannelSortMode.recent);
|
||||
expect(relay.fetchCount, greaterThanOrEqualTo(3));
|
||||
subject.dispose();
|
||||
});
|
||||
|
||||
test('setSortModeFor prunes deleted custom section keys', () async {
|
||||
final subject = manager(_FakeRelaySession(), _RecordingSignedEventRelay());
|
||||
await subject.initialize();
|
||||
subject.setSortModeFor('section:dead', ChannelSortMode.recent);
|
||||
subject.setSortModeFor(
|
||||
'channels',
|
||||
ChannelSortMode.recent,
|
||||
liveSectionIds: ['live'],
|
||||
);
|
||||
expect(subject.store.groups.keys, ['channels']);
|
||||
subject.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
class _SubmittedEvent {
|
||||
final String content;
|
||||
final List<List<String>> tags;
|
||||
final int? createdAt;
|
||||
const _SubmittedEvent(this.content, this.tags, this.createdAt);
|
||||
}
|
||||
|
||||
class _RecordingSignedEventRelay implements SignedEventRelay {
|
||||
final submitted = Completer<_SubmittedEvent>();
|
||||
int submitCount = 0;
|
||||
|
||||
@override
|
||||
String? get pubkey => null;
|
||||
|
||||
@override
|
||||
Future<NostrEvent> submit({
|
||||
required int kind,
|
||||
required String content,
|
||||
required List<List<String>> tags,
|
||||
int? createdAt,
|
||||
void Function(NostrEvent event)? onSigned,
|
||||
}) async {
|
||||
submitCount++;
|
||||
onSigned?.call(
|
||||
const NostrEvent(
|
||||
id: 'signed-event',
|
||||
pubkey: '',
|
||||
createdAt: 0,
|
||||
kind: 0,
|
||||
tags: [],
|
||||
content: '',
|
||||
sig: '',
|
||||
),
|
||||
);
|
||||
if (!submitted.isCompleted) {
|
||||
submitted.complete(_SubmittedEvent(content, tags, createdAt));
|
||||
}
|
||||
return const NostrEvent(
|
||||
id: 'ack',
|
||||
pubkey: '',
|
||||
createdAt: 0,
|
||||
kind: 0,
|
||||
tags: [],
|
||||
content: '',
|
||||
sig: '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FakeRelaySession extends RelaySessionNotifier {
|
||||
List<NostrEvent> historyEvents = [];
|
||||
int fetchFailures = 0;
|
||||
int fetchCount = 0;
|
||||
void Function(NostrEvent)? _listener;
|
||||
|
||||
@override
|
||||
Future<List<NostrEvent>> fetchHistory(
|
||||
NostrFilter filter, {
|
||||
Duration timeout = const Duration(seconds: 8),
|
||||
}) async {
|
||||
fetchCount++;
|
||||
if (fetchFailures > 0) {
|
||||
fetchFailures--;
|
||||
throw Exception('rate limited');
|
||||
}
|
||||
return historyEvents;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void Function()> subscribe(
|
||||
NostrFilter filter,
|
||||
void Function(NostrEvent) onEvent, {
|
||||
void Function(String message)? onClosed,
|
||||
}) async {
|
||||
_listener = onEvent;
|
||||
return () => _listener = null;
|
||||
}
|
||||
|
||||
void emit(NostrEvent event) => _listener?.call(event);
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:buzz/features/channels/channel.dart';
|
||||
import 'package:buzz/features/channels/channel_sort/channel_sort_storage.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
void main() {
|
||||
group('ChannelSortStore JSON', () {
|
||||
test('round-trips desktop wire format and drops invalid modes', () {
|
||||
final store = ChannelSortStore(
|
||||
groups: {
|
||||
'channels': ChannelSortMode.recent,
|
||||
'dms': ChannelSortMode.alpha,
|
||||
},
|
||||
);
|
||||
expect(store.toJson()['groups'], {'channels': 'recent', 'dms': 'alpha'});
|
||||
expect(ChannelSortStore.fromJson(store.toJson()).groups, store.groups);
|
||||
expect(
|
||||
ChannelSortStore.fromJson({
|
||||
'version': 1,
|
||||
'groups': {'channels': 'recent', 'starred': 'bogus'},
|
||||
}).groups,
|
||||
{'channels': ChannelSortMode.recent},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('ChannelSortStorage', () {
|
||||
test('normalizes relay scope and isolates communities', () async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final storage = ChannelSortStorage(prefs);
|
||||
final store = ChannelSortStore(
|
||||
groups: {'channels': ChannelSortMode.recent},
|
||||
);
|
||||
storage.write('pk', ' WSS://Relay.Example/ ', store);
|
||||
expect(storage.read('pk', 'wss://relay.example').groups, store.groups);
|
||||
expect(storage.read('pk', 'wss://other.example').groups, isEmpty);
|
||||
});
|
||||
|
||||
test('migrates legacy unscoped cache into the first relay scope', () async {
|
||||
SharedPreferences.setMockInitialValues({
|
||||
legacyChannelSortKey('pk'): jsonEncode({
|
||||
'version': 1,
|
||||
'groups': {'dms': 'recent'},
|
||||
}),
|
||||
});
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final storage = ChannelSortStorage(prefs);
|
||||
expect(storage.read('pk', 'wss://one').groups, {
|
||||
'dms': ChannelSortMode.recent,
|
||||
});
|
||||
expect(prefs.getString(channelSortKey('pk', 'wss://one')), isNotNull);
|
||||
expect(prefs.getString(legacyChannelSortKey('pk')), isNull);
|
||||
expect(storage.read('pk', 'wss://two').groups, isEmpty);
|
||||
});
|
||||
|
||||
test('ignores corrupt and unsupported payloads', () async {
|
||||
SharedPreferences.setMockInitialValues({
|
||||
channelSortKey('pk', 'wss://one'): 'nope',
|
||||
channelSortKey('pk', 'wss://two'): '{"version":2,"groups":{}}',
|
||||
});
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final storage = ChannelSortStorage(prefs);
|
||||
expect(storage.read('pk', 'wss://one').groups, isEmpty);
|
||||
expect(storage.read('pk', 'wss://two').groups, isEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
test('prunes orphaned section modes but keeps fixed groups', () {
|
||||
final store = ChannelSortStore(
|
||||
groups: {
|
||||
'channels': ChannelSortMode.recent,
|
||||
'section:live': ChannelSortMode.recent,
|
||||
'section:dead': ChannelSortMode.alpha,
|
||||
},
|
||||
);
|
||||
expect(stripOrphanedSectionModes(store, ['live']).groups.keys, [
|
||||
'channels',
|
||||
'section:live',
|
||||
]);
|
||||
});
|
||||
|
||||
group('sortChannelsForList', () {
|
||||
Channel channel(String id, String name, {DateTime? lastMessageAt}) =>
|
||||
Channel(
|
||||
id: id,
|
||||
name: name,
|
||||
channelType: 'stream',
|
||||
visibility: 'open',
|
||||
description: '',
|
||||
createdBy: 'pk',
|
||||
createdAt: DateTime.utc(2026),
|
||||
memberCount: 1,
|
||||
lastMessageAt: lastMessageAt,
|
||||
);
|
||||
|
||||
test('alpha matches desktop code-unit collation and id tie-break', () {
|
||||
final sorted = sortChannelsForList([
|
||||
channel('2', 'zeta'),
|
||||
channel('3', 'Alpha'),
|
||||
channel('1', 'alpha'),
|
||||
channel('4', 'Éclair'),
|
||||
], ChannelSortMode.alpha);
|
||||
expect(sorted.map((c) => c.id), ['1', '3', '2', '4']);
|
||||
});
|
||||
|
||||
test('recent puts newest first and quiet channels alpha last', () {
|
||||
final now = DateTime.utc(2026, 7, 25);
|
||||
final sorted = sortChannelsForList([
|
||||
channel('a', 'quiet-z'),
|
||||
channel(
|
||||
'b',
|
||||
'old',
|
||||
lastMessageAt: now.subtract(const Duration(days: 2)),
|
||||
),
|
||||
channel('c', 'new', lastMessageAt: now),
|
||||
channel('d', 'quiet-a'),
|
||||
], ChannelSortMode.recent);
|
||||
expect(sorted.map((c) => c.id), ['c', 'b', 'd', 'a']);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user