mirror of
https://github.com/Ujwal223/FocusGram.git
synced 2026-04-05 02:42:28 +02:00
RELEASE: moved from beta to First stable release.
Check CHANGELOG.md for full changelog
This commit is contained in:
@@ -1,372 +0,0 @@
|
||||
// test/services/injection_controller_test.dart
|
||||
//
|
||||
// Tests for InjectionController — JS/CSS builder, Ghost Mode keyword resolver,
|
||||
// and JS string generation.
|
||||
//
|
||||
// Run with: flutter test test/services/injection_controller_test.dart
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:focusgram/services/injection_controller.dart';
|
||||
|
||||
void main() {
|
||||
// ── resolveBlockedKeywords ───────────────────────────────────────────────
|
||||
|
||||
group('InjectionController.resolveBlockedKeywords', () {
|
||||
test('returns empty list when all flags are false', () {
|
||||
final kws = InjectionController.resolveBlockedKeywords(
|
||||
typingIndicator: false,
|
||||
seenStatus: false,
|
||||
stories: false,
|
||||
dmPhotos: false,
|
||||
);
|
||||
expect(kws, isEmpty);
|
||||
});
|
||||
|
||||
test('includes seen keywords when seenStatus is true', () {
|
||||
final kws = InjectionController.resolveBlockedKeywords(
|
||||
typingIndicator: false,
|
||||
seenStatus: true,
|
||||
stories: false,
|
||||
dmPhotos: false,
|
||||
);
|
||||
expect(
|
||||
kws,
|
||||
containsAll(['/seen', 'media/seen', 'reel/seen', '/mark_seen']),
|
||||
);
|
||||
});
|
||||
|
||||
test('includes typing keywords when typingIndicator is true', () {
|
||||
final kws = InjectionController.resolveBlockedKeywords(
|
||||
typingIndicator: true,
|
||||
seenStatus: false,
|
||||
stories: false,
|
||||
dmPhotos: false,
|
||||
);
|
||||
expect(kws, containsAll(['set_typing_status', '/typing']));
|
||||
});
|
||||
|
||||
test('includes live keywords when stories is true', () {
|
||||
final kws = InjectionController.resolveBlockedKeywords(
|
||||
typingIndicator: false,
|
||||
seenStatus: false,
|
||||
stories: true,
|
||||
dmPhotos: false,
|
||||
);
|
||||
expect(kws, contains('/live/'));
|
||||
});
|
||||
|
||||
test('includes visual_item_seen when dmPhotos is true', () {
|
||||
final kws = InjectionController.resolveBlockedKeywords(
|
||||
typingIndicator: false,
|
||||
seenStatus: false,
|
||||
stories: false,
|
||||
dmPhotos: true,
|
||||
);
|
||||
expect(kws, contains('visual_item_seen'));
|
||||
});
|
||||
|
||||
test('all flags true — returns all groups combined', () {
|
||||
final kws = InjectionController.resolveBlockedKeywords(
|
||||
typingIndicator: true,
|
||||
seenStatus: true,
|
||||
stories: true,
|
||||
dmPhotos: true,
|
||||
);
|
||||
// Must contain at least one keyword from every group
|
||||
expect(
|
||||
kws,
|
||||
containsAll([
|
||||
'/seen',
|
||||
'set_typing_status',
|
||||
'/live/',
|
||||
'visual_item_seen',
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('no duplicates in result (seen + typing + stories + dmPhotos)', () {
|
||||
final kws = InjectionController.resolveBlockedKeywords(
|
||||
typingIndicator: true,
|
||||
seenStatus: true,
|
||||
stories: true,
|
||||
dmPhotos: true,
|
||||
);
|
||||
final unique = kws.toSet();
|
||||
expect(kws.length, unique.length);
|
||||
});
|
||||
});
|
||||
|
||||
// ── resolveWsBlockedKeywords ─────────────────────────────────────────────
|
||||
|
||||
group('InjectionController.resolveWsBlockedKeywords', () {
|
||||
test('returns empty list when typingIndicator is false', () {
|
||||
expect(
|
||||
InjectionController.resolveWsBlockedKeywords(typingIndicator: false),
|
||||
isEmpty,
|
||||
);
|
||||
});
|
||||
|
||||
test('returns non-empty list when typingIndicator is true', () {
|
||||
final kws = InjectionController.resolveWsBlockedKeywords(
|
||||
typingIndicator: true,
|
||||
);
|
||||
expect(kws, isNotEmpty);
|
||||
expect(kws, contains('activity_status'));
|
||||
});
|
||||
});
|
||||
|
||||
// ── buildGhostModeJS ─────────────────────────────────────────────────────
|
||||
|
||||
group('InjectionController.buildGhostModeJS', () {
|
||||
test('returns empty string when all flags are false', () {
|
||||
final js = InjectionController.buildGhostModeJS(
|
||||
typingIndicator: false,
|
||||
seenStatus: false,
|
||||
stories: false,
|
||||
dmPhotos: false,
|
||||
);
|
||||
expect(js.trim(), isEmpty);
|
||||
});
|
||||
|
||||
test('generated JS contains seen keywords when seenStatus=true', () {
|
||||
final js = InjectionController.buildGhostModeJS(
|
||||
typingIndicator: false,
|
||||
seenStatus: true,
|
||||
stories: false,
|
||||
dmPhotos: false,
|
||||
);
|
||||
expect(js, contains('/seen'));
|
||||
expect(js, contains('media/seen'));
|
||||
});
|
||||
|
||||
test('generated JS contains typing keywords when typingIndicator=true', () {
|
||||
final js = InjectionController.buildGhostModeJS(
|
||||
typingIndicator: true,
|
||||
seenStatus: false,
|
||||
stories: false,
|
||||
dmPhotos: false,
|
||||
);
|
||||
expect(js, contains('set_typing_status'));
|
||||
});
|
||||
|
||||
test('generated JS contains live keyword when stories=true', () {
|
||||
final js = InjectionController.buildGhostModeJS(
|
||||
typingIndicator: false,
|
||||
seenStatus: false,
|
||||
stories: true,
|
||||
dmPhotos: false,
|
||||
);
|
||||
expect(js, contains('/live/'));
|
||||
});
|
||||
|
||||
test('generated JS contains BLOCKED array and shouldBlock function', () {
|
||||
final js = InjectionController.buildGhostModeJS(
|
||||
typingIndicator: false,
|
||||
seenStatus: true,
|
||||
stories: false,
|
||||
dmPhotos: false,
|
||||
);
|
||||
expect(js, contains('BLOCKED'));
|
||||
expect(js, contains('shouldBlock'));
|
||||
});
|
||||
|
||||
test('generated JS wraps XHR and fetch', () {
|
||||
final js = InjectionController.buildGhostModeJS(
|
||||
typingIndicator: true,
|
||||
seenStatus: true,
|
||||
stories: true,
|
||||
dmPhotos: true,
|
||||
);
|
||||
expect(js, contains('window.fetch'));
|
||||
expect(js, contains('XMLHttpRequest.prototype.open'));
|
||||
expect(js, contains('XMLHttpRequest.prototype.send'));
|
||||
});
|
||||
|
||||
test('WS patch is included when typingIndicator=true', () {
|
||||
final js = InjectionController.buildGhostModeJS(
|
||||
typingIndicator: true,
|
||||
seenStatus: false,
|
||||
stories: false,
|
||||
dmPhotos: false,
|
||||
);
|
||||
expect(js, contains('WebSocket'));
|
||||
});
|
||||
|
||||
test('WS patch is NOT included when typingIndicator=false', () {
|
||||
final js = InjectionController.buildGhostModeJS(
|
||||
typingIndicator: false,
|
||||
seenStatus: true,
|
||||
stories: false,
|
||||
dmPhotos: false,
|
||||
);
|
||||
// WS_KEYS will be empty array; the `if (WS_KEYS.length > 0)` guard
|
||||
// prevents the WS override from running — but the string may still be present.
|
||||
// At minimum, WS_KEYS should be empty in the output.
|
||||
expect(js, contains('WS_KEYS = []'));
|
||||
});
|
||||
});
|
||||
|
||||
// ── buildSessionStateJS ──────────────────────────────────────────────────
|
||||
|
||||
group('InjectionController.buildSessionStateJS', () {
|
||||
test('returns true assignment when active', () {
|
||||
expect(
|
||||
InjectionController.buildSessionStateJS(true),
|
||||
contains('__focusgramSessionActive = true'),
|
||||
);
|
||||
});
|
||||
|
||||
test('returns false assignment when inactive', () {
|
||||
expect(
|
||||
InjectionController.buildSessionStateJS(false),
|
||||
contains('__focusgramSessionActive = false'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ── softNavigateJS ───────────────────────────────────────────────────────
|
||||
|
||||
group('InjectionController.softNavigateJS', () {
|
||||
test('contains the target path', () {
|
||||
final js = InjectionController.softNavigateJS('/direct/inbox/');
|
||||
expect(js, contains('/direct/inbox/'));
|
||||
});
|
||||
|
||||
test('contains location.href assignment', () {
|
||||
final js = InjectionController.softNavigateJS('/explore/');
|
||||
expect(js, contains('location.href'));
|
||||
});
|
||||
});
|
||||
|
||||
// ── buildInjectionJS ─────────────────────────────────────────────────────
|
||||
|
||||
group('InjectionController.buildInjectionJS', () {
|
||||
InjectionController.buildInjectionJS; // reference check
|
||||
|
||||
test('contains session state flag', () {
|
||||
final js = _buildFull();
|
||||
expect(js, contains('__focusgramSessionActive'));
|
||||
});
|
||||
|
||||
test('contains path tracker when assembled', () {
|
||||
final js = _buildFull();
|
||||
expect(js, contains('fgTrackPath'));
|
||||
});
|
||||
|
||||
test('includes reels block JS when session is not active', () {
|
||||
final js = InjectionController.buildInjectionJS(
|
||||
sessionActive: false,
|
||||
blurExplore: false,
|
||||
blurReels: false,
|
||||
ghostTyping: false,
|
||||
ghostSeen: false,
|
||||
ghostStories: false,
|
||||
ghostDmPhotos: false,
|
||||
enableTextSelection: false,
|
||||
);
|
||||
expect(js, contains('fgReelsBlock'));
|
||||
});
|
||||
|
||||
test('does NOT include reels block JS when session is active', () {
|
||||
final js = InjectionController.buildInjectionJS(
|
||||
sessionActive: true,
|
||||
blurExplore: false,
|
||||
blurReels: false,
|
||||
ghostTyping: false,
|
||||
ghostSeen: false,
|
||||
ghostStories: false,
|
||||
ghostDmPhotos: false,
|
||||
enableTextSelection: false,
|
||||
);
|
||||
expect(js, isNot(contains('fgReelsBlock')));
|
||||
});
|
||||
|
||||
test('always includes link sanitizer', () {
|
||||
final js = InjectionController.buildInjectionJS(
|
||||
sessionActive: false,
|
||||
blurExplore: false,
|
||||
blurReels: false,
|
||||
ghostTyping: false,
|
||||
ghostSeen: false,
|
||||
ghostStories: false,
|
||||
ghostDmPhotos: false,
|
||||
enableTextSelection: false,
|
||||
);
|
||||
// linkSanitizationJS is now always injected (not togglable)
|
||||
expect(js, contains('fgSanitize'));
|
||||
});
|
||||
|
||||
test('returns non-empty string in all cases', () {
|
||||
expect(_buildFull().trim(), isNotEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
// ── iOSUserAgent sanity ──────────────────────────────────────────────────
|
||||
|
||||
group('InjectionController.iOSUserAgent', () {
|
||||
test('contains iPhone identifier', () {
|
||||
expect(InjectionController.iOSUserAgent, contains('iPhone'));
|
||||
});
|
||||
|
||||
test('contains FBAN (Instagram app identifier)', () {
|
||||
expect(InjectionController.iOSUserAgent, contains('FBAN'));
|
||||
});
|
||||
|
||||
test('is non-empty', () {
|
||||
expect(InjectionController.iOSUserAgent, isNotEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
// ── notificationBridgeJS ─────────────────────────────────────────────────
|
||||
|
||||
group('InjectionController.notificationBridgeJS', () {
|
||||
test('contains Notification bridge guard', () {
|
||||
expect(
|
||||
InjectionController.notificationBridgeJS,
|
||||
contains('fgNotifBridged'),
|
||||
);
|
||||
});
|
||||
|
||||
test('patches window.Notification', () {
|
||||
expect(
|
||||
InjectionController.notificationBridgeJS,
|
||||
contains('window.Notification'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ── linkSanitizationJS ───────────────────────────────────────────────────
|
||||
|
||||
group('InjectionController.linkSanitizationJS', () {
|
||||
test('strips igsh param', () {
|
||||
expect(InjectionController.linkSanitizationJS, contains('igsh'));
|
||||
});
|
||||
|
||||
test('strips utm params', () {
|
||||
expect(InjectionController.linkSanitizationJS, contains('utm_source'));
|
||||
});
|
||||
|
||||
test('strips fbclid', () {
|
||||
expect(InjectionController.linkSanitizationJS, contains('fbclid'));
|
||||
});
|
||||
|
||||
test('patches navigator.share', () {
|
||||
expect(
|
||||
InjectionController.linkSanitizationJS,
|
||||
contains('navigator.share'),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Helper to create a fully-featured injection JS for common assertions.
|
||||
String _buildFull() => InjectionController.buildInjectionJS(
|
||||
sessionActive: false,
|
||||
blurExplore: true,
|
||||
blurReels: true,
|
||||
ghostTyping: true,
|
||||
ghostSeen: true,
|
||||
ghostStories: true,
|
||||
ghostDmPhotos: true,
|
||||
enableTextSelection: false,
|
||||
);
|
||||
@@ -1,250 +0,0 @@
|
||||
// test/services/settings_service_test.dart
|
||||
//
|
||||
// Tests for SettingsService — default values, setters, tab management,
|
||||
// and the legacy GhostMode key migration.
|
||||
//
|
||||
// Note: SettingsService requires SharedPreferences. We use
|
||||
// SharedPreferences.setMockInitialValues({}) to avoid platform channel calls.
|
||||
//
|
||||
// Run with: flutter test test/services/settings_service_test.dart
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:focusgram/services/settings_service.dart';
|
||||
|
||||
/// Helper: create an initialised SettingsService with a clean prefs slate.
|
||||
Future<SettingsService> makeService() async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final svc = SettingsService();
|
||||
await svc.init();
|
||||
return svc;
|
||||
}
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
});
|
||||
|
||||
// ── Default values ──────────────────────────────────────────────────────
|
||||
|
||||
group('SettingsService — defaults', () {
|
||||
test('blurExplore defaults to true', () async {
|
||||
expect((await makeService()).blurExplore, isTrue);
|
||||
});
|
||||
|
||||
test('blurReels defaults to false', () async {
|
||||
expect((await makeService()).blurReels, isFalse);
|
||||
});
|
||||
|
||||
test('requireLongPress defaults to true', () async {
|
||||
expect((await makeService()).requireLongPress, isTrue);
|
||||
});
|
||||
|
||||
test('showBreathGate defaults to true', () async {
|
||||
expect((await makeService()).showBreathGate, isTrue);
|
||||
});
|
||||
|
||||
test('requireWordChallenge defaults to true', () async {
|
||||
expect((await makeService()).requireWordChallenge, isTrue);
|
||||
});
|
||||
|
||||
test('enableTextSelection defaults to false', () async {
|
||||
expect((await makeService()).enableTextSelection, isFalse);
|
||||
});
|
||||
|
||||
test('ghostTyping defaults to true', () async {
|
||||
expect((await makeService()).ghostTyping, isTrue);
|
||||
});
|
||||
|
||||
test('ghostSeen defaults to true', () async {
|
||||
expect((await makeService()).ghostSeen, isTrue);
|
||||
});
|
||||
|
||||
test('ghostStories defaults to true', () async {
|
||||
expect((await makeService()).ghostStories, isTrue);
|
||||
});
|
||||
|
||||
test('ghostDmPhotos defaults to true', () async {
|
||||
expect((await makeService()).ghostDmPhotos, isTrue);
|
||||
});
|
||||
|
||||
test('sanitizeLinks defaults to true', () async {
|
||||
expect((await makeService()).sanitizeLinks, isTrue);
|
||||
});
|
||||
|
||||
test('isFirstRun defaults to true', () async {
|
||||
expect((await makeService()).isFirstRun, isTrue);
|
||||
});
|
||||
|
||||
test(
|
||||
'anyGhostModeEnabled is true when all ghost settings are true',
|
||||
() async {
|
||||
expect((await makeService()).anyGhostModeEnabled, isTrue);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// ── Setters ─────────────────────────────────────────────────────────────
|
||||
|
||||
group('SettingsService — setters persist and notify', () {
|
||||
test('setBlurExplore changes value and notifies', () async {
|
||||
final svc = await makeService();
|
||||
bool notified = false;
|
||||
svc.addListener(() => notified = true);
|
||||
await svc.setBlurExplore(false);
|
||||
expect(svc.blurExplore, isFalse);
|
||||
expect(notified, isTrue);
|
||||
});
|
||||
|
||||
test('setBlurReels persists', () async {
|
||||
final svc = await makeService();
|
||||
await svc.setBlurReels(true);
|
||||
expect(svc.blurReels, isTrue);
|
||||
});
|
||||
|
||||
test('setRequireLongPress persists', () async {
|
||||
final svc = await makeService();
|
||||
await svc.setRequireLongPress(false);
|
||||
expect(svc.requireLongPress, isFalse);
|
||||
});
|
||||
|
||||
test('setGhostTyping turns off ghost typing', () async {
|
||||
final svc = await makeService();
|
||||
await svc.setGhostTyping(false);
|
||||
expect(svc.ghostTyping, isFalse);
|
||||
});
|
||||
|
||||
test('setGhostSeen turns off ghost seen', () async {
|
||||
final svc = await makeService();
|
||||
await svc.setGhostSeen(false);
|
||||
expect(svc.ghostSeen, isFalse);
|
||||
});
|
||||
|
||||
test('setGhostStories turns off ghost stories', () async {
|
||||
final svc = await makeService();
|
||||
await svc.setGhostStories(false);
|
||||
expect(svc.ghostStories, isFalse);
|
||||
});
|
||||
|
||||
test('setGhostDmPhotos turns off ghost dm photos', () async {
|
||||
final svc = await makeService();
|
||||
await svc.setGhostDmPhotos(false);
|
||||
expect(svc.ghostDmPhotos, isFalse);
|
||||
});
|
||||
|
||||
test('setSanitizeLinks persists', () async {
|
||||
final svc = await makeService();
|
||||
await svc.setSanitizeLinks(false);
|
||||
expect(svc.sanitizeLinks, isFalse);
|
||||
});
|
||||
|
||||
test('setFirstRunCompleted sets isFirstRun to false', () async {
|
||||
final svc = await makeService();
|
||||
await svc.setFirstRunCompleted();
|
||||
expect(svc.isFirstRun, isFalse);
|
||||
});
|
||||
|
||||
test('setEnableTextSelection persists', () async {
|
||||
final svc = await makeService();
|
||||
await svc.setEnableTextSelection(true);
|
||||
expect(svc.enableTextSelection, isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
// ── anyGhostModeEnabled ──────────────────────────────────────────────────
|
||||
|
||||
group('SettingsService.anyGhostModeEnabled', () {
|
||||
test('is false when all ghost flags are off', () async {
|
||||
final svc = await makeService();
|
||||
await svc.setGhostTyping(false);
|
||||
await svc.setGhostSeen(false);
|
||||
await svc.setGhostStories(false);
|
||||
await svc.setGhostDmPhotos(false);
|
||||
expect(svc.anyGhostModeEnabled, isFalse);
|
||||
});
|
||||
|
||||
test('is true when only one ghost flag is on', () async {
|
||||
final svc = await makeService();
|
||||
await svc.setGhostTyping(false);
|
||||
await svc.setGhostSeen(false);
|
||||
await svc.setGhostStories(false);
|
||||
await svc.setGhostDmPhotos(true); // only dmPhotos on
|
||||
expect(svc.anyGhostModeEnabled, isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Tab management ───────────────────────────────────────────────────────
|
||||
|
||||
group('SettingsService — tab management', () {
|
||||
test('default tabs include Home, Reels, Messages, Profile', () async {
|
||||
final svc = await makeService();
|
||||
expect(
|
||||
svc.enabledTabs,
|
||||
containsAll(['Home', 'Reels', 'Messages', 'Profile']),
|
||||
);
|
||||
});
|
||||
|
||||
test('toggleTab removes an enabled tab', () async {
|
||||
final svc = await makeService();
|
||||
final before = List<String>.from(svc.enabledTabs);
|
||||
await svc.toggleTab('Reels');
|
||||
expect(svc.enabledTabs, isNot(contains('Reels')));
|
||||
expect(svc.enabledTabs.length, before.length - 1);
|
||||
});
|
||||
|
||||
test('toggleTab adds a tab back when toggled again', () async {
|
||||
final svc = await makeService();
|
||||
await svc.toggleTab('Reels');
|
||||
await svc.toggleTab('Reels');
|
||||
expect(svc.enabledTabs, contains('Reels'));
|
||||
});
|
||||
|
||||
test('toggleTab does not remove the last remaining tab', () async {
|
||||
final svc = await makeService();
|
||||
final tabs = List<String>.from(svc.enabledTabs);
|
||||
for (final t in tabs.sublist(0, tabs.length - 1)) {
|
||||
await svc.toggleTab(t);
|
||||
}
|
||||
final last = svc.enabledTabs.first;
|
||||
await svc.toggleTab(last); // try to remove the last one
|
||||
expect(svc.enabledTabs.length, 1); // still 1
|
||||
});
|
||||
|
||||
test('reorderTab moves item correctly — no tabs are lost', () async {
|
||||
final svc = await makeService();
|
||||
final original = List<String>.from(svc.enabledTabs);
|
||||
await svc.reorderTab(0, 1);
|
||||
expect(svc.enabledTabs.toSet(), original.toSet());
|
||||
});
|
||||
});
|
||||
|
||||
// ── Legacy Ghost Mode migration ──────────────────────────────────────────
|
||||
|
||||
group('SettingsService — legacy ghost mode migration', () {
|
||||
test(
|
||||
'migrates legacy ghost_mode=true to all four granular flags',
|
||||
() async {
|
||||
SharedPreferences.setMockInitialValues({'set_ghost_mode': true});
|
||||
final svc = SettingsService();
|
||||
await svc.init();
|
||||
expect(svc.ghostTyping, isTrue);
|
||||
expect(svc.ghostSeen, isTrue);
|
||||
expect(svc.ghostStories, isTrue);
|
||||
expect(svc.ghostDmPhotos, isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'migrates legacy ghost_mode=false to all four granular flags off',
|
||||
() async {
|
||||
SharedPreferences.setMockInitialValues({'set_ghost_mode': false});
|
||||
final svc = SettingsService();
|
||||
await svc.init();
|
||||
expect(svc.ghostTyping, isFalse);
|
||||
expect(svc.ghostSeen, isFalse);
|
||||
expect(svc.ghostStories, isFalse);
|
||||
expect(svc.ghostDmPhotos, isFalse);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user