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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 1.2.0

* Control Sidebar UI overhaul

# 1.1.0

* Introduction of a StageMode to enable previews.
Expand Down
191 changes: 191 additions & 0 deletions EPIC1_TESTING_GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# Epic 1: Core Recording & Playback Engine - Testing Guide

## Overview

Epic 1 implements the foundational recording and playback system for StageCraft. This system captures widget state changes and canvas settings over time, allowing you to record scenarios and replay them later.

## Components Implemented

### Core Data Models
- **`ScenarioFrame`**: Represents a single moment in time with control values, canvas settings, and drawing calls
- **`TestScenario`**: Contains a collection of frames representing a complete recording session
- **`DrawingCall`**: Stores information about drawing operations (method, args, widget context)

### Controllers and Services
- **`StageController`**: Central controller for managing recording state and operations
- **`PlaybackController`**: Manages scenario replay with timing and playback controls
- **`ScenarioRepository`**: Abstract interface for saving/loading scenarios
- **`FileScenarioRepository`**: File-based implementation for scenario persistence

## Testing the Implementation

### Automated Tests

Run the comprehensive test suite:

```bash
flutter test test/recording/epic1_core_recording_test.dart
```

The test suite covers:
- ✅ Data model serialization/deserialization
- ✅ Recording state management (start/stop/cancel)
- ✅ Frame capture with timestamps
- ✅ Control and canvas state capture
- ✅ File-based scenario persistence
- ✅ Playback controller functionality
- ✅ End-to-end integration scenarios

### Manual Testing Steps

#### 1. Basic Recording Test

```dart
// Create a StageController with file repository
final repository = FileScenarioRepository(defaultDirectory: './test_scenarios');
final stageController = StageController(scenarioRepository: repository);

// Create some controls to record
final controls = <ValueControl>[
StringControl(label: 'title', initialValue: 'Hello'),
IntControl(label: 'count', initialValue: 1),
ColorControl(label: 'color', initialValue: Colors.blue),
];

// Create a canvas controller
final canvasController = StageCanvasController();

// Start recording
stageController.startRecording(controls, canvasController);
print('Recording started: ${stageController.isRecording}'); // Should print: true

// Simulate state changes and frame captures
controls[0].value = 'World';
controls[1].value = 5;
canvasController.zoomFactor = 1.5;
stageController.captureFrame([]); // Capture frame with empty drawing calls

// Wait a bit and capture another frame
await Future.delayed(Duration(milliseconds: 100));
controls[2].value = Colors.red;
canvasController.showRuler = true;
stageController.captureFrame([]);

// Stop recording
stageController.stopRecording();
print('Recording stopped: ${stageController.isRecording}'); // Should print: false

// Create and inspect the scenario
final scenario = stageController.createScenario(
name: 'Manual Test Scenario',
metadata: {'created': DateTime.now().toIso8601String()},
);

print('Scenario frames: ${scenario.frames.length}'); // Should show 2 frames
print('Total duration: ${scenario.totalDuration}'); // Should show ~100ms
```

#### 2. Persistence Test

```dart
// Save the scenario
await stageController.saveScenario(scenario);
print('Scenario saved to file');

// Load it back
final repository = FileScenarioRepository(defaultDirectory: './test_scenarios');
final loadedScenario = await repository.loadScenarioFromFile('path/to/saved/file.json');

print('Loaded scenario: ${loadedScenario.name}'); // Should match saved name
print('Loaded frames: ${loadedScenario.frames.length}'); // Should match frame count
```

#### 3. Playback Test

```dart
// Reset controls to initial state
controls[0].value = 'Initial';
controls[1].value = 0;
controls[2].value = Colors.grey;
canvasController.zoomFactor = 1.0;
canvasController.showRuler = false;

// Create playback controller
final playbackController = PlaybackController();

// Start playback
playbackController.playScenario(
loadedScenario,
controls: controls,
canvasController: canvasController,
);

print('Playback started: ${playbackController.isPlaying}'); // Should print: true

// Controls should now reflect the first frame's values
print('Title after playback: ${controls[0].value}'); // Should show "World"
print('Count after playback: ${controls[1].value}'); // Should show 5
print('Zoom after playback: ${canvasController.zoomFactor}'); // Should show 1.5

// Test playback controls
playbackController.pause();
print('Playback paused: ${playbackController.isPaused}'); // Should print: true

playbackController.resume(controls, canvasController);
print('Playback resumed: ${playbackController.isPaused}'); // Should print: false

// Clean up
playbackController.dispose();
canvasController.dispose();
```

### Expected Behavior

#### Recording Phase
1. **State Tracking**: Recording captures control values and canvas settings at the moment `captureFrame()` is called
2. **Timestamps**: Each frame gets a timestamp relative to when recording started
3. **Frame Structure**: Each frame contains complete state snapshot + drawing calls list
4. **Memory**: Frames are stored in memory until recording is stopped

#### Persistence Phase
1. **JSON Format**: Scenarios are saved as readable JSON files
2. **File Naming**: Auto-generated names based on scenario name + timestamp
3. **Directory**: Files saved to specified directory or current working directory
4. **Loading**: Can reconstruct identical scenario objects from saved JSON

#### Playback Phase
1. **Timing**: Frames are applied based on their recorded timestamps
2. **Speed Control**: Playback speed can be adjusted (0.5x, 2x, etc.)
3. **State Application**: Control values and canvas settings are applied to provided objects
4. **Lifecycle**: Supports play/pause/resume/stop operations

### Validation Criteria

✅ **Recording works** if:
- `isRecording` flag toggles correctly
- `recordingDuration` increases while recording
- Captured frames contain correct control values
- Canvas settings are properly captured
- Timestamps are sequential and reasonable

✅ **Persistence works** if:
- Scenarios save without errors
- Generated JSON files are valid and readable
- Loaded scenarios match original data exactly
- File names follow expected pattern

✅ **Playback works** if:
- Controls update to match recorded values
- Canvas settings apply correctly
- Timing respects original timestamps
- Playback controls (pause/resume/stop) function properly
- Multiple scenarios can be played back

### Integration Points

This Epic 1 implementation provides the foundation for:
- **Epic 2**: UI components will use `StageController` for recording controls
- **Epic 3**: `DrawingCall` objects will be populated by recording canvas interceptor
- **Testing**: Scenarios can be used for automated regression testing

The system is designed to be render-cycle based - call `captureFrame()` whenever the widget redraws to capture both state and visual changes together.
65 changes: 65 additions & 0 deletions RECORDING_SYSTEM_REQUIREMENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
Epic 1: Core Recording & Playback Engine
As a developer, I need a reliable way to record, save, load, and replay the state of my widget and its controls so that I can reproduce bugs and specific configurations.

User Story 1.1: Record State Changes
Task 1.1.1: Add isRecording flag and recordingDuration property to StageController.

Task 1.1.2: Implement startRecording, stopRecording, and cancelRecording methods on StageController.

Task 1.1.3: Create a ListenableGroup that aggregates all ValueControl instances provided to the StageBuilder. This group will notify a single listener of any change in any control.

Task 1.1.4: In startRecording, attach a single listener to the ListenableGroup and the StageCanvasController.

Task 1.1.5: On any listener event, capture the complete state of all controls and canvas settings into a new ScenarioFrame object. This frame must include a Duration timestamp indicating the time since the recording started.

Task 1.1.6: In stopRecording, detach all listeners from the ListenableGroup and StageCanvasController.

User Story 1.2: Persist and Manage Scenarios via a Service
Task 1.2.1: Define an abstract class ScenarioService with methods Future<void> saveScenario(TestScenario scenario) and Future<TestScenario> loadScenario().

Task 1.2.2: Create a concrete implementation FileScenarioService that uses a file picker for saving and loading. This keeps the persistence logic separate from the controller.

Task 1.2.3: The StageController will now hold a reference to a ScenarioService instance.

Task 1.2.4: The saveScenario and loadScenario methods on the StageController will now delegate their calls to the ScenarioService.

User Story 1.3: Replay a Scenario with Timed Playback
Task 1.3.1: Create a new PlaybackController to manage the state of playback (e.g., isPlaying, isPaused, current frame index, playback speed).

Task 1.3.2: Implement a playScenario(TestScenario scenario) method on the StageController that initializes and starts the PlaybackController.

Task 1.3.3: The PlaybackController will use the timestamp on each ScenarioFrame to drive a Timer or Ticker, applying each frame's state to the StageController and ValueControls at the correct time.

Epic 2: UI & In-Stage Development Workflow
As a developer, I need clear and intuitive UI elements to manage recordings and scenarios, and these features must integrate seamlessly with the existing "in-stage" workflow.

User Story 2.1: Control Recording from a Toolbar
Task 2.1.1: Implement a RecordingToolbar widget to replace the simple FAB.

Task 2.1.2: The toolbar should contain IconButtons for "Record," "Stop," "Play," and "Save," each with a descriptive tooltip.

Task 2.1.3: The state and onPressed callbacks of the toolbar buttons will be bound to the StageController and PlaybackController.

User Story 2.2: Manage Scenarios from an Integrated UI
Task 2.2.1: Implement a ScenarioManagementDrawer that can be opened from the side of the stage.

Task 2.2.2: The drawer will contain "Save Scenario" and "Load Scenario" buttons that call the StageController methods.

Task 2.2.3: (Future Enhancement) This drawer can later be expanded to show a list of recently loaded scenarios or include an integrated JSON viewer/editor.

Epic 3: Advanced Testing & Verification
As a developer, I want to capture the precise visual output of my widget and generate automated tests from my recordings to build a robust regression suite.

User Story 3.1: Record Visual Drawing Calls
Task 3.1.1: Create a RecordingCanvas class that implements the complete dart:ui.Canvas interface.

Task 3.1.2: Each method implementation will serialize its name and arguments into a DrawingCall object and add it to the current frame's list.

Task 3.1.3: Update the ScenarioFrame data model to include List<DrawingCall> drawingCalls.

User Story 3.2: Generate Versatile Test Files
Task 3.2.1: Create a TestGenerator service that takes a TestScenario and outputs a test file string.

Task 3.2.2: The generator must support creating assertions for both DrawingCall comparison and, optionally, matchesGoldenFile for specific frames.

Task 3.2.3: Implement a "Generate Test" button in the ScenarioManagementDrawer that uses this service.
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,10 @@
public final class GeneratedPluginRegistrant {
private static final String TAG = "GeneratedPluginRegistrant";
public static void registerWith(@NonNull FlutterEngine flutterEngine) {
try {
flutterEngine.getPlugins().add(new io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin());
} catch (Exception e) {
Log.e(TAG, "Error registering plugin shared_preferences_android, io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin", e);
}
}
}
7 changes: 7 additions & 0 deletions example/ios/Runner/GeneratedPluginRegistrant.m
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,16 @@

#import "GeneratedPluginRegistrant.h"

#if __has_include(<shared_preferences_foundation/SharedPreferencesPlugin.h>)
#import <shared_preferences_foundation/SharedPreferencesPlugin.h>
#else
@import shared_preferences_foundation;
#endif

@implementation GeneratedPluginRegistrant

+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
[SharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferencesPlugin"]];
}

@end
Loading