Compare commits
3 Commits
25d570c779
...
feat/mosai
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7bd109b728 | ||
|
|
1a21bc18bb | ||
|
|
fd72d53d2a |
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Scope: Agentische, statische QA-Review (Flutter/Dart CLI lokal nicht verfügbar: `flutter` fehlt). Fokus auf 4-Step-Flow, Save/Load/Delete, JSON-Export und UX-Kantenfälle.
|
Scope: Agentische, statische QA-Review (Flutter/Dart CLI lokal nicht verfügbar: `flutter` fehlt). Fokus auf 4-Step-Flow, Save/Load/Delete, JSON-Export und UX-Kantenfälle.
|
||||||
|
|
||||||
## Issue 1 (P1) – 4-Step-Flow lässt Fortschritt ohne Pflichtdaten zu
|
## Issue 1 (P1) – 4-Step-Flow lässt Fortschritt ohne Pflichtdaten zu ✅ ERLEDIGT (feat/mosaic-stepper-export)
|
||||||
**Bereich:** 4-Step-Flow
|
**Bereich:** 4-Step-Flow
|
||||||
|
|
||||||
**Beobachtung**
|
**Beobachtung**
|
||||||
@@ -27,7 +27,7 @@ Scope: Agentische, statische QA-Review (Flutter/Dart CLI lokal nicht verfügbar:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Issue 2 (P1) – Projekt-Load mit leerem Bild leert bestehenden Zustand nicht
|
## Issue 2 (P1) – Projekt-Load mit leerem Bild leert bestehenden Zustand nicht ✅ ERLEDIGT (feat/mosaic-stepper-export)
|
||||||
**Bereich:** Save/Load-Flow
|
**Bereich:** Save/Load-Flow
|
||||||
|
|
||||||
**Beobachtung**
|
**Beobachtung**
|
||||||
@@ -49,7 +49,7 @@ Scope: Agentische, statische QA-Review (Flutter/Dart CLI lokal nicht verfügbar:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Issue 3 (P1) – Projekt-Snapshots sind nicht reproduzierbar, da Katalog nicht versioniert wird
|
## Issue 3 (P1) – Projekt-Snapshots sind nicht reproduzierbar, da Katalog nicht versioniert wird ✅ ERLEDIGT (feat/mosaic-stepper-export)
|
||||||
**Bereich:** Save/Load-Flow + JSON-Export
|
**Bereich:** Save/Load-Flow + JSON-Export
|
||||||
|
|
||||||
**Beobachtung**
|
**Beobachtung**
|
||||||
|
|||||||
@@ -205,6 +205,31 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
|||||||
return File('${projectsDir.path}/latest_project.json');
|
return File('${projectsDir.path}/latest_project.json');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get _hasSourceImage => _sourceImageBytes != null;
|
||||||
|
|
||||||
|
bool get _canGenerate => _hasSourceImage && !_isGenerating;
|
||||||
|
|
||||||
|
int get _maxAccessibleStepIndex =>
|
||||||
|
_hasSourceImage ? MosaicFlowStep.result.index : MosaicFlowStep.image.index;
|
||||||
|
|
||||||
|
List<MosaicPaletteSnapshotEntry> _catalogSnapshotFromCurrentCatalog() {
|
||||||
|
return _catalog
|
||||||
|
.map((entry) => MosaicPaletteSnapshotEntry(
|
||||||
|
name: entry.name,
|
||||||
|
colorValue: entry.colorValue,
|
||||||
|
))
|
||||||
|
.toList(growable: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showMissingImageHint() {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Bitte zuerst ein Bild auswählen.'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
MosaicProjectData _buildProjectData() {
|
MosaicProjectData _buildProjectData() {
|
||||||
return MosaicProjectData(
|
return MosaicProjectData(
|
||||||
useCapSize: _useCapSize,
|
useCapSize: _useCapSize,
|
||||||
@@ -217,6 +242,7 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
|||||||
colorVariation: _colorVariation,
|
colorVariation: _colorVariation,
|
||||||
selectedPreset: _selectedPreset.name,
|
selectedPreset: _selectedPreset.name,
|
||||||
sourceImageBytes: _sourceImageBytes,
|
sourceImageBytes: _sourceImageBytes,
|
||||||
|
catalogSnapshot: _catalogSnapshotFromCurrentCatalog(),
|
||||||
savedAt: DateTime.now(),
|
savedAt: DateTime.now(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -288,6 +314,7 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
|||||||
final data = MosaicProjectData.fromJson(
|
final data = MosaicProjectData.fromJson(
|
||||||
jsonDecode(await file.readAsString()) as Map<String, dynamic>,
|
jsonDecode(await file.readAsString()) as Map<String, dynamic>,
|
||||||
);
|
);
|
||||||
|
final hasCatalogSnapshot = data.catalogSnapshot.isNotEmpty;
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -305,18 +332,22 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
|||||||
orElse: () => StylePreset.ausgewogen,
|
orElse: () => StylePreset.ausgewogen,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (data.sourceImageBytes != null) {
|
|
||||||
_sourceImageBytes = data.sourceImageBytes;
|
_sourceImageBytes = data.sourceImageBytes;
|
||||||
_result = null;
|
_result = null;
|
||||||
_currentFlowStep = MosaicFlowStep.result;
|
_currentFlowStep =
|
||||||
}
|
_sourceImageBytes == null ? MosaicFlowStep.image : MosaicFlowStep.result;
|
||||||
_activeSection = HomeSection.mosaic;
|
_activeSection = HomeSection.mosaic;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.sourceImageBytes != null) await _generate();
|
if (data.sourceImageBytes != null) {
|
||||||
|
await _generate(catalogSnapshotOverride: hasCatalogSnapshot ? data.catalogSnapshot : null);
|
||||||
|
}
|
||||||
if (!silent && mounted) {
|
if (!silent && mounted) {
|
||||||
|
final message = hasCatalogSnapshot
|
||||||
|
? 'Projekt geladen ✅ (mit gespeichertem Katalog-Snapshot)'
|
||||||
|
: 'Projekt geladen ✅ (ohne Snapshot: aktueller Katalog aktiv)';
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Projekt geladen ✅')),
|
SnackBar(content: Text(message)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
@@ -879,8 +910,24 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
|||||||
_scheduleRegenerate();
|
_scheduleRegenerate();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _generate() async {
|
Future<void> _generate({List<MosaicPaletteSnapshotEntry>? catalogSnapshotOverride}) async {
|
||||||
if (_sourceImageBytes == null || _catalog.isEmpty) return;
|
if (_sourceImageBytes == null) {
|
||||||
|
_showMissingImageHint();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final paletteSource = catalogSnapshotOverride != null && catalogSnapshotOverride.isNotEmpty
|
||||||
|
? catalogSnapshotOverride
|
||||||
|
.map((entry) => <String, dynamic>{
|
||||||
|
'name': entry.name,
|
||||||
|
'value': entry.colorValue,
|
||||||
|
})
|
||||||
|
.toList(growable: false)
|
||||||
|
: _catalog
|
||||||
|
.map((p) => <String, dynamic>{'name': p.name, 'value': p.colorValue})
|
||||||
|
.toList(growable: false);
|
||||||
|
|
||||||
|
if (paletteSource.isEmpty) return;
|
||||||
|
|
||||||
final int gridW = math.max(1, int.tryParse(_gridWidthCtrl.text) ?? 40);
|
final int gridW = math.max(1, int.tryParse(_gridWidthCtrl.text) ?? 40);
|
||||||
final int gridH = math.max(1, int.tryParse(_gridHeightCtrl.text) ?? 30);
|
final int gridH = math.max(1, int.tryParse(_gridHeightCtrl.text) ?? 30);
|
||||||
@@ -899,9 +946,7 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
|||||||
'ditheringStrength': _ditheringStrength,
|
'ditheringStrength': _ditheringStrength,
|
||||||
'edgeEmphasis': _edgeEmphasis,
|
'edgeEmphasis': _edgeEmphasis,
|
||||||
'colorVariation': _colorVariation,
|
'colorVariation': _colorVariation,
|
||||||
'palette': _catalog
|
'palette': paletteSource,
|
||||||
.map((p) => <String, dynamic>{'name': p.name, 'value': p.colorValue})
|
|
||||||
.toList(growable: false),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
final out = await compute(_generateMosaicIsolate, payload);
|
final out = await compute(_generateMosaicIsolate, payload);
|
||||||
@@ -952,7 +997,7 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
|||||||
? FloatingActionButton.extended(
|
? FloatingActionButton.extended(
|
||||||
backgroundColor: Colors.white.withValues(alpha: 0.85),
|
backgroundColor: Colors.white.withValues(alpha: 0.85),
|
||||||
foregroundColor: Theme.of(context).colorScheme.primary,
|
foregroundColor: Theme.of(context).colorScheme.primary,
|
||||||
onPressed: _isGenerating ? null : _generate,
|
onPressed: _canGenerate ? _generate : null,
|
||||||
icon: _isGenerating
|
icon: _isGenerating
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
width: 18,
|
width: 18,
|
||||||
@@ -1085,8 +1130,12 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
onStepContinue: () {
|
onStepContinue: () {
|
||||||
setState(() {
|
|
||||||
final next = _currentFlowStep.index + 1;
|
final next = _currentFlowStep.index + 1;
|
||||||
|
if (next > _maxAccessibleStepIndex) {
|
||||||
|
_showMissingImageHint();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
if (next <= MosaicFlowStep.result.index) {
|
if (next <= MosaicFlowStep.result.index) {
|
||||||
_currentFlowStep = MosaicFlowStep.values[next];
|
_currentFlowStep = MosaicFlowStep.values[next];
|
||||||
}
|
}
|
||||||
@@ -1101,6 +1150,10 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
onStepTapped: (index) {
|
onStepTapped: (index) {
|
||||||
|
if (index > _maxAccessibleStepIndex) {
|
||||||
|
_showMissingImageHint();
|
||||||
|
return;
|
||||||
|
}
|
||||||
setState(() => _currentFlowStep = MosaicFlowStep.values[index]);
|
setState(() => _currentFlowStep = MosaicFlowStep.values[index]);
|
||||||
},
|
},
|
||||||
steps: [
|
steps: [
|
||||||
@@ -1250,7 +1303,7 @@ class _MosaicHomePageState extends State<MosaicHomePage>
|
|||||||
children: [
|
children: [
|
||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
key: const Key('generate-btn'),
|
key: const Key('generate-btn'),
|
||||||
onPressed: _isGenerating ? null : _generate,
|
onPressed: _canGenerate ? _generate : null,
|
||||||
icon: const Icon(Icons.auto_fix_high_rounded),
|
icon: const Icon(Icons.auto_fix_high_rounded),
|
||||||
label: const Text('Generate Mosaic'),
|
label: const Text('Generate Mosaic'),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,6 +1,28 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
class MosaicPaletteSnapshotEntry {
|
||||||
|
final String name;
|
||||||
|
final int colorValue;
|
||||||
|
|
||||||
|
const MosaicPaletteSnapshotEntry({
|
||||||
|
required this.name,
|
||||||
|
required this.colorValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'name': name,
|
||||||
|
'colorValue': colorValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
factory MosaicPaletteSnapshotEntry.fromJson(Map<String, dynamic> json) {
|
||||||
|
return MosaicPaletteSnapshotEntry(
|
||||||
|
name: json['name'] as String? ?? 'Unbenannt',
|
||||||
|
colorValue: (json['colorValue'] as num?)?.toInt() ?? 0xFF000000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class MosaicProjectData {
|
class MosaicProjectData {
|
||||||
final bool useCapSize;
|
final bool useCapSize;
|
||||||
final String gridWidth;
|
final String gridWidth;
|
||||||
@@ -12,6 +34,7 @@ class MosaicProjectData {
|
|||||||
final double colorVariation;
|
final double colorVariation;
|
||||||
final String selectedPreset;
|
final String selectedPreset;
|
||||||
final Uint8List? sourceImageBytes;
|
final Uint8List? sourceImageBytes;
|
||||||
|
final List<MosaicPaletteSnapshotEntry> catalogSnapshot;
|
||||||
final DateTime savedAt;
|
final DateTime savedAt;
|
||||||
|
|
||||||
const MosaicProjectData({
|
const MosaicProjectData({
|
||||||
@@ -25,6 +48,7 @@ class MosaicProjectData {
|
|||||||
required this.colorVariation,
|
required this.colorVariation,
|
||||||
required this.selectedPreset,
|
required this.selectedPreset,
|
||||||
required this.sourceImageBytes,
|
required this.sourceImageBytes,
|
||||||
|
required this.catalogSnapshot,
|
||||||
required this.savedAt,
|
required this.savedAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -40,23 +64,32 @@ class MosaicProjectData {
|
|||||||
'selectedPreset': selectedPreset,
|
'selectedPreset': selectedPreset,
|
||||||
'sourceImageBase64':
|
'sourceImageBase64':
|
||||||
sourceImageBytes == null ? null : base64Encode(sourceImageBytes!),
|
sourceImageBytes == null ? null : base64Encode(sourceImageBytes!),
|
||||||
|
'catalogSnapshot': catalogSnapshot.map((entry) => entry.toJson()).toList(),
|
||||||
'savedAt': savedAt.toIso8601String(),
|
'savedAt': savedAt.toIso8601String(),
|
||||||
};
|
};
|
||||||
|
|
||||||
factory MosaicProjectData.fromJson(Map<String, dynamic> json) {
|
factory MosaicProjectData.fromJson(Map<String, dynamic> json) {
|
||||||
final sourceB64 = json['sourceImageBase64'] as String?;
|
final sourceB64 = json['sourceImageBase64'] as String?;
|
||||||
|
final snapshotRaw = (json['catalogSnapshot'] as List?) ?? const [];
|
||||||
return MosaicProjectData(
|
return MosaicProjectData(
|
||||||
useCapSize: json['useCapSize'] as bool? ?? false,
|
useCapSize: json['useCapSize'] as bool? ?? false,
|
||||||
gridWidth: json['gridWidth'] as String? ?? '40',
|
gridWidth: json['gridWidth'] as String? ?? '40',
|
||||||
gridHeight: json['gridHeight'] as String? ?? '30',
|
gridHeight: json['gridHeight'] as String? ?? '30',
|
||||||
capSize: json['capSize'] as String? ?? '12',
|
capSize: json['capSize'] as String? ?? '12',
|
||||||
fidelityStructure: (json['fidelityStructure'] as num?)?.toDouble() ?? 0.5,
|
fidelityStructure: (json['fidelityStructure'] as num?)?.toDouble() ?? 0.5,
|
||||||
ditheringStrength: (json['ditheringStrength'] as num?)?.toDouble() ?? 0.35,
|
ditheringStrength:
|
||||||
|
(json['ditheringStrength'] as num?)?.toDouble() ?? 0.35,
|
||||||
edgeEmphasis: (json['edgeEmphasis'] as num?)?.toDouble() ?? 0.4,
|
edgeEmphasis: (json['edgeEmphasis'] as num?)?.toDouble() ?? 0.4,
|
||||||
colorVariation: (json['colorVariation'] as num?)?.toDouble() ?? 0.3,
|
colorVariation: (json['colorVariation'] as num?)?.toDouble() ?? 0.3,
|
||||||
selectedPreset: json['selectedPreset'] as String? ?? 'ausgewogen',
|
selectedPreset: json['selectedPreset'] as String? ?? 'ausgewogen',
|
||||||
sourceImageBytes: sourceB64 == null ? null : base64Decode(sourceB64),
|
sourceImageBytes: sourceB64 == null ? null : base64Decode(sourceB64),
|
||||||
savedAt: DateTime.tryParse(json['savedAt'] as String? ?? '') ?? DateTime.now(),
|
catalogSnapshot: snapshotRaw
|
||||||
|
.whereType<Map>()
|
||||||
|
.map((entry) => MosaicPaletteSnapshotEntry.fromJson(
|
||||||
|
Map<String, dynamic>.from(entry)))
|
||||||
|
.toList(growable: false),
|
||||||
|
savedAt:
|
||||||
|
DateTime.tryParse(json['savedAt'] as String? ?? '') ?? DateTime.now(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
import 'package:korken_mosaic/project_codec.dart';
|
import 'package:korken_mosaic/project_codec.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
test('MosaicProjectData json roundtrip keeps values', () {
|
test('MosaicProjectData json roundtrip keeps values including catalog snapshot', () {
|
||||||
final original = MosaicProjectData(
|
final original = MosaicProjectData(
|
||||||
useCapSize: true,
|
useCapSize: true,
|
||||||
gridWidth: '50',
|
gridWidth: '50',
|
||||||
@@ -16,6 +16,10 @@ void main() {
|
|||||||
colorVariation: 0.5,
|
colorVariation: 0.5,
|
||||||
selectedPreset: 'realistisch',
|
selectedPreset: 'realistisch',
|
||||||
sourceImageBytes: Uint8List.fromList([1, 2, 3]),
|
sourceImageBytes: Uint8List.fromList([1, 2, 3]),
|
||||||
|
catalogSnapshot: const [
|
||||||
|
MosaicPaletteSnapshotEntry(name: 'White', colorValue: 0xFFF2F2F2),
|
||||||
|
MosaicPaletteSnapshotEntry(name: 'Blue', colorValue: 0xFF3F6FD8),
|
||||||
|
],
|
||||||
savedAt: DateTime.parse('2026-01-01T12:00:00Z'),
|
savedAt: DateTime.parse('2026-01-01T12:00:00Z'),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -28,5 +32,22 @@ void main() {
|
|||||||
expect(decoded.selectedPreset, 'realistisch');
|
expect(decoded.selectedPreset, 'realistisch');
|
||||||
expect(decoded.sourceImageBytes, isNotNull);
|
expect(decoded.sourceImageBytes, isNotNull);
|
||||||
expect(decoded.sourceImageBytes!, [1, 2, 3]);
|
expect(decoded.sourceImageBytes!, [1, 2, 3]);
|
||||||
|
expect(decoded.catalogSnapshot.length, 2);
|
||||||
|
expect(decoded.catalogSnapshot.first.name, 'White');
|
||||||
|
expect(decoded.catalogSnapshot.last.colorValue, 0xFF3F6FD8);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('MosaicProjectData defaults to empty snapshot when old project has none', () {
|
||||||
|
final decoded = MosaicProjectData.fromJson({
|
||||||
|
'useCapSize': false,
|
||||||
|
'gridWidth': '40',
|
||||||
|
'gridHeight': '30',
|
||||||
|
'capSize': '12',
|
||||||
|
'selectedPreset': 'ausgewogen',
|
||||||
|
'savedAt': '2026-01-01T12:00:00Z',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(decoded.catalogSnapshot, isEmpty);
|
||||||
|
expect(decoded.sourceImageBytes, isNull);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:korken_mosaic/main.dart';
|
import 'package:korken_mosaic/main.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('shows 4-step workflow and can navigate to result step',
|
testWidgets('stepper blocks forward navigation without image and shows hint',
|
||||||
(WidgetTester tester) async {
|
(WidgetTester tester) async {
|
||||||
await tester.pumpWidget(const KorkenMosaicApp());
|
await tester.pumpWidget(const KorkenMosaicApp());
|
||||||
|
|
||||||
@@ -12,13 +13,23 @@ void main() {
|
|||||||
expect(find.text('4) Ergebnis'), findsOneWidget);
|
expect(find.text('4) Ergebnis'), findsOneWidget);
|
||||||
|
|
||||||
await tester.tap(find.text('Weiter'));
|
await tester.tap(find.text('Weiter'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pump();
|
||||||
await tester.tap(find.text('Weiter'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
await tester.tap(find.text('Weiter'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
expect(find.byKey(const Key('generate-btn')), findsOneWidget);
|
// Stays on step 1 because no source image is available yet.
|
||||||
expect(find.text('Export JSON'), findsOneWidget);
|
expect(find.text('Import target image'), findsOneWidget);
|
||||||
|
expect(find.text('Bitte zuerst ein Bild auswählen.'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('generate actions are disabled without image',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(const KorkenMosaicApp());
|
||||||
|
|
||||||
|
final fab = tester.widget<FloatingActionButton>(
|
||||||
|
find.byType(FloatingActionButton),
|
||||||
|
);
|
||||||
|
expect(fab.onPressed, isNull);
|
||||||
|
|
||||||
|
// Result-step action is not reachable before image selection.
|
||||||
|
expect(find.byKey(const Key('generate-btn')), findsNothing);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user