правки по лаунчеру

This commit is contained in:
2026-07-28 20:43:53 +04:00
parent 155922a3f1
commit 0b6f7fc647
51 changed files with 3158 additions and 437 deletions
@@ -1,15 +1,18 @@
import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:moonwell_launcher/core/moonwell_theme_variant.dart';
import 'package:moonwell_launcher/app/home_screen/bloc/home_screen_bloc.dart';
import 'package:moonwell_launcher/core/moonwell_theme_variant.dart';
import 'package:moonwell_launcher/features/downloader/domain/entities/download_progress.dart';
import 'package:moonwell_launcher/features/launcher/application/client_sync_use_case.dart';
import 'package:moonwell_launcher/features/launcher/data/game_installation_service.dart';
import 'package:moonwell_launcher/features/launcher/data/launcher_api_client.dart';
import 'package:moonwell_launcher/features/launcher/domain/entities/client_installation_snapshot.dart';
import 'package:moonwell_launcher/features/launcher/domain/entities/client_manifest.dart';
import 'package:moonwell_launcher/features/launcher/domain/entities/client_sync_status.dart';
import 'package:moonwell_launcher/features/launcher/domain/entities/launcher_account.dart';
import 'package:moonwell_launcher/features/launcher/domain/entities/launcher_news_item.dart';
import 'package:moonwell_launcher/features/launcher/domain/entities/launcher_realm.dart';
import 'package:moonwell_launcher/features/launcher/domain/entities/launcher_session.dart';
import 'package:moonwell_launcher/features/preferences/domain/repositories/preferences_repository.dart';
@@ -29,6 +32,19 @@ void main() {
createdAt: null,
),
],
realms: const [
LauncherRealm(
id: 1,
name: 'MoonWell',
address: 'logon.moon-well.online',
port: 8085,
population: 0.5,
gameBuild: 12340,
online: true,
status: 'online',
),
],
account: const LauncherAccount(username: 'PLAYERONE'),
),
preferencesRepository: _FakePreferencesRepository(),
session: LauncherSession(
@@ -47,12 +63,18 @@ void main() {
expect(bloc.state.model.newsErrorMessage, isNull);
expect(bloc.state.model.newsItems, hasLength(1));
expect(bloc.state.model.newsItems.first.title, 'Update 1.2');
expect(bloc.state.model.realm?.name, 'MoonWell');
expect(bloc.state.model.realm?.online, isTrue);
expect(bloc.state.model.accountUsername, 'PLAYERONE');
await bloc.close();
});
test('fetches a fresh manifest for every client sync', () async {
test('starts sync on startup when output directory is saved', () async {
final syncUseCase = _CapturingClientSyncUseCase();
final manifest = ClientManifest.fromJson(<String, Object?>{
'files': <Map<String, Object?>>[],
});
final bloc = HomeScreenBloc(
clientSyncUseCase: syncUseCase,
gameInstallationService: _FakeGameInstallationService(),
@@ -65,17 +87,99 @@ void main() {
tokenType: 'Bearer',
expiresAt: null,
),
manifest: ClientManifest.fromJson(<String, Object?>{
'files': <Map<String, Object?>>[],
}),
manifest: manifest,
);
await pumpEventQueue(times: 20);
bloc.add(HomeScreenSyncRequested());
expect(syncUseCase.inputs, hasLength(1));
expect(syncUseCase.inputs.single.request.manifest, same(manifest));
await bloc.close();
});
test('refreshes manifest and syncs when build changes', () async {
final startupManifest = ClientManifest.fromJson(<String, Object?>{
'files': <Map<String, Object?>>[],
});
final updatedManifest = ClientManifest.fromJson(<String, Object?>{
'files': <Map<String, Object?>>[
<String, Object?>{
'path': 'Wow.exe',
'size': 42,
'sha256':
'0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
},
],
});
final syncUseCase = _CapturingClientSyncUseCase();
final apiClient = _FakeLauncherApiClient(
newsItems: const [],
manifest: updatedManifest,
);
final bloc = HomeScreenBloc(
clientSyncUseCase: syncUseCase,
gameInstallationService: _FakeGameInstallationService(),
launcherApiClient: apiClient,
preferencesRepository: _FakePreferencesRepository(
outputDir: Uri.directory('C:/World of Warcraft'),
),
session: LauncherSession(
accessToken: 'token',
tokenType: 'Bearer',
expiresAt: null,
),
manifest: startupManifest,
);
await pumpEventQueue(times: 20);
bloc.add(HomeScreenManifestRefreshRequested());
await pumpEventQueue(times: 20);
expect(syncUseCase.lastInput, isNotNull);
expect(syncUseCase.lastInput!.request.manifest, isNull);
expect(apiClient.manifestRequestCount, 1);
expect(syncUseCase.inputs, hasLength(2));
expect(syncUseCase.inputs.last.request.manifest, same(updatedManifest));
await bloc.close();
});
test('returns to ready state after launched game exits', () async {
final manifest = ClientManifest.fromJson(<String, Object?>{
'files': <Map<String, Object?>>[],
});
final gameService = _TrackingGameInstallationService();
final bloc = HomeScreenBloc(
clientSyncUseCase: _CapturingClientSyncUseCase(),
gameInstallationService: gameService,
launcherApiClient: _FakeLauncherApiClient(newsItems: const []),
preferencesRepository: _FakePreferencesRepository(
outputDir: Uri.directory('C:/World of Warcraft'),
),
session: LauncherSession(
accessToken: 'token',
tokenType: 'Bearer',
expiresAt: null,
),
manifest: manifest,
);
await pumpEventQueue(times: 20);
bloc.add(HomeScreenPlayRequested());
await pumpEventQueue(times: 20);
expect(gameService.launchCount, 1);
expect(bloc.state.model.isGameRunning, isTrue);
expect(bloc.state.model.canPlay, isFalse);
gameService.exitCode.complete(0);
await pumpEventQueue(times: 20);
expect(bloc.state.model.isGameRunning, isFalse);
expect(bloc.state.model.canPlay, isTrue);
expect(
bloc.state.model.statusText,
'Игра закрыта. Клиент готов к запуску.',
);
await bloc.close();
});
@@ -96,23 +200,59 @@ class _IdleClientSyncUseCase extends ClientSyncUseCase {
}
class _CapturingClientSyncUseCase extends _IdleClientSyncUseCase {
ClientSyncUseCaseInput? lastInput;
final List<ClientSyncUseCaseInput> inputs = [];
@override
Stream<ClientSyncStatus> call(ClientSyncUseCaseInput input) {
lastInput = input;
return const Stream<ClientSyncStatus>.empty();
inputs.add(input);
final manifest = input.request.manifest;
return Stream<ClientSyncStatus>.value(
ClientSyncStatus(
stage: ClientSyncStage.completed,
progress: const DownloadProgress.initial(),
message: 'Клиент обновлён.',
currentPath: null,
localBuildHash: manifest?.buildHash,
remoteBuildHash: manifest?.buildHash,
processedFiles: manifest?.files.length ?? 0,
totalFiles: manifest?.files.length ?? 0,
),
);
}
}
class _FakeLauncherApiClient extends LauncherApiClient {
_FakeLauncherApiClient({required this.newsItems});
_FakeLauncherApiClient({
required this.newsItems,
this.manifest,
this.realms = const [],
this.account = const LauncherAccount(username: ''),
});
final List<LauncherNewsItem> newsItems;
final ClientManifest? manifest;
final List<LauncherRealm> realms;
final LauncherAccount account;
int manifestRequestCount = 0;
@override
Future<List<LauncherNewsItem>> fetchNews(String accessToken) async =>
newsItems;
@override
Future<List<LauncherRealm>> fetchRealms() async => realms;
@override
Future<LauncherAccount> fetchAccount(String accessToken) async => account;
@override
Future<ClientManifest> fetchManifest(String accessToken) async {
manifestRequestCount += 1;
return manifest ??
ClientManifest.fromJson(<String, Object?>{
'files': <Map<String, Object?>>[],
});
}
}
class _FakeGameInstallationService extends GameInstallationService {
@@ -129,6 +269,23 @@ class _FakeGameInstallationService extends GameInstallationService {
}
}
class _TrackingGameInstallationService extends _FakeGameInstallationService {
final Completer<int> exitCode = Completer<int>();
int launchCount = 0;
@override
Future<bool> hasClientExecutable(String installationDir) async => true;
@override
Future<void> clearCache(String installationDir) async {}
@override
Future<GameProcessHandle> launchGame(String installationDir) async {
launchCount += 1;
return GameProcessHandle(pid: 42, exitCode: exitCode.future);
}
}
class _FakePreferencesRepository implements PreferencesRepository {
_FakePreferencesRepository({this.outputDir});