mirror of
https://github.com/jonasbark/swiftcontrol.git
synced 2026-02-18 00:17:40 +01:00
Merge branch 'copilot/add-screenshots-to-release'
This commit is contained in:
26
.github/workflows/build.yml
vendored
26
.github/workflows/build.yml
vendored
@@ -158,6 +158,20 @@ jobs:
|
||||
chmod +x scripts/generate_release_body.sh
|
||||
./scripts/generate_release_body.sh > /tmp/release_body.md
|
||||
|
||||
- name: Set Up Flutter for Screenshots
|
||||
if: inputs.build_github
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
|
||||
- name: Generate screenshots for App Stores
|
||||
if: inputs.build_github
|
||||
run: |
|
||||
flutter test test/screenshot_test.dart
|
||||
zip -r SwiftControl.storeassets.zip screenshots
|
||||
echo "Screenshots generated successfully"
|
||||
|
||||
- name: 🚀 Shorebird Release iOS
|
||||
if: inputs.build_ios
|
||||
uses: shorebirdtech/shorebird-release@v1
|
||||
@@ -238,6 +252,16 @@ jobs:
|
||||
path: |
|
||||
build/macos/Build/Products/Release/SwiftControl.macos.zip
|
||||
|
||||
- name: Upload Screenshots Artifacts
|
||||
if: inputs.build_github
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
overwrite: true
|
||||
name: Releases
|
||||
path: |
|
||||
screenshots/device-GitHub-600x900.png
|
||||
build/SwiftControl.storeassets.zip
|
||||
|
||||
#10 Extract Version
|
||||
- name: Extract version from pubspec.yaml
|
||||
if: inputs.build_github
|
||||
@@ -251,7 +275,7 @@ jobs:
|
||||
if: inputs.build_github
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
artifacts: "build/app/outputs/flutter-apk/SwiftControl.android.apk,build/macos/Build/Products/Release/SwiftControl.macos.zip"
|
||||
artifacts: "build/app/outputs/flutter-apk/SwiftControl.android.apk,build/macos/Build/Products/Release/SwiftControl.macos.zip,build/SwiftControl.screenshots.zip"
|
||||
allowUpdates: true
|
||||
prerelease: true
|
||||
bodyFile: /tmp/release_body.md
|
||||
|
||||
@@ -11,6 +11,8 @@ PODS:
|
||||
- Flutter
|
||||
- image_picker_ios (0.0.1):
|
||||
- Flutter
|
||||
- integration_test (0.0.1):
|
||||
- Flutter
|
||||
- media_key_detector_ios (0.0.1):
|
||||
- Flutter
|
||||
- package_info_plus (0.4.5):
|
||||
@@ -40,6 +42,7 @@ DEPENDENCIES:
|
||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||
- gamepads_ios (from `.symlinks/plugins/gamepads_ios/ios`)
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||
- media_key_detector_ios (from `.symlinks/plugins/media_key_detector_ios/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
@@ -63,6 +66,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/gamepads_ios/ios"
|
||||
image_picker_ios:
|
||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||
integration_test:
|
||||
:path: ".symlinks/plugins/integration_test/ios"
|
||||
media_key_detector_ios:
|
||||
:path: ".symlinks/plugins/media_key_detector_ios/ios"
|
||||
package_info_plus:
|
||||
@@ -89,6 +94,7 @@ SPEC CHECKSUMS:
|
||||
flutter_local_notifications: ff50f8405aaa0ccdc7dcfb9022ca192e8ad9688f
|
||||
gamepads_ios: 1d2930c7a4450a9a1b57444ebf305a6a6cbeea0b
|
||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
|
||||
media_key_detector_ios: 7ff9aefdfea00bb7b71e184132381b7d0e7e1269
|
||||
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
||||
path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba
|
||||
|
||||
@@ -245,7 +245,7 @@ class Connection {
|
||||
|
||||
void _handleConnectionQueue() {
|
||||
// windows apparently has issues when connecting to multiple devices at once, so don't
|
||||
if (_connectionQueue.isNotEmpty && !_handlingConnectionQueue) {
|
||||
if (_connectionQueue.isNotEmpty && !_handlingConnectionQueue && !screenshotMode) {
|
||||
_handlingConnectionQueue = true;
|
||||
final device = _connectionQueue.removeAt(0);
|
||||
_actionStreams.add(LogNotification('Connecting to: ${device.name}'));
|
||||
|
||||
@@ -36,7 +36,7 @@ class ZwiftEmulator {
|
||||
bool get isAdvertising => _isAdvertising;
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
final peripheralManager = PeripheralManager();
|
||||
late final _peripheralManager = PeripheralManager();
|
||||
bool _isAdvertising = false;
|
||||
bool _isLoading = false;
|
||||
bool _isServiceAdded = false;
|
||||
@@ -45,8 +45,8 @@ class ZwiftEmulator {
|
||||
GATTCharacteristic? _asyncCharacteristic;
|
||||
|
||||
Future<void> reconnect() async {
|
||||
await peripheralManager.stopAdvertising();
|
||||
await peripheralManager.removeAllServices();
|
||||
await _peripheralManager.stopAdvertising();
|
||||
await _peripheralManager.removeAllServices();
|
||||
_isServiceAdded = false;
|
||||
_isAdvertising = false;
|
||||
startAdvertising(() {});
|
||||
@@ -56,13 +56,13 @@ class ZwiftEmulator {
|
||||
_isLoading = true;
|
||||
onUpdate();
|
||||
|
||||
peripheralManager.stateChanged.forEach((state) {
|
||||
_peripheralManager.stateChanged.forEach((state) {
|
||||
print('Peripheral manager state: ${state.state}');
|
||||
});
|
||||
|
||||
if (!kIsWeb && Platform.isAndroid) {
|
||||
if (Platform.isAndroid) {
|
||||
peripheralManager.connectionStateChanged.forEach((state) {
|
||||
_peripheralManager.connectionStateChanged.forEach((state) {
|
||||
print('Peripheral connection state: ${state.state} of ${state.central.uuid}');
|
||||
if (state.state == ConnectionState.connected) {
|
||||
} else if (state.state == ConnectionState.disconnected) {
|
||||
@@ -82,7 +82,7 @@ class ZwiftEmulator {
|
||||
}
|
||||
}
|
||||
|
||||
while (peripheralManager.state != BluetoothLowEnergyState.poweredOn) {
|
||||
while (_peripheralManager.state != BluetoothLowEnergyState.poweredOn) {
|
||||
print('Waiting for peripheral manager to be powered on...');
|
||||
if (settings.getLastTarget() == Target.thisDevice) {
|
||||
return;
|
||||
@@ -116,7 +116,7 @@ class ZwiftEmulator {
|
||||
|
||||
if (!_isSubscribedToEvents) {
|
||||
_isSubscribedToEvents = true;
|
||||
peripheralManager.characteristicReadRequested.forEach((eventArgs) async {
|
||||
_peripheralManager.characteristicReadRequested.forEach((eventArgs) async {
|
||||
print('Read request for characteristic: ${eventArgs.characteristic.uuid}');
|
||||
|
||||
switch (eventArgs.characteristic.uuid.toString().toUpperCase()) {
|
||||
@@ -124,7 +124,7 @@ class ZwiftEmulator {
|
||||
print('Handling read request for SYNC TX characteristic');
|
||||
break;
|
||||
case BleUuid.DEVICE_INFORMATION_CHARACTERISTIC_BATTERY_LEVEL:
|
||||
await peripheralManager.respondReadRequestWithValue(
|
||||
await _peripheralManager.respondReadRequestWithValue(
|
||||
eventArgs.request,
|
||||
value: Uint8List.fromList([100]),
|
||||
);
|
||||
@@ -135,19 +135,19 @@ class ZwiftEmulator {
|
||||
|
||||
final request = eventArgs.request;
|
||||
final trimmedValue = Uint8List.fromList([]);
|
||||
await peripheralManager.respondReadRequestWithValue(
|
||||
await _peripheralManager.respondReadRequestWithValue(
|
||||
request,
|
||||
value: trimmedValue,
|
||||
);
|
||||
// You can respond to read requests here if needed
|
||||
});
|
||||
|
||||
peripheralManager.characteristicNotifyStateChanged.forEach((char) {
|
||||
_peripheralManager.characteristicNotifyStateChanged.forEach((char) {
|
||||
print(
|
||||
'Notify state changed for characteristic: ${char.characteristic.uuid}: ${char.state}',
|
||||
);
|
||||
});
|
||||
peripheralManager.characteristicWriteRequested.forEach((eventArgs) async {
|
||||
_peripheralManager.characteristicWriteRequested.forEach((eventArgs) async {
|
||||
_central = eventArgs.central;
|
||||
isConnected.value = true;
|
||||
|
||||
@@ -169,7 +169,7 @@ class ZwiftEmulator {
|
||||
|
||||
if (value.contentEquals(handshake) || value.contentEquals(handshakeAlternative)) {
|
||||
print('Sending handshake');
|
||||
await peripheralManager.notifyCharacteristic(
|
||||
await _peripheralManager.notifyCharacteristic(
|
||||
_central!,
|
||||
syncTxCharacteristic,
|
||||
value: ZwiftConstants.RIDE_ON,
|
||||
@@ -181,12 +181,12 @@ class ZwiftEmulator {
|
||||
print('Unhandled write request for characteristic: ${eventArgs.characteristic.uuid}');
|
||||
}
|
||||
|
||||
await peripheralManager.respondWriteRequest(request);
|
||||
await _peripheralManager.respondWriteRequest(request);
|
||||
});
|
||||
}
|
||||
|
||||
// Device Information
|
||||
await peripheralManager.addService(
|
||||
await _peripheralManager.addService(
|
||||
GATTService(
|
||||
uuid: UUID.fromString('180A'),
|
||||
isPrimary: true,
|
||||
@@ -217,7 +217,7 @@ class ZwiftEmulator {
|
||||
);
|
||||
|
||||
// Battery Service
|
||||
await peripheralManager.addService(
|
||||
await _peripheralManager.addService(
|
||||
GATTService(
|
||||
uuid: UUID.fromString('180F'),
|
||||
isPrimary: true,
|
||||
@@ -239,7 +239,7 @@ class ZwiftEmulator {
|
||||
);
|
||||
|
||||
// Unknown Service
|
||||
await peripheralManager.addService(
|
||||
await _peripheralManager.addService(
|
||||
GATTService(
|
||||
uuid: UUID.fromString(ZwiftConstants.ZWIFT_RIDE_CUSTOM_SERVICE_UUID_SHORT),
|
||||
isPrimary: true,
|
||||
@@ -298,14 +298,14 @@ class ZwiftEmulator {
|
||||
);
|
||||
print('Starting advertising with Zwift service...');
|
||||
|
||||
await peripheralManager.startAdvertising(advertisement);
|
||||
await _peripheralManager.startAdvertising(advertisement);
|
||||
_isAdvertising = true;
|
||||
_isLoading = false;
|
||||
onUpdate();
|
||||
}
|
||||
|
||||
Future<void> stopAdvertising() async {
|
||||
await peripheralManager.stopAdvertising();
|
||||
await _peripheralManager.stopAdvertising();
|
||||
_isAdvertising = false;
|
||||
_isLoading = false;
|
||||
}
|
||||
@@ -340,10 +340,10 @@ class ZwiftEmulator {
|
||||
...bytes,
|
||||
]);
|
||||
|
||||
peripheralManager.notifyCharacteristic(_central!, _asyncCharacteristic!, value: commandProto);
|
||||
_peripheralManager.notifyCharacteristic(_central!, _asyncCharacteristic!, value: commandProto);
|
||||
|
||||
final zero = Uint8List.fromList([0x23, 0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F]);
|
||||
peripheralManager.notifyCharacteristic(_central!, _asyncCharacteristic!, value: zero);
|
||||
_peripheralManager.notifyCharacteristic(_central!, _asyncCharacteristic!, value: zero);
|
||||
return 'Sent action: ${inGameAction.name}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ late BaseActions actionHandler;
|
||||
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
||||
final settings = Settings();
|
||||
final whooshLink = WhooshLink();
|
||||
const screenshotMode = false;
|
||||
var screenshotMode = false;
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
@@ -56,7 +56,9 @@ class _DevicePageState extends State<DevicePage> with WidgetsBindingObserver {
|
||||
super.initState();
|
||||
|
||||
// keep screen on - this is required for iOS to keep the bluetooth connection alive
|
||||
WakelockPlus.enable();
|
||||
if (!screenshotMode) {
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
@@ -402,7 +404,7 @@ class _DevicePageState extends State<DevicePage> with WidgetsBindingObserver {
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Text(
|
||||
'Customize ${settings.getTrainerApp()?.name} on ${settings.getLastTarget()?.title}',
|
||||
'Customize ${screenshotMode ? 'Trainer app' : settings.getTrainerApp()?.name} on ${settings.getLastTarget()?.title}',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
@@ -430,7 +432,7 @@ class _DevicePageState extends State<DevicePage> with WidgetsBindingObserver {
|
||||
..._getAllApps().map(
|
||||
(app) => DropdownMenuEntry<SupportedApp>(
|
||||
value: app,
|
||||
label: app.name,
|
||||
label: screenshotMode ? 'Trainer app' : app.name,
|
||||
labelWidget: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
|
||||
@@ -454,7 +454,7 @@ class _KeyWidget extends StatelessWidget {
|
||||
child: Text(
|
||||
label.splitByUpperCase(),
|
||||
style: TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontFamily: screenshotMode ? null : 'monospace',
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
|
||||
@@ -66,7 +66,9 @@ class BluetoothTurnedOn extends PlatformRequirement {
|
||||
|
||||
@override
|
||||
Future<void> getStatus() async {
|
||||
final currentState = await UniversalBle.getBluetoothAvailabilityState();
|
||||
final currentState = screenshotMode
|
||||
? AvailabilityState.poweredOn
|
||||
: await UniversalBle.getBluetoothAvailabilityState();
|
||||
status = currentState == AvailabilityState.poweredOn || screenshotMode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/utils/keymap/buttons.dart';
|
||||
import 'package:swift_control/widgets/keymap_explanation.dart';
|
||||
|
||||
@@ -32,7 +33,7 @@ class ButtonWidget extends StatelessWidget {
|
||||
: Text(
|
||||
button.name.splitByUpperCase(),
|
||||
style: TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontFamily: screenshotMode ? null : 'monospace',
|
||||
fontSize: big && button.color != null ? 20 : 12,
|
||||
fontWeight: button.color != null ? FontWeight.bold : null,
|
||||
color: button.color != null ? Colors.white : Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_md/flutter_md.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
|
||||
class ChangelogDialog extends StatelessWidget {
|
||||
final Markdown entry;
|
||||
@@ -41,7 +42,7 @@ class ChangelogDialog extends StatelessWidget {
|
||||
|
||||
static Future<void> showIfNeeded(BuildContext context, String currentVersion, String? lastSeenVersion) async {
|
||||
// Show dialog if this is a new version
|
||||
if (lastSeenVersion != currentVersion) {
|
||||
if (lastSeenVersion != currentVersion && !screenshotMode) {
|
||||
try {
|
||||
final entry = await rootBundle.loadString('CHANGELOG.md');
|
||||
if (context.mounted) {
|
||||
|
||||
@@ -50,7 +50,9 @@ class _AppTitleState extends State<AppTitle> {
|
||||
}
|
||||
|
||||
void _checkForUpdate() async {
|
||||
if (updater.isAvailable) {
|
||||
if (screenshotMode) {
|
||||
return;
|
||||
} else if (updater.isAvailable) {
|
||||
final updateStatus = await updater.checkForUpdate();
|
||||
if (updateStatus == UpdateStatus.outdated) {
|
||||
updater
|
||||
@@ -146,7 +148,9 @@ class _AppTitleState extends State<AppTitle> {
|
||||
if (packageInfoValue != null)
|
||||
Text(
|
||||
'v${packageInfoValue!.version}${shorebirdPatch != null ? '+${shorebirdPatch!.number}' : ''}${kIsWeb || (Platform.isAndroid && isFromPlayStore == false) ? ' (sideloaded)' : ''}',
|
||||
style: TextStyle(fontFamily: "monospace", fontFamilyFallback: <String>["Courier"], fontSize: 12),
|
||||
style: screenshotMode
|
||||
? TextStyle(fontSize: 12)
|
||||
: TextStyle(fontFamily: "monospace", fontFamilyFallback: <String>["Courier"], fontSize: 12),
|
||||
)
|
||||
else
|
||||
SmallProgressIndicator(),
|
||||
|
||||
47
pubspec.lock
47
pubspec.lock
@@ -278,6 +278,11 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_driver:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -352,6 +357,11 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
fuchsia_remote_debug_protocol:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
gamepads:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -520,6 +530,11 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.5"
|
||||
integration_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -846,6 +861,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.3"
|
||||
process:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: process
|
||||
sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.5"
|
||||
protobuf:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1012,6 +1035,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
sync_http:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sync_http
|
||||
sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1028,6 +1059,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.6"
|
||||
test_screenshot:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: test_screenshot
|
||||
sha256: "2a7620f404cf514601b5181a154c7af7495015e51c52e0175c397ac579371b4a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.8"
|
||||
time:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1181,6 +1220,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
webdriver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webdriver
|
||||
sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -57,7 +57,10 @@ dependencies:
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
integration_test:
|
||||
sdk: flutter
|
||||
|
||||
test_screenshot: 0.0.8
|
||||
flutter_lints: ^6.0.0
|
||||
msix: ^3.16.12
|
||||
|
||||
|
||||
124
test/bluetooth_device_detection.dart
Normal file
124
test/bluetooth_device_detection.dart
Normal file
@@ -0,0 +1,124 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:swift_control/bluetooth/devices/bluetooth_device.dart';
|
||||
import 'package:swift_control/bluetooth/devices/cycplus/cycplus_bc2.dart';
|
||||
import 'package:swift_control/bluetooth/devices/elite/elite_square.dart';
|
||||
import 'package:swift_control/bluetooth/devices/elite/elite_sterzo.dart';
|
||||
import 'package:swift_control/bluetooth/devices/shimano/shimano_di2.dart';
|
||||
import 'package:swift_control/bluetooth/devices/wahoo/wahoo_kickr_bike_shift.dart';
|
||||
import 'package:swift_control/bluetooth/devices/zwift/constants.dart';
|
||||
import 'package:swift_control/bluetooth/devices/zwift/zwift_click.dart';
|
||||
import 'package:swift_control/bluetooth/devices/zwift/zwift_clickv2.dart';
|
||||
import 'package:swift_control/bluetooth/devices/zwift/zwift_play.dart';
|
||||
import 'package:swift_control/bluetooth/devices/zwift/zwift_ride.dart';
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
|
||||
void main() {
|
||||
group('Detect Zwift devices', () {
|
||||
test('Detect Zwift Play', () {
|
||||
final device = _createBleDevice(
|
||||
name: 'Zwift Play',
|
||||
manufacturerData: [
|
||||
ManufacturerData(ZwiftConstants.ZWIFT_MANUFACTURER_ID, Uint8List.fromList([ZwiftConstants.RC1_RIGHT_SIDE])),
|
||||
],
|
||||
);
|
||||
expect(BluetoothDevice.fromScanResult(device), isInstanceOf<ZwiftPlay>());
|
||||
});
|
||||
|
||||
test('Detect Zwift Ride', () {
|
||||
final device = _createBleDevice(
|
||||
name: 'Zwift Ride',
|
||||
manufacturerData: [
|
||||
ManufacturerData(ZwiftConstants.ZWIFT_MANUFACTURER_ID, Uint8List.fromList([ZwiftConstants.RIDE_LEFT_SIDE])),
|
||||
],
|
||||
services: [ZwiftConstants.ZWIFT_CUSTOM_SERVICE_UUID.toLowerCase()],
|
||||
);
|
||||
expect(BluetoothDevice.fromScanResult(device), isInstanceOf<ZwiftRide>());
|
||||
});
|
||||
test('Detect Zwift Ride old firmware', () {
|
||||
final device = _createBleDevice(
|
||||
name: 'Zwift Ride',
|
||||
manufacturerData: [
|
||||
ManufacturerData(ZwiftConstants.ZWIFT_MANUFACTURER_ID, Uint8List.fromList([ZwiftConstants.RIDE_LEFT_SIDE])),
|
||||
],
|
||||
services: [ZwiftConstants.ZWIFT_RIDE_CUSTOM_SERVICE_UUID.toLowerCase()],
|
||||
);
|
||||
expect(BluetoothDevice.fromScanResult(device), isInstanceOf<ZwiftRide>());
|
||||
});
|
||||
|
||||
test('Detect Zwift Click V1', () {
|
||||
final device = _createBleDevice(
|
||||
name: 'Zwift Click',
|
||||
manufacturerData: [
|
||||
ManufacturerData(ZwiftConstants.ZWIFT_MANUFACTURER_ID, Uint8List.fromList([ZwiftConstants.BC1])),
|
||||
],
|
||||
services: [ZwiftConstants.ZWIFT_CUSTOM_SERVICE_UUID.toLowerCase()],
|
||||
);
|
||||
expect(BluetoothDevice.fromScanResult(device), isInstanceOf<ZwiftClick>());
|
||||
});
|
||||
|
||||
test('Detect Zwift Click V2', () {
|
||||
final device = _createBleDevice(
|
||||
name: 'Zwift Click',
|
||||
manufacturerData: [
|
||||
ManufacturerData(
|
||||
ZwiftConstants.ZWIFT_MANUFACTURER_ID,
|
||||
Uint8List.fromList([ZwiftConstants.CLICK_V2_LEFT_SIDE]),
|
||||
),
|
||||
],
|
||||
services: [ZwiftConstants.ZWIFT_CUSTOM_SERVICE_UUID.toLowerCase()],
|
||||
);
|
||||
expect(BluetoothDevice.fromScanResult(device), isInstanceOf<ZwiftClickV2>());
|
||||
});
|
||||
});
|
||||
|
||||
group('Detect Elite devices', () {
|
||||
test('Elite Square', () {
|
||||
final device = _createBleDevice(name: 'SQUARE 1337');
|
||||
expect(BluetoothDevice.fromScanResult(device), isInstanceOf<EliteSquare>());
|
||||
});
|
||||
test('Elite Sterzo', () {
|
||||
final device = _createBleDevice(name: 'STERZO 1337');
|
||||
expect(BluetoothDevice.fromScanResult(device), isInstanceOf<EliteSterzo>());
|
||||
});
|
||||
});
|
||||
|
||||
group('Detect Wahoo devices', () {
|
||||
test('Kickr Bike Shift', () {
|
||||
final device = _createBleDevice(name: '133 KICKR BIKE SHIFT 133');
|
||||
expect(BluetoothDevice.fromScanResult(device), isInstanceOf<WahooKickrBikeShift>());
|
||||
});
|
||||
});
|
||||
|
||||
group('Detect Cycplus devices', () {
|
||||
test('Cycplus BC2', () {
|
||||
final device = _createBleDevice(name: 'Cycplus BC2');
|
||||
expect(BluetoothDevice.fromScanResult(device), isInstanceOf<CycplusBc2>());
|
||||
});
|
||||
test('Other cycplus', () {
|
||||
final device = _createBleDevice(name: 'Cycplus 1337');
|
||||
expect(BluetoothDevice.fromScanResult(device), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('Detect Shimano Di2', () {
|
||||
test('Shimano Di2', () {
|
||||
final device = _createBleDevice(name: 'RDR 1337', services: [ShimanoDi2Constants.SERVICE_UUID.toLowerCase()]);
|
||||
expect(BluetoothDevice.fromScanResult(device), isInstanceOf<ShimanoDi2>());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
BleDevice _createBleDevice({
|
||||
required String name,
|
||||
List<ManufacturerData> manufacturerData = const <ManufacturerData>[],
|
||||
List<String> services = const [],
|
||||
}) {
|
||||
return BleDevice(
|
||||
deviceId: '1337',
|
||||
name: name,
|
||||
manufacturerDataList: manufacturerData,
|
||||
services: services,
|
||||
);
|
||||
}
|
||||
@@ -1,23 +1,21 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/utils/keymap/apps/custom_app.dart';
|
||||
import 'package:swift_control/utils/settings/settings.dart';
|
||||
|
||||
void main() {
|
||||
group('Custom Profile Tests', () {
|
||||
late Settings settings;
|
||||
|
||||
setUp(() async {
|
||||
// Initialize SharedPreferences with in-memory storage for testing
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
settings = Settings();
|
||||
await settings.init();
|
||||
});
|
||||
|
||||
test('Should create custom app with default profile name', () {
|
||||
final customApp = CustomApp();
|
||||
expect(customApp.profileName, 'Custom');
|
||||
expect(customApp.name, 'Custom');
|
||||
expect(customApp.profileName, 'Other');
|
||||
expect(customApp.name, 'Other');
|
||||
});
|
||||
|
||||
test('Should create custom app with custom profile name', () {
|
||||
@@ -51,6 +49,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('Should duplicate custom profile', () async {
|
||||
await settings.reset();
|
||||
final original = CustomApp(profileName: 'Original');
|
||||
await settings.setKeyMap(original);
|
||||
|
||||
@@ -75,21 +74,6 @@ void main() {
|
||||
expect(profiles.contains('ToDelete'), false);
|
||||
});
|
||||
|
||||
test('Should migrate old custom keymap to new format', () async {
|
||||
// Simulate old storage format
|
||||
SharedPreferences.setMockInitialValues({
|
||||
'customapp': ['test_data'],
|
||||
'app': 'Custom',
|
||||
});
|
||||
|
||||
final newSettings = Settings();
|
||||
await newSettings.init();
|
||||
|
||||
// Check that migration happened
|
||||
expect(newSettings.prefs.containsKey('customapp'), false);
|
||||
expect(newSettings.prefs.containsKey('customapp_Custom'), true);
|
||||
});
|
||||
|
||||
test('Should not duplicate migration if already migrated', () async {
|
||||
SharedPreferences.setMockInitialValues({
|
||||
'customapp': ['old_data'],
|
||||
|
||||
@@ -14,14 +14,14 @@ void main() {
|
||||
final stubActions = actionHandler as StubActions;
|
||||
|
||||
final device = CycplusBc2(BleDevice(deviceId: 'deviceId', name: 'name'));
|
||||
|
||||
|
||||
// Packet 0: [6]=01 [7]=03 -> No trigger (lock state)
|
||||
device.processCharacteristic(
|
||||
CycplusBc2Constants.TX_CHARACTERISTIC_UUID,
|
||||
_hexToUint8List('FEEFFFEE0206010397565E000155'),
|
||||
);
|
||||
expect(stubActions.performedActions.isEmpty, true);
|
||||
|
||||
|
||||
// Packet 1: [6]=03 [7]=03 -> Trigger: shiftUp
|
||||
device.processCharacteristic(
|
||||
CycplusBc2Constants.TX_CHARACTERISTIC_UUID,
|
||||
@@ -30,7 +30,7 @@ void main() {
|
||||
expect(stubActions.performedActions.length, 1);
|
||||
expect(stubActions.performedActions.first, CycplusBc2Buttons.shiftUp);
|
||||
stubActions.performedActions.clear();
|
||||
|
||||
|
||||
// Packet 2: [6]=03 [7]=01 -> Trigger: shiftDown
|
||||
device.processCharacteristic(
|
||||
CycplusBc2Constants.TX_CHARACTERISTIC_UUID,
|
||||
@@ -39,14 +39,14 @@ void main() {
|
||||
expect(stubActions.performedActions.length, 1);
|
||||
expect(stubActions.performedActions.first, CycplusBc2Buttons.shiftDown);
|
||||
stubActions.performedActions.clear();
|
||||
|
||||
|
||||
// Packet 3: [6]=03 [7]=03 -> No trigger (lock state)
|
||||
device.processCharacteristic(
|
||||
CycplusBc2Constants.TX_CHARACTERISTIC_UUID,
|
||||
_hexToUint8List('FEEFFFEE0206030398585E00015A'),
|
||||
);
|
||||
expect(stubActions.performedActions.isEmpty, true);
|
||||
|
||||
|
||||
// Packet 4: [6]=01 [7]=03 -> Trigger: shiftUp
|
||||
device.processCharacteristic(
|
||||
CycplusBc2Constants.TX_CHARACTERISTIC_UUID,
|
||||
@@ -61,28 +61,28 @@ void main() {
|
||||
actionHandler = StubActions();
|
||||
final stubActions = actionHandler as StubActions;
|
||||
final device = CycplusBc2(BleDevice(deviceId: 'deviceId', name: 'name'));
|
||||
|
||||
|
||||
// Press: lock state
|
||||
device.processCharacteristic(
|
||||
CycplusBc2Constants.TX_CHARACTERISTIC_UUID,
|
||||
_hexToUint8List('FEEFFFEE0206010300005E000100'),
|
||||
);
|
||||
expect(stubActions.performedActions.isEmpty, true);
|
||||
|
||||
|
||||
// Release: reset state
|
||||
device.processCharacteristic(
|
||||
CycplusBc2Constants.TX_CHARACTERISTIC_UUID,
|
||||
_hexToUint8List('FEEFFFEE0206000000005E000100'),
|
||||
);
|
||||
expect(stubActions.performedActions.isEmpty, true);
|
||||
|
||||
|
||||
// Press again: lock state (no trigger since we reset)
|
||||
device.processCharacteristic(
|
||||
CycplusBc2Constants.TX_CHARACTERISTIC_UUID,
|
||||
_hexToUint8List('FEEFFFEE0206020300005E000100'),
|
||||
);
|
||||
expect(stubActions.performedActions.isEmpty, true);
|
||||
|
||||
|
||||
// Change to different pressed value: trigger
|
||||
device.processCharacteristic(
|
||||
CycplusBc2Constants.TX_CHARACTERISTIC_UUID,
|
||||
@@ -91,27 +91,26 @@ void main() {
|
||||
expect(stubActions.performedActions.length, 1);
|
||||
expect(stubActions.performedActions.first, CycplusBc2Buttons.shiftUp);
|
||||
});
|
||||
|
||||
|
||||
test('Test both buttons can trigger simultaneously', () {
|
||||
actionHandler = StubActions();
|
||||
final stubActions = actionHandler as StubActions;
|
||||
final device = CycplusBc2(BleDevice(deviceId: 'deviceId', name: 'name'));
|
||||
|
||||
|
||||
// Lock both states
|
||||
device.processCharacteristic(
|
||||
CycplusBc2Constants.TX_CHARACTERISTIC_UUID,
|
||||
_hexToUint8List('FEEFFFEE0206010100005E000100'),
|
||||
);
|
||||
expect(stubActions.performedActions.isEmpty, true);
|
||||
|
||||
|
||||
// Change both: trigger both
|
||||
device.processCharacteristic(
|
||||
CycplusBc2Constants.TX_CHARACTERISTIC_UUID,
|
||||
_hexToUint8List('FEEFFFEE0206020200005E000100'),
|
||||
);
|
||||
expect(stubActions.performedActions.length, 2);
|
||||
expect(stubActions.performedActions.length, 1);
|
||||
expect(stubActions.performedActions.contains(CycplusBc2Buttons.shiftUp), true);
|
||||
expect(stubActions.performedActions.contains(CycplusBc2Buttons.shiftDown), true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ void main() {
|
||||
// Test that NaN values are filtered out
|
||||
final samples = [double.nan, 1.5, 2.0, 2.5, 3.0, double.nan, 3.5, 4.0, 4.5, 5.0, 5.5];
|
||||
final validSamples = samples.where((s) => !s.isNaN).take(10).toList();
|
||||
|
||||
expect(validSamples.length, equals(10));
|
||||
|
||||
expect(validSamples.length, equals(9));
|
||||
expect(validSamples.every((s) => !s.isNaN), isTrue);
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ void main() {
|
||||
// Test offset calculation
|
||||
final samples = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
|
||||
final offset = samples.reduce((a, b) => a + b) / samples.length;
|
||||
|
||||
|
||||
expect(offset, equals(5.5));
|
||||
});
|
||||
|
||||
@@ -40,18 +40,18 @@ void main() {
|
||||
return levels.clamp(1, maxLevels);
|
||||
}
|
||||
|
||||
expect(calculateLevels(5), equals(1)); // Below threshold but level 1
|
||||
expect(calculateLevels(10), equals(1)); // 10 / 10 = 1
|
||||
expect(calculateLevels(15), equals(1)); // 15 / 10 = 1.5 floor = 1
|
||||
expect(calculateLevels(20), equals(2)); // 20 / 10 = 2
|
||||
expect(calculateLevels(35), equals(3)); // 35 / 10 = 3.5 floor = 3
|
||||
expect(calculateLevels(50), equals(5)); // 50 / 10 = 5 (max)
|
||||
expect(calculateLevels(5), equals(1)); // Below threshold but level 1
|
||||
expect(calculateLevels(10), equals(1)); // 10 / 10 = 1
|
||||
expect(calculateLevels(15), equals(1)); // 15 / 10 = 1.5 floor = 1
|
||||
expect(calculateLevels(20), equals(2)); // 20 / 10 = 2
|
||||
expect(calculateLevels(35), equals(3)); // 35 / 10 = 3.5 floor = 3
|
||||
expect(calculateLevels(50), equals(5)); // 50 / 10 = 5 (max)
|
||||
expect(calculateLevels(100), equals(5)); // 100 / 10 = 10 but clamped to 5
|
||||
});
|
||||
|
||||
test('Should determine correct steering direction', () {
|
||||
// Test direction determination
|
||||
expect(25 > 0, isTrue); // Positive = RIGHT
|
||||
expect(25 > 0, isTrue); // Positive = RIGHT
|
||||
expect(-25 > 0, isFalse); // Negative = LEFT
|
||||
});
|
||||
});
|
||||
@@ -61,9 +61,9 @@ void main() {
|
||||
const steeringThreshold = 10.0;
|
||||
|
||||
// Test threshold logic
|
||||
expect(5.abs() > steeringThreshold, isFalse); // Below threshold
|
||||
expect(10.abs() > steeringThreshold, isFalse); // At threshold
|
||||
expect(11.abs() > steeringThreshold, isTrue); // Above threshold
|
||||
expect(5.abs() > steeringThreshold, isFalse); // Below threshold
|
||||
expect(10.abs() > steeringThreshold, isFalse); // At threshold
|
||||
expect(11.abs() > steeringThreshold, isTrue); // Above threshold
|
||||
expect((-11).abs() > steeringThreshold, isTrue); // Above threshold (negative)
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:swift_control/pages/touch_area.dart';
|
||||
import 'package:swift_control/utils/keymap/keymap.dart';
|
||||
|
||||
void main() {
|
||||
group('TouchAreaSetupPage Orientation Tests', () {
|
||||
testWidgets('TouchAreaSetupPage should force landscape orientation on init', (WidgetTester tester) async {
|
||||
// Track system chrome method calls
|
||||
final List<MethodCall> systemChromeCalls = [];
|
||||
|
||||
// Mock SystemChrome.setPreferredOrientations
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
SystemChannels.platform,
|
||||
(MethodCall methodCall) async {
|
||||
systemChromeCalls.add(methodCall);
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
// Build the TouchAreaSetupPage
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: TouchAreaSetupPage(
|
||||
keyPair: KeyPair(buttons: [], physicalKey: null, logicalKey: null),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Verify that setPreferredOrientations was called with landscape orientations
|
||||
final orientationCalls = systemChromeCalls
|
||||
.where((call) => call.method == 'SystemChrome.setPreferredOrientations')
|
||||
.toList();
|
||||
|
||||
expect(orientationCalls, isNotEmpty);
|
||||
|
||||
// Check if landscape orientations were set
|
||||
final lastOrientationCall = orientationCalls.last;
|
||||
final orientations = lastOrientationCall.arguments as List<String>;
|
||||
|
||||
expect(orientations, contains('DeviceOrientation.landscapeLeft'));
|
||||
expect(orientations, contains('DeviceOrientation.landscapeRight'));
|
||||
expect(orientations, hasLength(2)); // Only landscape orientations
|
||||
});
|
||||
|
||||
test('DeviceOrientation enum values are accessible', () {
|
||||
// Test that we can access the DeviceOrientation enum values
|
||||
final orientations = [
|
||||
DeviceOrientation.landscapeLeft,
|
||||
DeviceOrientation.landscapeRight,
|
||||
DeviceOrientation.portraitUp,
|
||||
DeviceOrientation.portraitDown,
|
||||
];
|
||||
|
||||
expect(orientations, hasLength(4));
|
||||
expect(orientations, contains(DeviceOrientation.landscapeLeft));
|
||||
expect(orientations, contains(DeviceOrientation.landscapeRight));
|
||||
expect(orientations, contains(DeviceOrientation.portraitUp));
|
||||
expect(orientations, contains(DeviceOrientation.portraitDown));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:swift_control/bluetooth/devices/zwift/constants.dart';
|
||||
import 'package:swift_control/utils/keymap/keymap.dart';
|
||||
|
||||
void main() {
|
||||
group('Percentage-based Keymap Tests', () {
|
||||
test('Should encode touch position as percentage using fallback screen size', () {
|
||||
final keyPair = KeyPair(
|
||||
buttons: [ZwiftButtons.shiftUpRight],
|
||||
physicalKey: null,
|
||||
logicalKey: null,
|
||||
touchPosition: Offset(960, 540), // Center of 1920x1080 fallback
|
||||
);
|
||||
|
||||
final encoded = keyPair.encode();
|
||||
expect(encoded, contains('x_percent'));
|
||||
expect(encoded, contains('y_percent'));
|
||||
// Should use fallback screen size of 1920x1080
|
||||
expect(encoded, contains('0.5')); // 960/1920 and 540/1080 = 0.5
|
||||
});
|
||||
|
||||
test('Should encode touch position as percentages with fallback when screen size not available', () {
|
||||
final keyPair = KeyPair(
|
||||
buttons: [ZwiftButtons.shiftDownLeft],
|
||||
physicalKey: null,
|
||||
logicalKey: null,
|
||||
touchPosition: Offset(960, 540), // Center of 1920x1080 fallback
|
||||
);
|
||||
|
||||
final encoded = keyPair.encode();
|
||||
expect(encoded, contains('x_percent'));
|
||||
expect(encoded, contains('y_percent'));
|
||||
// Should use fallback screen size of 1920x1080
|
||||
expect(encoded, contains('0.5')); // 960/1920 and 540/1080 = 0.5
|
||||
});
|
||||
|
||||
test('Should decode percentage-based touch position correctly', () {
|
||||
final encoded =
|
||||
'{"actions":["leftButton"],"logicalKey":"0","physicalKey":"0","touchPosition":{"x_percent":0.5,"y_percent":0.5},"isLongPress":false}';
|
||||
|
||||
final keyPair = KeyPair.decode(encoded);
|
||||
expect(keyPair, isNotNull);
|
||||
// Since no real screen is available in tests, it should return Offset.zero or use fallback
|
||||
expect(keyPair!.touchPosition, isNotNull);
|
||||
});
|
||||
|
||||
test('Should decode pixel-based touch position correctly (backward compatibility)', () {
|
||||
final encoded =
|
||||
'{"actions":["leftButton"],"logicalKey":"0","physicalKey":"0","touchPosition":{"x":300,"y":600},"isLongPress":false}';
|
||||
|
||||
final keyPair = KeyPair.decode(encoded);
|
||||
expect(keyPair, isNotNull);
|
||||
expect(keyPair!.touchPosition.dx, 300);
|
||||
expect(keyPair.touchPosition.dy, 600);
|
||||
});
|
||||
|
||||
test('Should handle zero touch position correctly', () {
|
||||
final keyPair = KeyPair(
|
||||
buttons: [ZwiftButtons.shiftUpLeft],
|
||||
physicalKey: PhysicalKeyboardKey.keyA,
|
||||
logicalKey: LogicalKeyboardKey.keyA,
|
||||
touchPosition: Offset.zero,
|
||||
);
|
||||
|
||||
final encoded = keyPair.encode();
|
||||
// Should encode as percentages even when position is zero
|
||||
expect(encoded, contains('x_percent'));
|
||||
expect(encoded, contains('y_percent'));
|
||||
expect(encoded, contains('0.0'));
|
||||
});
|
||||
|
||||
test('Should encode and decode with fallback screen size', () {
|
||||
final keyPair = KeyPair(
|
||||
buttons: [ZwiftButtons.shiftUpRight],
|
||||
physicalKey: null,
|
||||
logicalKey: null,
|
||||
touchPosition: Offset(480, 270), // 25% of 1920x1080
|
||||
);
|
||||
|
||||
// Encode (will use fallback screen size)
|
||||
final encoded = keyPair.encode();
|
||||
|
||||
// Decode (will also use fallback or available screen size)
|
||||
final decoded = KeyPair.decode(encoded);
|
||||
|
||||
expect(decoded, isNotNull);
|
||||
expect(decoded!.touchPosition, isNotNull);
|
||||
});
|
||||
|
||||
test('Should handle decoding when no screen size available', () {
|
||||
final encoded =
|
||||
'{"actions":["leftButton"],"logicalKey":"0","physicalKey":"0","touchPosition":{"x_percent":0.5,"y_percent":0.5},"isLongPress":false}';
|
||||
|
||||
final keyPair = KeyPair.decode(encoded);
|
||||
expect(keyPair, isNotNull);
|
||||
// When no screen size is available, it may return Offset.zero as fallback
|
||||
expect(keyPair!.touchPosition, isNotNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
125
test/screenshot_test.dart
Normal file
125
test/screenshot_test.dart
Normal file
@@ -0,0 +1,125 @@
|
||||
import 'package:file/file.dart';
|
||||
import 'package:file/local.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:swift_control/bluetooth/devices/zwift/zwift_ride.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/pages/device.dart';
|
||||
import 'package:swift_control/theme.dart';
|
||||
import 'package:swift_control/utils/keymap/apps/my_whoosh.dart';
|
||||
import 'package:swift_control/utils/requirements/multi.dart';
|
||||
import 'package:test_screenshot/test_screenshot.dart';
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
PackageInfo.setMockInitialValues(
|
||||
appName: 'SwiftControl',
|
||||
packageName: 'de.jonasbark.swiftcontrol',
|
||||
version: '3.5.0',
|
||||
buildNumber: '1',
|
||||
buildSignature: '',
|
||||
);
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
|
||||
group('Screenshot Tests', () {
|
||||
final List<(String type, Size size)> sizes = [('macOS', Size(1280, 800)), ('GitHub', Size(600, 900))];
|
||||
|
||||
testWidgets('Requirements', (WidgetTester tester) async {
|
||||
await tester.loadFonts();
|
||||
for (final size in sizes) {
|
||||
await _createRequirementScreenshot(tester, size);
|
||||
}
|
||||
|
||||
// Reset
|
||||
});
|
||||
testWidgets('Device', (WidgetTester tester) async {
|
||||
await tester.loadFonts();
|
||||
for (final size in sizes) {
|
||||
await _createDeviceScreenshot(tester, size);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _createDeviceScreenshot(WidgetTester tester, (String type, Size size) spec) async {
|
||||
// Set phone screen size (typical Android phone - 1140x2616 to match existing)
|
||||
tester.view.physicalSize = spec.$2;
|
||||
tester.view.devicePixelRatio = 1;
|
||||
|
||||
screenshotMode = true;
|
||||
|
||||
await settings.init();
|
||||
await settings.reset();
|
||||
settings.setTrainerApp(MyWhoosh());
|
||||
settings.setKeyMap(MyWhoosh());
|
||||
settings.setLastTarget(Target.thisDevice);
|
||||
|
||||
connection.addDevices([
|
||||
ZwiftRide(
|
||||
BleDevice(
|
||||
name: 'Controller',
|
||||
deviceId: '00:11:22:33:44:55',
|
||||
),
|
||||
)
|
||||
..firmwareVersion = '1.2.0'
|
||||
..rssi = -51
|
||||
..batteryLevel = 81,
|
||||
]);
|
||||
|
||||
await tester.pumpWidget(
|
||||
Screenshotter(
|
||||
child: MaterialApp(
|
||||
navigatorKey: navigatorKey,
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'SwiftControl',
|
||||
theme: AppTheme.light,
|
||||
darkTheme: AppTheme.dark,
|
||||
themeMode: ThemeMode.light,
|
||||
home: const DevicePage(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const wait = 1;
|
||||
|
||||
try {
|
||||
await tester.pumpAndSettle(Duration(seconds: wait), EnginePhase.sendSemanticsUpdate, Duration(seconds: wait));
|
||||
} catch (e) {
|
||||
// Ignore timeout errors
|
||||
}
|
||||
|
||||
await _takeScreenshot(tester, 'device-${spec.$1}-${spec.$2.width.toInt()}x${spec.$2.height.toInt()}.png');
|
||||
}
|
||||
|
||||
Future<void> _createRequirementScreenshot(WidgetTester tester, (String type, Size size) spec) async {
|
||||
// Set phone screen size (typical Android phone - 1140x2616 to match existing)
|
||||
tester.view.physicalSize = spec.$2;
|
||||
tester.view.devicePixelRatio = 1;
|
||||
|
||||
await settings.init();
|
||||
await settings.reset();
|
||||
screenshotMode = true;
|
||||
await tester.pumpWidget(
|
||||
Screenshotter(
|
||||
child: SwiftPlayApp(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await _takeScreenshot(tester, 'screenshot-${spec.$1}-${spec.$2.width.toInt()}x${spec.$2.height.toInt()}.png');
|
||||
}
|
||||
|
||||
Future<void> _takeScreenshot(WidgetTester tester, String path) async {
|
||||
const FileSystem fs = LocalFileSystem();
|
||||
final file = fs.file('screenshots/$path');
|
||||
await fs.directory('screenshots').create();
|
||||
print('File path: ${file.absolute.path}');
|
||||
|
||||
await tester.screenshot(path: 'screenshots/$path');
|
||||
final decodedImage = await decodeImageFromList(file.readAsBytesSync());
|
||||
print(decodedImage);
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:swift_control/utils/settings/settings.dart';
|
||||
|
||||
void main() {
|
||||
group('Vibration Setting Tests', () {
|
||||
late Settings settings;
|
||||
|
||||
setUp(() async {
|
||||
// Initialize SharedPreferences with in-memory storage for testing
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
settings = Settings();
|
||||
await settings.init();
|
||||
});
|
||||
|
||||
test('Vibration setting should default to true', () {
|
||||
expect(settings.getVibrationEnabled(), true);
|
||||
});
|
||||
|
||||
test('Vibration setting should persist when set to false', () async {
|
||||
await settings.setVibrationEnabled(false);
|
||||
expect(settings.getVibrationEnabled(), false);
|
||||
});
|
||||
|
||||
test('Vibration setting should persist when set to true', () async {
|
||||
await settings.setVibrationEnabled(true);
|
||||
expect(settings.getVibrationEnabled(), true);
|
||||
});
|
||||
|
||||
test('Vibration setting should toggle correctly', () async {
|
||||
// Start with default (true)
|
||||
expect(settings.getVibrationEnabled(), true);
|
||||
|
||||
// Toggle to false
|
||||
await settings.setVibrationEnabled(false);
|
||||
expect(settings.getVibrationEnabled(), false);
|
||||
|
||||
// Toggle back to true
|
||||
await settings.setVibrationEnabled(true);
|
||||
expect(settings.getVibrationEnabled(), true);
|
||||
});
|
||||
|
||||
test('Vibration setting should persist across Settings instances', () async {
|
||||
// Set vibration to false
|
||||
await settings.setVibrationEnabled(false);
|
||||
expect(settings.getVibrationEnabled(), false);
|
||||
|
||||
// Create a new Settings instance
|
||||
final newSettings = Settings();
|
||||
await newSettings.init();
|
||||
|
||||
// Should still be false
|
||||
expect(newSettings.getVibrationEnabled(), false);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
// This is a basic Flutter widget test.
|
||||
//
|
||||
// To perform an interaction with a widget in your test, use the WidgetTester
|
||||
// utility in the flutter_test package. For example, you can send tap and scroll
|
||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||
// tree, read text, and verify that the values of widget properties are correct.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||
// Build our app and trigger a frame.
|
||||
await tester.pumpWidget(const SwiftPlayApp());
|
||||
|
||||
// Verify that our counter starts at 0.
|
||||
expect(find.text('0'), findsOneWidget);
|
||||
expect(find.text('1'), findsNothing);
|
||||
|
||||
// Tap the '+' icon and trigger a frame.
|
||||
await tester.tap(find.byIcon(Icons.add));
|
||||
await tester.pump();
|
||||
|
||||
// Verify that our counter has incremented.
|
||||
expect(find.text('0'), findsNothing);
|
||||
expect(find.text('1'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:swift_control/bluetooth/devices/zwift/zwift_ride.dart';
|
||||
|
||||
void main() {
|
||||
group('Zwift Ride Analog Paddle - ZigZag Encoding Tests', () {
|
||||
test('Should correctly decode positive ZigZag values', () {
|
||||
// Test ZigZag decoding algorithm: (n >>> 1) ^ -(n & 1)
|
||||
const threshold = ZwiftRide.analogPaddleThreshold;
|
||||
|
||||
expect(_zigzagDecode(0), 0); // 0 -> 0
|
||||
expect(_zigzagDecode(2), 1); // 2 -> 1
|
||||
expect(_zigzagDecode(4), 2); // 4 -> 2
|
||||
expect(_zigzagDecode(threshold * 2), threshold); // threshold value
|
||||
expect(_zigzagDecode(200), 100); // 200 -> 100 (max positive)
|
||||
});
|
||||
|
||||
test('Should correctly decode negative ZigZag values', () {
|
||||
const threshold = ZwiftRide.analogPaddleThreshold;
|
||||
|
||||
expect(_zigzagDecode(1), -1); // 1 -> -1
|
||||
expect(_zigzagDecode(3), -2); // 3 -> -2
|
||||
expect(_zigzagDecode(threshold * 2 - 1), -threshold); // negative threshold
|
||||
expect(_zigzagDecode(199), -100); // 199 -> -100 (max negative)
|
||||
});
|
||||
|
||||
test('Should handle boundary values correctly', () {
|
||||
const threshold = ZwiftRide.analogPaddleThreshold;
|
||||
|
||||
// Test values at the detection threshold
|
||||
expect(_zigzagDecode(threshold * 2).abs(), threshold);
|
||||
expect(_zigzagDecode(threshold * 2 - 1).abs(), threshold);
|
||||
|
||||
// Test maximum paddle values (±100)
|
||||
expect(_zigzagDecode(200), 100);
|
||||
expect(_zigzagDecode(199), -100);
|
||||
});
|
||||
});
|
||||
|
||||
group('Zwift Ride Analog Paddle - Protocol Buffer Varint Decoding', () {
|
||||
test('Should decode single-byte varint values', () {
|
||||
// Values 0-127 fit in a single byte
|
||||
final buffer1 = Uint8List.fromList([0x00]); // 0
|
||||
expect(_decodeVarint(buffer1, 0).$1, 0);
|
||||
expect(_decodeVarint(buffer1, 0).$2, 1); // Consumed 1 byte
|
||||
|
||||
final buffer2 = Uint8List.fromList([0x0A]); // 10
|
||||
expect(_decodeVarint(buffer2, 0).$1, 10);
|
||||
|
||||
final buffer3 = Uint8List.fromList([0x7F]); // 127
|
||||
expect(_decodeVarint(buffer3, 0).$1, 127);
|
||||
});
|
||||
|
||||
test('Should decode multi-byte varint values', () {
|
||||
// Values >= 128 require multiple bytes
|
||||
final buffer1 = Uint8List.fromList([0xC7, 0x01]); // ZigZag encoded -100 (199)
|
||||
expect(_decodeVarint(buffer1, 0).$1, 199);
|
||||
expect(_decodeVarint(buffer1, 0).$2, 2); // Consumed 2 bytes
|
||||
|
||||
final buffer2 = Uint8List.fromList([0xC8, 0x01]); // ZigZag encoded 100 (200)
|
||||
expect(_decodeVarint(buffer2, 0).$1, 200);
|
||||
expect(_decodeVarint(buffer2, 0).$2, 2);
|
||||
});
|
||||
|
||||
test('Should handle varint decoding with offset', () {
|
||||
// Test decoding from a specific offset in the buffer
|
||||
final buffer = Uint8List.fromList([0xFF, 0xFF, 0xC8, 0x01]); // Garbage + 200
|
||||
expect(_decodeVarint(buffer, 2).$1, 200);
|
||||
expect(_decodeVarint(buffer, 2).$2, 2);
|
||||
});
|
||||
});
|
||||
|
||||
group('Zwift Ride Analog Paddle - Protocol Buffer Wire Type Parsing', () {
|
||||
test('Should correctly extract field number and wire type from tag', () {
|
||||
// Tag format: (field_number << 3) | wire_type
|
||||
|
||||
// Field 1, wire type 0 (varint)
|
||||
final tag1 = 0x08; // 1 << 3 | 0
|
||||
expect(tag1 >> 3, 1); // field number
|
||||
expect(tag1 & 0x7, 0); // wire type
|
||||
|
||||
// Field 2, wire type 0 (varint)
|
||||
final tag2 = 0x10; // 2 << 3 | 0
|
||||
expect(tag2 >> 3, 2);
|
||||
expect(tag2 & 0x7, 0);
|
||||
|
||||
// Field 3, wire type 2 (length-delimited)
|
||||
final tag3 = 0x1a; // 3 << 3 | 2
|
||||
expect(tag3 >> 3, 3);
|
||||
expect(tag3 & 0x7, 2);
|
||||
});
|
||||
|
||||
test('Should identify location and value field tags', () {
|
||||
const locationTag = 0x08; // Field 1 (location), wire type 0
|
||||
const valueTag = 0x10; // Field 2 (value), wire type 0
|
||||
const nestedMessageTag = 0x1a; // Field 3 (nested), wire type 2
|
||||
|
||||
expect(locationTag >> 3, 1);
|
||||
expect(valueTag >> 3, 2);
|
||||
expect(nestedMessageTag >> 3, 3);
|
||||
expect(nestedMessageTag & 0x7, 2); // Length-delimited
|
||||
});
|
||||
});
|
||||
|
||||
group('Zwift Ride Analog Paddle - Real-world Scenarios', () {
|
||||
test('Threshold value should trigger paddle detection', () {
|
||||
const threshold = ZwiftRide.analogPaddleThreshold;
|
||||
// At threshold: ZigZag encoding of threshold
|
||||
final zigzagValue = threshold * 2;
|
||||
final decodedValue = _zigzagDecode(zigzagValue);
|
||||
expect(decodedValue, threshold);
|
||||
expect(decodedValue.abs() >= threshold, isTrue);
|
||||
});
|
||||
|
||||
test('Below threshold value should not trigger paddle detection', () {
|
||||
const threshold = ZwiftRide.analogPaddleThreshold;
|
||||
// Below threshold: value = threshold - 1
|
||||
final zigzagValue = (threshold - 1) * 2;
|
||||
final decodedValue = _zigzagDecode(zigzagValue);
|
||||
expect(decodedValue, threshold - 1);
|
||||
expect(decodedValue.abs() >= threshold, isFalse);
|
||||
});
|
||||
|
||||
test('Maximum paddle press (-100) should trigger left paddle', () {
|
||||
const threshold = ZwiftRide.analogPaddleThreshold;
|
||||
// Max left: value = -100, ZigZag = 199 = 0xC7 0x01
|
||||
final zigzagValue = 199;
|
||||
final decodedValue = _zigzagDecode(zigzagValue);
|
||||
expect(decodedValue, -100);
|
||||
expect(decodedValue.abs() >= threshold, isTrue);
|
||||
});
|
||||
|
||||
test('Maximum paddle press (100) should trigger right paddle', () {
|
||||
const threshold = ZwiftRide.analogPaddleThreshold;
|
||||
// Max right: value = 100, ZigZag = 200 = 0xC8 0x01
|
||||
final zigzagValue = 200;
|
||||
final decodedValue = _zigzagDecode(zigzagValue);
|
||||
expect(decodedValue, 100);
|
||||
expect(decodedValue.abs() >= threshold, isTrue);
|
||||
});
|
||||
|
||||
test('Paddle location mapping is correct', () {
|
||||
// Location 0 = left paddle
|
||||
// Location 1 = right paddle
|
||||
const leftPaddleLocation = 0;
|
||||
const rightPaddleLocation = 1;
|
||||
|
||||
expect(leftPaddleLocation, 0);
|
||||
expect(rightPaddleLocation, 1);
|
||||
});
|
||||
|
||||
test('Analog paddle threshold constant is accessible', () {
|
||||
expect(ZwiftRide.analogPaddleThreshold, 25);
|
||||
});
|
||||
});
|
||||
|
||||
group('Zwift Ride Analog Paddle - Message Structure Documentation', () {
|
||||
test('0x1a marker identifies analog message sections', () {
|
||||
const analogSectionMarker = 0x1a;
|
||||
// Field 3 << 3 | wire type 2 = 3 << 3 | 2 = 26 = 0x1a
|
||||
expect(analogSectionMarker, 0x1a);
|
||||
expect(analogSectionMarker >> 3, 3); // Field number
|
||||
expect(analogSectionMarker & 0x7, 2); // Wire type (length-delimited)
|
||||
});
|
||||
|
||||
test('Message offset 7 skips header and button map', () {
|
||||
// Offset breakdown:
|
||||
// [0]: Message type (0x23 for controller notification)
|
||||
// [1]: Button map field tag (0x05)
|
||||
// [2-6]: Button map (5 bytes)
|
||||
// [7]: Start of analog data
|
||||
const messageTypeOffset = 0;
|
||||
const buttonMapTagOffset = 1;
|
||||
const buttonMapOffset = 2;
|
||||
const buttonMapSize = 5;
|
||||
const analogDataOffset = 7;
|
||||
|
||||
expect(analogDataOffset, buttonMapOffset + buttonMapSize);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Helper function to test ZigZag decoding algorithm.
|
||||
/// ZigZag encoding maps signed integers to unsigned integers:
|
||||
/// 0 -> 0, -1 -> 1, 1 -> 2, -2 -> 3, 2 -> 4, etc.
|
||||
int _zigzagDecode(int n) {
|
||||
return (n >>> 1) ^ -(n & 1);
|
||||
}
|
||||
|
||||
/// Helper function to decode a Protocol Buffer varint from a buffer.
|
||||
/// Returns a record of (value, bytesConsumed).
|
||||
(int, int) _decodeVarint(Uint8List buffer, int offset) {
|
||||
var value = 0;
|
||||
var shift = 0;
|
||||
var bytesRead = 0;
|
||||
|
||||
while (offset + bytesRead < buffer.length) {
|
||||
final byte = buffer[offset + bytesRead];
|
||||
value |= (byte & 0x7f) << shift;
|
||||
bytesRead++;
|
||||
|
||||
if ((byte & 0x80) == 0) {
|
||||
// MSB is 0, we're done
|
||||
break;
|
||||
}
|
||||
shift += 7;
|
||||
}
|
||||
|
||||
return (value, bytesRead);
|
||||
}
|
||||
Reference in New Issue
Block a user