blob: 37c8eef9c9224f7a05e9e41c5c6c73dbe32c93fb [file] [edit]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// ignore_for_file: invalid_use_of_visible_for_testing_member, devtools_test is only used in test code.
import 'dart:async';
import 'package:devtools_app_shared/service_extensions.dart';
// ignore: implementation_imports, intentional import from src/
import 'package:devtools_app_shared/src/service/service_extension_manager.dart';
import 'package:flutter/foundation.dart';
import 'package:mockito/mockito.dart';
// ignore: subtype_of_sealed_class, fake for testing.
/// Fake that simplifies writing UI tests that depend on the
/// ServiceExtensionManager.
// TODO(jacobr): refactor ServiceExtensionManager so this fake can reuse more
// code from ServiceExtensionManager instead of reimplementing it.
base class FakeServiceExtensionManager extends Fake
with TestServiceExtensionManager {
bool _firstFrameEventReceived = false;
final _serviceExtensionStateController =
<String, ValueNotifier<ServiceExtensionState>>{};
final _serviceExtensionAvailable = <String, ValueNotifier<bool>>{};
/// All available service extensions.
final _serviceExtensions = <String>{};
/// All service extensions that are currently enabled.
final _enabledServiceExtensions = <String, ServiceExtensionState>{};
/// Temporarily stores service extensions that we need to add. We should not
/// add extensions until the first frame event has been received
/// [_firstFrameEventReceived].
final _pendingServiceExtensions = <String>{};
/// Hook to simulate receiving the first frame event.
///
/// Service extensions are only reported once a frame has been received.
Future<void> fakeFrame() async {
await _onFrameEventReceived();
}
Map<String, dynamic> extensionValueOnDevice = {};
@override
ValueListenable<bool> hasServiceExtension(String name) {
return _hasServiceExtension(name);
}
ValueNotifier<bool> _hasServiceExtension(String name) {
return _serviceExtensionAvailable.putIfAbsent(
name,
() => ValueNotifier(_hasServiceExtensionNow(name)),
);
}
bool _hasServiceExtensionNow(String name) {
return _serviceExtensions.contains(name);
}
@override
Future<bool> waitForServiceExtensionAvailable(String name) {
return Future.value(true);
}
/// Hook for tests to call to simulate adding a service extension.
Future<void> fakeAddServiceExtension(String name) async {
if (_firstFrameEventReceived) {
assert(_pendingServiceExtensions.isEmpty);
await _addServiceExtension(name);
} else {
_pendingServiceExtensions.add(name);
}
}
/// Hook for tests to call to fake changing the state of a service
/// extension.
void fakeServiceExtensionStateChanged(
final String name,
String valueFromJson,
) async {
final extension = serviceExtensionsAllowlist[name];
if (extension != null) {
final value = _getExtensionValueFromJson(name, valueFromJson);
final enabled = extension is ToggleableServiceExtension
? value == extension.enabledValue
// For extensions that have more than two states
// (enabled / disabled), we will always consider them to be
// enabled with the current value.
: true;
await setServiceExtensionState(
name,
enabled: enabled,
value: value,
callExtension: false,
);
}
}
Object? _getExtensionValueFromJson(String name, String valueFromJson) {
return switch (serviceExtensionsAllowlist[name]!.values.first) {
bool() => valueFromJson == 'true' ? true : false,
num() => num.parse(valueFromJson),
_ => valueFromJson,
};
}
Future<void> _onFrameEventReceived() async {
if (_firstFrameEventReceived) {
// The first frame event was already received.
return;
}
_firstFrameEventReceived = true;
for (final extension in _pendingServiceExtensions) {
await _addServiceExtension(extension);
}
_pendingServiceExtensions.clear();
}
Future<void> _addServiceExtension(String name) {
_hasServiceExtension(name).value = true;
_serviceExtensions.add(name);
if (_enabledServiceExtensions.containsKey(name)) {
// Restore any previously enabled states by calling their service
// extension. This will restore extension states on the device after a hot
// restart. [_enabledServiceExtensions] will be empty on page refresh or
// initial start.
return callServiceExtension(name, _enabledServiceExtensions[name]!.value);
} else {
// Set any extensions that are already enabled on the device. This will
// enable extension states in DevTools on page refresh or initial start.
return _restoreExtensionFromDevice(name);
}
}
@override
ValueListenable<ServiceExtensionState> getServiceExtensionState(String name) {
return _serviceExtensionState(name);
}
ValueNotifier<ServiceExtensionState> _serviceExtensionState(String name) {
return _serviceExtensionStateController.putIfAbsent(
name,
() {
return ValueNotifier<ServiceExtensionState>(
_enabledServiceExtensions.containsKey(name)
? _enabledServiceExtensions[name]!
: ServiceExtensionState(enabled: false, value: null),
);
},
);
}
Future<void> _restoreExtensionFromDevice(String name) async {
if (!serviceExtensionsAllowlist.containsKey(name)) {
return;
}
final extensionDescription = serviceExtensionsAllowlist[name];
final value = extensionValueOnDevice[name];
if (extensionDescription is ToggleableServiceExtension) {
await setServiceExtensionState(
name,
enabled: value == extensionDescription.enabledValue,
value: value,
callExtension: false,
);
} else {
await setServiceExtensionState(
name,
enabled: true,
value: value,
callExtension: false,
);
}
}
Future<void> callServiceExtension(String name, Object? value) {
extensionValueOnDevice[name] = value;
return Future.value();
}
@override
void vmServiceClosed() {
_firstFrameEventReceived = false;
_pendingServiceExtensions.clear();
_serviceExtensions.clear();
for (final listenable in _serviceExtensionAvailable.values) {
listenable.value = false;
}
}
/// Sets the state for a service extension and makes the call to the VMService.
@override
Future<void> setServiceExtensionState(
String name, {
required bool enabled,
required Object? value,
bool callExtension = true,
}) async {
if (callExtension && _serviceExtensions.contains(name)) {
await callServiceExtension(name, value);
}
_serviceExtensionState(name).value = ServiceExtensionState(
enabled: enabled,
value: value,
);
// Add or remove service extension from [enabledServiceExtensions].
if (enabled) {
_enabledServiceExtensions[name] = ServiceExtensionState(
enabled: enabled,
value: value,
);
} else {
_enabledServiceExtensions.remove(name);
}
}
@override
bool isServiceExtensionAvailable(String name) {
return _serviceExtensions.contains(name) ||
_pendingServiceExtensions.contains(name);
}
}