RELEASE: moved from beta to First stable release.

Check CHANGELOG.md for full changelog
This commit is contained in:
Ujwal
2026-02-27 04:14:40 +05:45
parent eecb823e62
commit 7992d65bc8
64 changed files with 6208 additions and 2752 deletions

View File

@@ -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,
);

View File

@@ -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);
},
);
});
}