mobile bluesky app made with flutter
lazurite.stormlightlabs.org/
mobile
bluesky
flutter
1import 'package:atproto/com_atproto_repo_listrecords.dart';
2import 'package:atproto_core/atproto_core.dart';
3import 'package:bluesky/app_bsky_actor_defs.dart';
4import 'package:flutter_test/flutter_test.dart';
5import 'package:lazurite/features/devtools/cubit/dev_tools_cubit.dart';
6
7void main() {
8 group('CollectionSummary', () {
9 test('countLabel shows placeholder until count is known', () {
10 expect(const CollectionSummary('app.bsky.feed.post').countLabel, '...');
11 expect(const CollectionSummary('app.bsky.feed.post', recordCount: 12).countLabel, '12');
12 });
13 });
14
15 group('RecordInfo', () {
16 test('creates with required parameters', () {
17 const record = RecordInfo(
18 uri: 'at://did:plc:test/app.bsky.feed.post/abc123',
19 cid: 'bafyrei123',
20 value: {'text': 'Hello'},
21 );
22
23 expect(record.uri, 'at://did:plc:test/app.bsky.feed.post/abc123');
24 expect(record.cid, 'bafyrei123');
25 expect(record.value, {'text': 'Hello'});
26 });
27
28 test('rkey extracts last path segment', () {
29 const record = RecordInfo(uri: 'at://did:plc:test/app.bsky.feed.post/abc123', value: {});
30 expect(record.rkey, 'abc123');
31 });
32
33 test('rkey returns empty string for invalid URI', () {
34 const record = RecordInfo(uri: 'invalid', value: {});
35 expect(record.rkey, '');
36 });
37
38 test('atUriToLink constructs aturi.to URL', () {
39 const record = RecordInfo(
40 uri: 'at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/app.bsky.feed.post/3m6mwoadjbp2d',
41 value: {},
42 );
43
44 expect(record.atUriToLink, 'https://aturi.to/did:plc:ewvi7nxzyoun6zhxrhs64oiz/app.bsky.feed.post/3m6mwoadjbp2d');
45 });
46 });
47
48 group('DevToolsState', () {
49 test('initial state has default values', () {
50 const state = DevToolsState();
51
52 expect(state.status, DevToolsStatus.initial);
53 expect(state.did, isNull);
54 expect(state.handle, isNull);
55 expect(state.repoHandle, isNull);
56 expect(state.typeaheadActors, isEmpty);
57 expect(state.isTypeaheadLoading, isFalse);
58 expect(state.collections, isEmpty);
59 expect(state.isCollectionCountsLoading, isFalse);
60 expect(state.selectedCollection, isNull);
61 expect(state.records, isNull);
62 expect(state.recordsCursor, isNull);
63 expect(state.selectedRecord, isNull);
64 expect(state.isCollectionLoading, isFalse);
65 expect(state.isRecordLoading, isFalse);
66 expect(state.errorMessage, isNull);
67 });
68
69 test('isLoading returns true for loading statuses', () {
70 expect(const DevToolsState(status: DevToolsStatus.loading).isLoading, isTrue);
71 expect(const DevToolsState(status: DevToolsStatus.loadingMore).isLoading, isTrue);
72 expect(const DevToolsState(status: DevToolsStatus.initial).isLoading, isFalse);
73 expect(const DevToolsState(status: DevToolsStatus.repoLoaded).isLoading, isFalse);
74 });
75
76 test('isNavigating returns true for collection or record transitions', () {
77 expect(const DevToolsState(isCollectionLoading: true).isNavigating, isTrue);
78 expect(const DevToolsState(isRecordLoading: true).isNavigating, isTrue);
79 expect(const DevToolsState().isNavigating, isFalse);
80 });
81
82 test('hasMoreRecords returns true when cursor exists', () {
83 expect(const DevToolsState(recordsCursor: 'cursor123').hasMoreRecords, isTrue);
84 expect(const DevToolsState(recordsCursor: '').hasMoreRecords, isFalse);
85 expect(const DevToolsState().hasMoreRecords, isFalse);
86 });
87
88 test('totalRecords returns loaded records length', () {
89 final records = [
90 const RepoListRecordsRecord(
91 uri: AtUri('at://did:plc:test/app.bsky.feed.post/1'),
92 cid: 'cid1',
93 value: {'text': 'test'},
94 ),
95 const RepoListRecordsRecord(
96 uri: AtUri('at://did:plc:test/app.bsky.feed.post/2'),
97 cid: 'cid2',
98 value: {'text': 'test2'},
99 ),
100 ];
101
102 expect(DevToolsState(records: records).totalRecords, 2);
103 expect(const DevToolsState().totalRecords, 0);
104 });
105
106 test('totalRepoRecords returns null until all counts are known', () {
107 const state = DevToolsState(
108 collections: [CollectionSummary('app.bsky.feed.post', recordCount: 2), CollectionSummary('app.bsky.feed.like')],
109 );
110
111 expect(state.totalRepoRecords, isNull);
112 expect(
113 const DevToolsState(
114 collections: [
115 CollectionSummary('app.bsky.feed.post', recordCount: 2),
116 CollectionSummary('app.bsky.feed.like', recordCount: 3),
117 ],
118 ).totalRepoRecords,
119 5,
120 );
121 });
122
123 test('copyWith preserves unspecified values', () {
124 const state = DevToolsState(
125 status: DevToolsStatus.repoLoaded,
126 did: 'did:plc:test',
127 handle: 'test.bsky.social',
128 repoHandle: 'test.bsky.social',
129 collections: [CollectionSummary('app.bsky.feed.post')],
130 );
131
132 final copied = state.copyWith(selectedCollection: 'app.bsky.feed.post');
133
134 expect(copied.status, DevToolsStatus.repoLoaded);
135 expect(copied.did, 'did:plc:test');
136 expect(copied.selectedCollection, 'app.bsky.feed.post');
137 });
138
139 test('copyWith can clear nullable fields', () {
140 final records = [
141 const RepoListRecordsRecord(
142 uri: AtUri('at://did:plc:test/app.bsky.feed.post/1'),
143 cid: 'cid1',
144 value: {'text': 'test'},
145 ),
146 ];
147
148 final state = DevToolsState(
149 status: DevToolsStatus.recordLoaded,
150 did: 'did:plc:test',
151 handle: 'test.bsky.social',
152 repoHandle: 'test.bsky.social',
153 typeaheadActors: [const ProfileViewBasic(did: 'did:plc:test', handle: 'test.bsky.social')],
154 isTypeaheadLoading: true,
155 collections: const [CollectionSummary('app.bsky.feed.post', recordCount: 1)],
156 isCollectionCountsLoading: true,
157 selectedCollection: 'app.bsky.feed.post',
158 records: records,
159 recordsCursor: 'cursor',
160 selectedRecord: const RecordInfo(uri: 'at://did:plc:test/app.bsky.feed.post/1', value: {'text': 'full'}),
161 isCollectionLoading: true,
162 isRecordLoading: true,
163 errorMessage: 'error',
164 );
165
166 final updated = state.copyWith(
167 did: null,
168 handle: null,
169 repoHandle: null,
170 typeaheadActors: const [],
171 isTypeaheadLoading: false,
172 isCollectionCountsLoading: false,
173 selectedCollection: null,
174 records: null,
175 recordsCursor: null,
176 selectedRecord: null,
177 isCollectionLoading: false,
178 isRecordLoading: false,
179 errorMessage: null,
180 );
181
182 expect(updated.did, isNull);
183 expect(updated.handle, isNull);
184 expect(updated.repoHandle, isNull);
185 expect(updated.typeaheadActors, isEmpty);
186 expect(updated.isTypeaheadLoading, isFalse);
187 expect(updated.isCollectionCountsLoading, isFalse);
188 expect(updated.selectedCollection, isNull);
189 expect(updated.records, isNull);
190 expect(updated.recordsCursor, isNull);
191 expect(updated.selectedRecord, isNull);
192 expect(updated.isCollectionLoading, isFalse);
193 expect(updated.isRecordLoading, isFalse);
194 expect(updated.errorMessage, isNull);
195 });
196
197 test('props includes all fields for equality', () {
198 const state = DevToolsState(
199 status: DevToolsStatus.repoLoaded,
200 did: 'did:plc:test',
201 handle: 'test.bsky.social',
202 repoHandle: 'test.bsky.social',
203 typeaheadActors: [ProfileViewBasic(did: 'did:plc:test', handle: 'test.bsky.social')],
204 isTypeaheadLoading: true,
205 collections: [CollectionSummary('app.bsky.feed.post', recordCount: 1)],
206 isCollectionCountsLoading: true,
207 selectedCollection: 'app.bsky.feed.post',
208 recordsCursor: 'cursor',
209 selectedRecord: RecordInfo(uri: 'at://test', value: {}),
210 isCollectionLoading: true,
211 isRecordLoading: true,
212 errorMessage: 'error',
213 );
214
215 expect(state.props.length, 16);
216 expect(state.props, contains(DevToolsStatus.repoLoaded));
217 expect(state.props, contains(true));
218 expect(state.props, contains('did:plc:test'));
219 });
220 });
221}