mirror of
https://github.com/Ujwal223/FocusGram.git
synced 2026-07-02 09:35:31 +02:00
Feature Pack with bug fixes for V2
This commit is contained in:
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user