Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,24 @@ 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: |
mkdir -p build/screenshots
flutter pub get
flutter test integration_test/screenshot_test.dart --driver=test_driver/integration_test.dart
cd build/screenshots
zip -r ../SwiftControl.screenshots.zip .
cd ../..
echo "Screenshots generated successfully"

- name: 🚀 Shorebird Release iOS
if: inputs.build_ios
uses: shorebirdtech/shorebird-release@v1
Expand Down Expand Up @@ -238,6 +256,15 @@ 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: |
build/SwiftControl.screenshots.zip

#10 Extract Version
- name: Extract version from pubspec.yaml
if: inputs.build_github
Expand All @@ -251,7 +278,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
Expand Down
53 changes: 53 additions & 0 deletions integration_test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Screenshot Generation

This directory contains integration tests for generating app store screenshots during the build pipeline.

## Overview

The screenshot generation system automatically creates promotional screenshots for:
- **Phone screens**: 1140x2616 (mob1.png, mob2.png)
- **Tablet screens**: 2248x2480 (tab1.png, tab2.png)
- **macOS screens**: 1280x800 (mac_screenshot_1.png, mac_screenshot_2.png)

## How It Works

1. **Integration Test**: `screenshot_test.dart` defines test cases for each screen size
2. **Test Driver**: `../test_driver/integration_test.dart` handles saving screenshots to disk
3. **CI/CD Integration**: The build workflow runs these tests and packages screenshots

## Running Locally

To generate screenshots locally:

```bash
# From the project root
flutter test integration_test/screenshot_test.dart --driver=test_driver/integration_test.dart
```

Screenshots will be saved to `build/screenshots/`.

## CI/CD Process

During the GitHub Actions build workflow:
1. Flutter environment is set up
2. Dependencies are installed with `flutter pub get`
3. Integration tests run and generate screenshots
4. Screenshots are zipped into `SwiftControl.screenshots.zip`
5. The zip file is uploaded as a workflow artifact
6. The zip file is attached to the GitHub release

## Modifying Screenshots

To modify the screenshots:
1. Edit `screenshot_test.dart` to change the UI or add new screens
2. Adjust screen sizes by modifying `physicalSizeTestValue`
3. Test locally to verify the output
4. Commit changes - CI will automatically generate new screenshots

## Screenshot Content

The generated screenshots show:
- Main app screen with branding and connect button
- Device list showing various supported devices (Zwift Click, Play, Elite Sterzo, etc.)
- Dark theme matching the app's design
- Consistent branding with the app's color scheme
278 changes: 278 additions & 0 deletions integration_test/screenshot_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';

void main() {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();

group('Screenshot Tests', () {
testWidgets('Generate phone screenshots', (WidgetTester tester) async {
// Set phone screen size (typical Android phone - 1140x2616 to match existing)
binding.window.physicalSizeTestValue = const Size(1140, 2616);
binding.window.devicePixelRatioTestValue = 1.0;

// Build a simple demo screen
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
backgroundColor: const Color(0xFF121212),
appBar: AppBar(
title: const Text('SwiftControl'),
backgroundColor: const Color(0xFF1E88E5),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.bluetooth, size: 100, color: Color(0xFF1E88E5)),
const SizedBox(height: 20),
const Text(
'SwiftControl',
style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: Colors.white),
),
const SizedBox(height: 10),
const Text(
'Control your virtual riding',
style: TextStyle(fontSize: 16, color: Colors.white70),
),
const SizedBox(height: 40),
ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF1E88E5),
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 16),
),
child: const Text('Connect Device', style: TextStyle(fontSize: 18)),
),
],
),
),
),
),
);
await tester.pumpAndSettle();

// Take screenshot
await takeScreenshot(binding, 'mob1', tester);

// Build second screen variant
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
backgroundColor: const Color(0xFF121212),
appBar: AppBar(
title: const Text('Connected Devices'),
backgroundColor: const Color(0xFF1E88E5),
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_buildDeviceCard('Zwift Click', 'Connected', true),
_buildDeviceCard('Zwift Play', 'Paired', false),
_buildDeviceCard('Elite Sterzo', 'Available', false),
],
),
),
),
);
await tester.pumpAndSettle();

await takeScreenshot(binding, 'mob2', tester);

// Reset
binding.window.clearPhysicalSizeTestValue();
binding.window.clearDevicePixelRatioTestValue();
});

testWidgets('Generate tablet screenshots', (WidgetTester tester) async {
// Set tablet screen size (2248x2480 to match existing)
binding.window.physicalSizeTestValue = const Size(2248, 2480);
binding.window.devicePixelRatioTestValue = 1.0;

// Build demo screen
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
backgroundColor: const Color(0xFF121212),
appBar: AppBar(
title: const Text('SwiftControl'),
backgroundColor: const Color(0xFF1E88E5),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.bluetooth, size: 120, color: Color(0xFF1E88E5)),
const SizedBox(height: 20),
const Text(
'SwiftControl',
style: TextStyle(fontSize: 40, fontWeight: FontWeight.bold, color: Colors.white),
),
const SizedBox(height: 10),
const Text(
'Control your virtual riding',
style: TextStyle(fontSize: 20, color: Colors.white70),
),
const SizedBox(height: 40),
ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF1E88E5),
padding: const EdgeInsets.symmetric(horizontal: 50, vertical: 20),
),
child: const Text('Connect Device', style: TextStyle(fontSize: 22)),
),
],
),
),
),
),
);
await tester.pumpAndSettle();

await takeScreenshot(binding, 'tab1', tester);

// Build second screen
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
backgroundColor: const Color(0xFF121212),
appBar: AppBar(
title: const Text('Connected Devices'),
backgroundColor: const Color(0xFF1E88E5),
),
body: GridView.count(
crossAxisCount: 2,
padding: const EdgeInsets.all(16),
children: [
_buildDeviceCard('Zwift Click', 'Connected', true),
_buildDeviceCard('Zwift Play', 'Paired', false),
_buildDeviceCard('Elite Sterzo', 'Available', false),
_buildDeviceCard('Shimano Di2', 'Available', false),
],
),
),
),
);
await tester.pumpAndSettle();

await takeScreenshot(binding, 'tab2', tester);

// Reset
binding.window.clearPhysicalSizeTestValue();
binding.window.clearDevicePixelRatioTestValue();
});

testWidgets('Generate macOS screenshots', (WidgetTester tester) async {
// Set desktop screen size (1280x800)
binding.window.physicalSizeTestValue = const Size(1280, 800);
binding.window.devicePixelRatioTestValue = 1.0;

// Build demo screen
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
backgroundColor: const Color(0xFF121212),
appBar: AppBar(
title: const Text('SwiftControl'),
backgroundColor: const Color(0xFF1E88E5),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.bluetooth, size: 80, color: Color(0xFF1E88E5)),
const SizedBox(height: 20),
const Text(
'SwiftControl',
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.white),
),
const SizedBox(height: 10),
const Text(
'Control your virtual riding',
style: TextStyle(fontSize: 14, color: Colors.white70),
),
const SizedBox(height: 30),
ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF1E88E5),
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 12),
),
child: const Text('Connect Device', style: TextStyle(fontSize: 16)),
),
],
),
),
),
),
);
await tester.pumpAndSettle();

await takeScreenshot(binding, 'mac_screenshot_1', tester);

// Build second screen
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
backgroundColor: const Color(0xFF121212),
appBar: AppBar(
title: const Text('Connected Devices'),
backgroundColor: const Color(0xFF1E88E5),
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_buildDeviceCard('Zwift Click', 'Connected', true),
_buildDeviceCard('Zwift Play', 'Paired', false),
_buildDeviceCard('Elite Sterzo', 'Available', false),
],
),
),
),
);
await tester.pumpAndSettle();

await takeScreenshot(binding, 'mac_screenshot_2', tester);

// Reset
binding.window.clearPhysicalSizeTestValue();
binding.window.clearDevicePixelRatioTestValue();
});
});
}

Widget _buildDeviceCard(String name, String status, bool isConnected) {
return Card(
color: const Color(0xFF1E1E1E),
margin: const EdgeInsets.symmetric(vertical: 8),
child: ListTile(
leading: Icon(
isConnected ? Icons.bluetooth_connected : Icons.bluetooth,
color: isConnected ? Colors.green : Colors.grey,
size: 32,
),
title: Text(
name,
style: const TextStyle(color: Colors.white, fontSize: 16),
),
subtitle: Text(
status,
style: TextStyle(
color: isConnected ? Colors.green : Colors.grey,
fontSize: 14,
),
),
),
);
}

Future<void> takeScreenshot(
IntegrationTestWidgetsFlutterBinding binding,
String screenshotName,
WidgetTester tester,
) async {
await tester.pumpAndSettle();
await binding.takeScreenshot(screenshotName);
}

2 changes: 2 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
integration_test:
sdk: flutter

flutter_lints: ^6.0.0
msix: ^3.16.12
Expand Down
Loading