TF-4265 Add integration test for attachment reminder on mobile

This commit is contained in:
dab246
2026-01-30 16:09:38 +07:00
committed by Dat H. Pham
parent 74c0c75023
commit 2a3325ab7a
17 changed files with 781 additions and 899 deletions
@@ -0,0 +1,94 @@
import 'package:core/utils/config/app_config_loader.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:tmail_ui_user/features/composer/presentation/manager/attachment_keyword_config_manager.dart';
import 'package:tmail_ui_user/features/composer/presentation/manager/attachment_keywords_configuration_parser.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/attachment_keyword_config.dart';
import 'attachment_keyword_config_manager_test.mocks.dart';
@GenerateMocks([AppConfigLoader])
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late AttachmentKeywordConfigManager manager;
late MockAppConfigLoader mockLoader;
setUp(() {
manager = AttachmentKeywordConfigManager();
manager.clearCache();
mockLoader = MockAppConfigLoader();
manager.injectLoader(mockLoader);
});
group('AttachmentKeywordConfigManager.getConfig', () {
test('returns config loaded from loader on first call', () async {
final config = AttachmentKeywordConfig(
includeList: ['invoice'],
excludeList: ['invoice-draft'],
);
when(mockLoader.load<AttachmentKeywordConfig>(
any,
any,
)).thenAnswer((_) async => config);
final result = await manager.getConfig();
expect(result.includeList, equals(['invoice']));
expect(result.excludeList, equals(['invoice-draft']));
});
test('returns cached result on second call without calling loader again', () async {
final config = AttachmentKeywordConfig(includeList: ['invoice']);
when(mockLoader.load<AttachmentKeywordConfig>(any, any))
.thenAnswer((_) async => config);
await manager.getConfig();
await manager.getConfig();
verify(mockLoader.load<AttachmentKeywordConfig>(
any,
argThat(isA<AttachmentKeywordsConfigurationParser>()),
)).called(1);
});
test('returns empty AttachmentKeywordConfig when loader throws', () async {
when(mockLoader.load<AttachmentKeywordConfig>(any, any))
.thenThrow(Exception('file not found'));
final result = await manager.getConfig();
expect(result.includeList, isEmpty);
expect(result.excludeList, isEmpty);
});
test('caches the empty fallback config on error (loader not called again)', () async {
when(mockLoader.load<AttachmentKeywordConfig>(any, any))
.thenThrow(Exception('file not found'));
await manager.getConfig();
await manager.getConfig();
verify(mockLoader.load<AttachmentKeywordConfig>(any, any)).called(1);
});
});
group('AttachmentKeywordConfigManager.clearCache', () {
test('clearCache forces re-load on next getConfig call', () async {
final config = AttachmentKeywordConfig(includeList: ['invoice']);
when(mockLoader.load<AttachmentKeywordConfig>(any, any))
.thenAnswer((_) async => config);
await manager.getConfig();
manager.clearCache();
manager.injectLoader(mockLoader);
await manager.getConfig();
verify(mockLoader.load<AttachmentKeywordConfig>(any, any)).called(2);
});
});
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,61 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:tmail_ui_user/features/composer/presentation/manager/exclude_list_filter.dart';
/// Helper: simulate a regex [Match] at [start]..[end] within [text].
Match _fakeMatch(String text, int start, int end) {
final regex = RegExp(RegExp.escape(text.substring(start, end)));
return regex.allMatches(text).firstWhere((m) => m.start == start);
}
void main() {
group('ExcludeListFilter.isValid', () {
test('returns true when excludeList is empty', () {
final filter = ExcludeListFilter([]);
const text ='See the file attached.';
final match = _fakeMatch(text, 8, 12); // "file"
expect(filter.isValid(text, match), isTrue);
});
test('blocks token that exactly matches an exclude entry', () {
final filter = ExcludeListFilter(['file-246']);
const text ='Please find file-246 here.';
final match = _fakeMatch(text, 12, 16); // "file"
expect(filter.isValid(text, match), isFalse);
});
test('blocks token with trailing punctuation (e.g. "file-246.")', () {
final filter = ExcludeListFilter(['file-246']);
const text ='Check file-246.';
final match = _fakeMatch(text, 6, 10); // "file"
expect(filter.isValid(text, match), isFalse);
});
test('blocks token with leading punctuation (e.g. "(file-246")', () {
final filter = ExcludeListFilter(['file-246']);
const text ='See (file-246 for details.';
final match = _fakeMatch(text, 5, 9); // "file"
expect(filter.isValid(text, match), isFalse);
});
test('allows token not in the exclude list', () {
final filter = ExcludeListFilter(['invoice-draft']);
const text ='Please attach the file.';
final match = _fakeMatch(text, 18, 22); // "file"
expect(filter.isValid(text, match), isTrue);
});
test('matching is case-insensitive', () {
final filter = ExcludeListFilter(['File-246']);
const text ='See file-246 here.';
final match = _fakeMatch(text, 4, 8); // "file"
expect(filter.isValid(text, match), isFalse);
});
test('allows a standalone keyword not surrounded by exclude context', () {
final filter = ExcludeListFilter(['file-246']);
const text ='Please attach the file here.';
final match = _fakeMatch(text, 18, 22); // "file"
expect(filter.isValid(text, match), isTrue);
});
});
}