Feature Pack with bug fixes for V2

This commit is contained in:
Ujwal223
2026-06-09 23:39:43 +05:45
parent f1bd12f0bd
commit 39b6545e4a
53 changed files with 7314 additions and 328 deletions
+80
View File
@@ -0,0 +1,80 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:focusgram/services/app_lock_service.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() {
SharedPreferences.setMockInitialValues({});
});
group('AppLockService — PIN verification', () {
test('verifyPin returns true for correct PIN', () async {
final service = AppLockService();
await service.init();
// Set a PIN first
await service.setPin('1234', forAppWide: true);
// Verify it
final valid = await service.verifyPin('1234');
expect(valid, isTrue);
});
test('verifyPin returns false for wrong PIN', () async {
final service = AppLockService();
await service.init();
await service.setPin('1234', forAppWide: true);
final valid = await service.verifyPin('0000');
expect(valid, isFalse);
});
test('verifyPin with forAppWide:false checks messages PIN', () async {
final service = AppLockService();
await service.init();
// Set messages PIN
await service.setPin('5678', forAppWide: false);
// Verify with forAppWide: false (messages PIN)
final valid = await service.verifyPin('5678', forAppWide: false);
expect(valid, isTrue);
});
test('onUnlocked resets lock state', () async {
final service = AppLockService();
await service.init();
await service.setPin('1234', forAppWide: true);
await service.onBackgrounded();
expect(service.shouldLockOnResume, isTrue);
service.onUnlocked();
expect(service.shouldLockOnResume, isFalse);
expect(service.isShowingLock, isFalse);
});
});
group('AppLockService — PIN management', () {
test('hasPin returns true after PIN is set', () async {
final service = AppLockService();
await service.init();
expect(service.hasPin, isFalse);
await service.setPin('1234', forAppWide: true);
expect(service.hasPin, isTrue);
});
test('verifyPin returns false when no PIN is set', () async {
final service = AppLockService();
await service.init();
final valid = await service.verifyPin('1234');
expect(valid, isFalse);
});
});
}
@@ -0,0 +1,92 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:focusgram/focus_settings.dart';
import 'package:focusgram/scripts/focus_scripts.dart';
void main() {
group('FocusSettings — Field cleanup', () {
test('only ghostMode remains (fullDmGhost and storyGhost removed)',
() async {
const settings = FocusSettings(ghostMode: true);
expect(settings.ghostMode, isTrue);
expect(settings.noAds, isTrue);
expect(settings.noStories, isFalse);
expect(settings.noReels, isFalse);
expect(settings.noAutoplay, isFalse);
expect(settings.noDMs, isFalse);
// Verify fullDmGhost and storyGhost are NOT fields anymore
// (these would be compile errors if they existed)
});
test('default ghostMode is false', () async {
const settings = FocusSettings();
expect(settings.ghostMode, isFalse);
});
});
group('buildUserScripts — DM Ghost injection', () {
test('injects kFullDmGhostJS when ghostMode is true', () async {
final scripts = buildUserScripts(const FocusSettings(ghostMode: true));
expect(scripts.length, equals(1));
expect(
scripts[0].injectionTime,
equals(UserScriptInjectionTime.AT_DOCUMENT_START),
);
// Verify the comprehensive Full DM ghost JS is injected
final src = scripts[0].source;
expect(src, contains('__fgFullDmGhost=true'));
expect(src, contains('__fgFullDmGhostPatched'));
expect(src, contains('shouldBlockDmPath'));
expect(src, contains('DM_URLS'));
expect(src, contains('DM_OPS'));
expect(src, contains('serviceWorker'));
expect(src, contains('sendBeacon'));
});
test('does NOT inject ghost scripts when ghostMode is false', () async {
final scripts =
buildUserScripts(const FocusSettings(ghostMode: false));
// Should have no DOCUMENT_START scripts
final startScripts =
scripts.where((s) =>
s.injectionTime == UserScriptInjectionTime.AT_DOCUMENT_START);
for (final s in startScripts) {
expect(s.source.contains('__fgFullDmGhost'), isFalse);
}
});
test('injects noAutoplay alongside DM Ghost', () async {
final scripts = buildUserScripts(
const FocusSettings(ghostMode: true, noAutoplay: true),
);
final startScripts =
scripts.where((s) =>
s.injectionTime == UserScriptInjectionTime.AT_DOCUMENT_START);
expect(startScripts.length, equals(1));
expect(startScripts.first.source, contains('__fgFullDmGhost=true'));
expect(startScripts.first.source, contains('document.addEventListener'));
});
test('injects hideStoryTray at DOCUMENT_END when noStories is true',
() async {
final scripts = buildUserScripts(
const FocusSettings(noStories: true),
);
final endScripts =
scripts.where((s) =>
s.injectionTime == UserScriptInjectionTime.AT_DOCUMENT_END);
expect(endScripts.length, equals(1));
expect(
endScripts.first.source,
contains('[data-pagelet="story_tray"]'),
);
});
});
}
@@ -0,0 +1,73 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:focusgram/focus_settings.dart';
import 'package:focusgram/scripts/focus_scripts.dart';
void main() {
group('FocusSettings — Field cleanup', () {
test('only ghostMode remains (fullDmGhost and storyGhost removed)',
() async {
const settings = FocusSettings(ghostMode: true);
expect(settings.ghostMode, isTrue);
expect(settings.noAds, isTrue);
expect(settings.noStories, isFalse);
expect(settings.noReels, isFalse);
expect(settings.noAutoplay, isFalse);
expect(settings.noDMs, isFalse);
// Verify fullDmGhost and storyGhost are NOT fields anymore
// (these would be compile errors if they existed)
});
test('default ghostMode is false', () async {
const settings = FocusSettings();
expect(settings.ghostMode, isFalse);
});
});
group('buildUserScripts — Ghost mode injection', () {
test('injects kFullDmGhostJS when ghostMode is true', () async {
final scripts = buildUserScripts(const FocusSettings(ghostMode: true));
// Should have exactly 1 DOCUMENT_START script
expect(scripts.length, equals(1));
expect(
scripts[0].injectionTime,
equals(UserScriptInjectionTime.AT_DOCUMENT_START),
);
// The script source should contain the Full DM ghost code
expect(scripts[0].source, contains('__fgFullDmGhost=true'));
expect(scripts[0].source, contains('__fgFullDmGhostPatched'));
});
test('does NOT inject ghost scripts when ghostMode is false', () async {
final scripts =
buildUserScripts(const FocusSettings(ghostMode: false));
// Should have no start scripts (ghostMode is the only start-level script)
// unless other features like noAutoplay are also false
if (scripts.isEmpty) return;
// If scripts exist (e.g. noAutoplay), verify ghost mode NOT in them
for (final s in scripts) {
expect(s.source.contains('__fgFullDmGhost'), isFalse);
}
});
test('injects noAutoplay when set', () async {
final scripts = buildUserScripts(
const FocusSettings(ghostMode: true, noAutoplay: true),
);
// Should have 1 DOCUMENT_START script combining ghost + autoplay
final startScripts =
scripts.where((s) =>
s.injectionTime == UserScriptInjectionTime.AT_DOCUMENT_START);
expect(startScripts.length, equals(1));
expect(startScripts.first.source, contains('__fgFullDmGhost=true'));
expect(startScripts.first.source, contains('document.addEventListener'));
});
});
}
@@ -0,0 +1,78 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:focusgram/services/settings_service.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() {
SharedPreferences.setMockInitialValues({});
});
group('SettingsService — Ghost mode toggle', () {
test('ghostMode defaults to false', () async {
final s = SettingsService();
await s.init();
expect(s.ghostMode, isFalse);
});
test('ghostMode toggle persists and loads on restart', () async {
final s = SettingsService();
await s.init();
await s.setGhostMode(true);
expect(s.ghostMode, isTrue);
// Simulate restart by creating a new instance with saved prefs
final s2 = SettingsService();
await s2.init();
expect(s2.ghostMode, isTrue);
});
test('ghostMode toggles off correctly', () async {
final s = SettingsService();
await s.init();
await s.setGhostMode(true);
expect(s.ghostMode, isTrue);
await s.setGhostMode(false);
expect(s.ghostMode, isFalse);
});
});
group('SettingsService — Grayscale persistence', () {
test('grayscaleEnabled defaults to false', () async {
final s = SettingsService();
await s.init();
expect(s.grayscaleEnabled, isFalse);
});
test('setGrayscaleEnabled persists and isActiveNow returns true', () async {
final s = SettingsService();
await s.init();
await s.setGrayscaleEnabled(true);
expect(s.grayscaleEnabled, isTrue);
expect(s.isGrayscaleActiveNow, isTrue);
// Simulate restart
final s2 = SettingsService();
await s2.init();
expect(s2.grayscaleEnabled, isTrue);
});
test('isGrayscaleActiveNow returns true when toggle is on', () async {
final s = SettingsService();
await s.init();
await s.setGrayscaleEnabled(true);
expect(s.isGrayscaleActiveNow, isTrue);
});
test('isGrayscaleActiveNow returns false when toggle off and no schedules',
() async {
final s = SettingsService();
await s.init();
expect(s.isGrayscaleActiveNow, isFalse);
});
});
}
@@ -0,0 +1,257 @@
import 'package:flutter_test/flutter_test.dart';
void main() {
// The regex patterns used in shouldInterceptRequest for DM Ghost blocking.
// These are the same patterns embedded in main_webview_page.dart.
final seenPattern = RegExp(
r'/api/v1/media/[\w-]+/seen/|'
r'/api/v1/stories/reel/seen/|'
r'/api/v1/direct_v2/threads/[\w-]+/seen/|'
r'/api/v1/direct_v2/visual_message/[\w-]+/seen/|'
r'/api/v1/live/[\w-]+/comment/seen/|'
r'/api/v1/direct_v2/threads/[^/]+/mark_item_seen/|'
r'/api/v1/direct_v2/mark_item_seen/|'
r'/api/v1/direct_v2/threads/[^/]+/items/[^/]+/mark_visual_item_seen/|'
r'/api/v1/direct_v2/visual_thread/[^/]+/seen/|'
r'/api/v1/direct_v2/threads/[^/]+/items/[^/]+/mark_audio_seen/|'
r'/api/v1/live/[^/]+/join/|'
r'/api/v1/live/[^/]+/get_join_requests/|'
r'/api/v1/media/seen/|'
r'/api/v1/feed/viewed_story/|'
r'/api/v1/feed/reels_tray/seen/|'
r'/api/v1/qe/|'
r'/api/v1/launcher/sync/|'
r'/api/v1/logging/|'
r'/api/v1/fb_onetap_logging/|'
r'/ajax/bz|'
r'/ajax/logging/|'
r'/api/v1/stats/|'
r'/api/v1/fbanalytics/',
);
group('DM Ghost — Seen endpoint pattern matching', () {
// ── Story seen endpoints ───────────────────────────────────
test('blocks /api/v1/media/{id}/seen/', () {
expect(
seenPattern.hasMatch(
'https://www.instagram.com/api/v1/media/12345/seen/',
),
isTrue,
);
});
test('blocks /api/v1/stories/reel/seen/', () {
expect(
seenPattern.hasMatch(
'https://www.instagram.com/api/v1/stories/reel/seen/',
),
isTrue,
);
});
test('blocks /api/v1/feed/viewed_story/', () {
expect(
seenPattern.hasMatch(
'https://www.instagram.com/api/v1/feed/viewed_story/',
),
isTrue,
);
});
test('blocks /api/v1/feed/reels_tray/seen/', () {
expect(
seenPattern.hasMatch(
'https://www.instagram.com/api/v1/feed/reels_tray/seen/',
),
isTrue,
);
});
// ── DM read receipts ──────────────────────────────────────
test('blocks /api/v1/direct_v2/threads/{id}/mark_item_seen/', () {
expect(
seenPattern.hasMatch(
'https://www.instagram.com/api/v1/direct_v2/threads/abc123/mark_item_seen/',
),
isTrue,
);
});
test('blocks /api/v1/direct_v2/mark_item_seen/', () {
expect(
seenPattern.hasMatch(
'https://www.instagram.com/api/v1/direct_v2/mark_item_seen/',
),
isTrue,
);
});
test('blocks /api/v1/direct_v2/threads/{id}/seen/', () {
expect(
seenPattern.hasMatch(
'https://www.instagram.com/api/v1/direct_v2/threads/abc123/seen/',
),
isTrue,
);
});
test('blocks /api/v1/direct_v2/visual_message/{id}/seen/', () {
expect(
seenPattern.hasMatch(
'https://www.instagram.com/api/v1/direct_v2/visual_message/xyz/seen/',
),
isTrue,
);
});
// ── Ephemeral / visual seen ───────────────────────────────
test('blocks /api/v1/direct_v2/threads/{id}/items/{id}/mark_visual_item_seen/', () {
expect(
seenPattern.hasMatch(
'https://www.instagram.com/api/v1/direct_v2/threads/abc/items/def/mark_visual_item_seen/',
),
isTrue,
);
});
test('blocks /api/v1/direct_v2/visual_thread/{id}/seen/', () {
expect(
seenPattern.hasMatch(
'https://www.instagram.com/api/v1/direct_v2/visual_thread/abc/seen/',
),
isTrue,
);
});
// ── Audio seen ────────────────────────────────────────────
test('blocks /api/v1/direct_v2/threads/{id}/items/{id}/mark_audio_seen/', () {
expect(
seenPattern.hasMatch(
'https://www.instagram.com/api/v1/direct_v2/threads/abc/items/def/mark_audio_seen/',
),
isTrue,
);
});
// ── Live ──────────────────────────────────────────────────
test('blocks /api/v1/live/{id}/join/', () {
expect(
seenPattern.hasMatch(
'https://www.instagram.com/api/v1/live/abc123/join/',
),
isTrue,
);
});
test('blocks /api/v1/live/{id}/get_join_requests/', () {
expect(
seenPattern.hasMatch(
'https://www.instagram.com/api/v1/live/abc123/get_join_requests/',
),
isTrue,
);
});
test('blocks /api/v1/live/{id}/comment/seen/', () {
expect(
seenPattern.hasMatch(
'https://www.instagram.com/api/v1/live/abc123/comment/seen/',
),
isTrue,
);
});
// ── Analytics / tracking ──────────────────────────────────
test('blocks /api/v1/qe/', () {
expect(
seenPattern.hasMatch('https://www.instagram.com/api/v1/qe/some_param'),
isTrue,
);
});
test('blocks /api/v1/launcher/sync/', () {
expect(
seenPattern.hasMatch(
'https://www.instagram.com/api/v1/launcher/sync/',
),
isTrue,
);
});
test('blocks /api/v1/logging/', () {
expect(
seenPattern.hasMatch(
'https://www.instagram.com/api/v1/logging/event',
),
isTrue,
);
});
test('blocks /api/v1/stats/', () {
expect(
seenPattern.hasMatch('https://www.instagram.com/api/v1/stats/'),
isTrue,
);
});
test('blocks /api/v1/fbanalytics/', () {
expect(
seenPattern.hasMatch(
'https://www.instagram.com/api/v1/fbanalytics/event',
),
isTrue,
);
});
test('blocks /ajax/bz', () {
expect(
seenPattern.hasMatch('https://www.instagram.com/ajax/bz'),
isTrue,
);
});
test('blocks /ajax/logging/', () {
expect(
seenPattern.hasMatch('https://www.instagram.com/ajax/logging/'),
isTrue,
);
});
// ── Should NOT block legitimate endpoints ─────────────────
test('does NOT block normal feed timeline request', () {
expect(
seenPattern.hasMatch(
'https://www.instagram.com/api/v1/feed/timeline/',
),
isFalse,
);
});
test('does NOT block graphql queries', () {
expect(
seenPattern.hasMatch(
'https://www.instagram.com/api/graphql',
),
isFalse,
);
});
test('does NOT block direct_v2 inbox', () {
expect(
seenPattern.hasMatch(
'https://www.instagram.com/api/v1/direct_v2/inbox/',
),
isFalse,
);
});
test('does NOT block user posts', () {
expect(
seenPattern.hasMatch(
'https://www.instagram.com/api/v1/users/12345/posts/',
),
isFalse,
);
});
});
}
+86
View File
@@ -0,0 +1,86 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:focusgram/services/level_service.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() async {
SharedPreferences.setMockInitialValues({});
// Ensure Hive is available for LevelService
if (!Hive.isAdapterRegistered(0)) {
await Hive.initFlutter();
}
});
group('AppFeature — Your Journey unlock table', () {
test('fullDmGhost is NOT in the all list', () async {
// fullDmGhost should still be defined as a constant
expect(AppFeature.fullDmGhost, isNotNull);
// But should NOT appear in the unlock table shown to users
final contains = AppFeature.all.any(
(f) => f.id == 'full_dm_ghost',
);
expect(contains, isFalse);
});
test('storyGhost and reelsHistory are NOT in the all list', () async {
final hasStory = AppFeature.all.any((f) => f.id == 'custom_friction');
final hasReels = AppFeature.all.any((f) => f.id == 'reels_history');
expect(hasStory, isFalse);
expect(hasReels, isFalse);
});
test('all list contains only active features', () async {
final ids = AppFeature.all.map((f) => f.id).toSet();
expect(ids, contains('ghost_mode'));
expect(ids, contains('effort_friction'));
expect(ids, contains('download_media'));
expect(ids, contains('bait_me'));
expect(ids, contains('app_lock'));
expect(ids.length, equals(5));
});
});
group('LevelService — No Firestore dependency', () {
test('init succeeds without Firestore (uses Hive only)', () async {
// This would crash if init tried to reach Firestore
// Since we removed Firebase, it should work with just Hive cache
final levelService = LevelService();
// Should not throw — even if no Firestore is available
await expectLater(
() => levelService.init(),
returnsNormally,
);
// Default state
expect(levelService.level, equals(1));
expect(levelService.xp, equals(0));
expect(levelService.synced, isFalse);
});
test('addXpForAd awards XP without Firestore', () async {
final levelService = LevelService();
await levelService.init();
await levelService.addXpForAd();
expect(levelService.xp, greaterThan(0));
expect(levelService.adsWatchedTotal, equals(1));
});
test('debugSetLevel works with Hive-only storage', () async {
final levelService = LevelService();
await levelService.init();
await levelService.debugSetLevel(3, 300);
expect(levelService.level, equals(3));
expect(levelService.xp, equals(300));
});
});
}
+74
View File
@@ -0,0 +1,74 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:focusgram/services/session_manager.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() {
SharedPreferences.setMockInitialValues({});
});
group('SessionManager — Extension flow', () {
test('canExtendAppSession is true when session just ended', () async {
final sm = SessionManager();
await sm.init();
// Start an app session
sm.startAppSession(60);
expect(sm.isSessionActive, isTrue);
// End it
sm.endAppSession();
expect(sm.isAppSessionExpired, isTrue);
expect(sm.canExtendAppSession, isTrue);
});
test('extendAppSession sets canExtendAppSession to false', () async {
final sm = SessionManager();
await sm.init();
sm.startAppSession(60);
sm.endAppSession();
expect(sm.canExtendAppSession, isTrue);
sm.extendAppSession();
expect(sm.canExtendAppSession, isFalse);
expect(sm.isSessionActive, isTrue);
});
test('canExtendAppSession is false after re-ending an extended session',
() async {
final sm = SessionManager();
await sm.init();
sm.startAppSession(60);
sm.endAppSession();
sm.extendAppSession();
sm.endAppSession();
expect(sm.canExtendAppSession, isFalse);
});
});
group('SessionManager — App session lifecycle', () {
test('startAppSession sets isSessionActive', () async {
final sm = SessionManager();
await sm.init();
sm.startAppSession(30);
expect(sm.isSessionActive, isTrue);
});
test('endAppSession clears session and sets expired', () async {
final sm = SessionManager();
await sm.init();
sm.startAppSession(30);
sm.endAppSession();
expect(sm.isSessionActive, isFalse);
expect(sm.isAppSessionExpired, isTrue);
});
});
}