Compare commits
10 Commits
build-2026
...
feat/mosai
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7bd109b728 | ||
|
|
1a21bc18bb | ||
|
|
fd72d53d2a | ||
|
|
25d570c779 | ||
|
|
df7406b494 | ||
|
|
ab7aa625e6 | ||
|
|
814705cac6 | ||
|
|
3651b073bf | ||
|
|
b553c29d39 | ||
|
|
84d649ac6d |
136
docs/qa/issues-20260224.md
Normal file
136
docs/qa/issues-20260224.md
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
# QA Issues – 2026-02-24
|
||||||
|
|
||||||
|
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 ✅ ERLEDIGT (feat/mosaic-stepper-export)
|
||||||
|
**Bereich:** 4-Step-Flow
|
||||||
|
|
||||||
|
**Beobachtung**
|
||||||
|
- Im `Stepper` kann man per `Weiter`/Tap bis Schritt 4 springen, auch ohne Bildauswahl.
|
||||||
|
- Auf Schritt 4 ist „Generate Mosaic“ aktiv, aber `_generate()` bricht stillschweigend ab, wenn kein Bild vorhanden ist.
|
||||||
|
|
||||||
|
**Code-Hinweis**
|
||||||
|
- `lib/main.dart:1068-1127` (`Stepper` ohne Guard/Validation)
|
||||||
|
- `lib/main.dart:1250-1255` (Generate-Button nur an `_isGenerating` gebunden)
|
||||||
|
- `lib/main.dart:884`ff (`_generate()` returnt bei `_sourceImageBytes == null` ohne User-Feedback)
|
||||||
|
|
||||||
|
**Repro-Schritte**
|
||||||
|
1. App öffnen (kein Bild geladen).
|
||||||
|
2. Im Mosaic-Stepper mehrfach „Weiter“ klicken oder direkt auf Schritt 4 tippen.
|
||||||
|
3. „Generate Mosaic“ klicken.
|
||||||
|
4. Es passiert visuell nichts (kein Ergebnis, kein Hinweis).
|
||||||
|
|
||||||
|
**Akzeptanzkriterium**
|
||||||
|
- Schritt-Navigation ist zustandsbasiert: Schritt 2-4 erst zugänglich, wenn notwendige Voraussetzungen erfüllt sind.
|
||||||
|
- „Generate Mosaic“ ist deaktiviert, solange kein Bild geladen ist.
|
||||||
|
- Alternativ/zusätzlich: verständliche Fehlermeldung (Snackbar), falls Generierung ohne Bild ausgelöst wird.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue 2 (P1) – Projekt-Load mit leerem Bild leert bestehenden Zustand nicht ✅ ERLEDIGT (feat/mosaic-stepper-export)
|
||||||
|
**Bereich:** Save/Load-Flow
|
||||||
|
|
||||||
|
**Beobachtung**
|
||||||
|
- Beim Laden werden `_sourceImageBytes` und `_result` nur gesetzt, wenn `data.sourceImageBytes != null`.
|
||||||
|
- Lädt man ein Projekt ohne Bild, bleibt ggf. ein altes Bild/Ergebnis aus vorherigem Zustand erhalten.
|
||||||
|
|
||||||
|
**Code-Hinweis**
|
||||||
|
- `lib/main.dart:308-313` (fehlender `else`-Zweig zum expliziten Zurücksetzen)
|
||||||
|
|
||||||
|
**Repro-Schritte**
|
||||||
|
1. Projekt A mit Bild laden/generieren.
|
||||||
|
2. Projekt B laden, das ohne Bild gespeichert wurde (oder manuell erstellt wurde).
|
||||||
|
3. Erwartung: leerer/initialer Zustand.
|
||||||
|
4. Ist-Zustand: vorheriges Bild kann bestehen bleiben.
|
||||||
|
|
||||||
|
**Akzeptanzkriterium**
|
||||||
|
- Beim Laden eines Projekts ohne `sourceImageBytes` wird Zustand explizit zurückgesetzt (`_sourceImageBytes = null`, `_result = null`, ggf. Step auf 1).
|
||||||
|
- UI zeigt konsistent den tatsächlich geladenen Projektzustand.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue 3 (P1) – Projekt-Snapshots sind nicht reproduzierbar, da Katalog nicht versioniert wird ✅ ERLEDIGT (feat/mosaic-stepper-export)
|
||||||
|
**Bereich:** Save/Load-Flow + JSON-Export
|
||||||
|
|
||||||
|
**Beobachtung**
|
||||||
|
- Gespeicherte Projekte enthalten Parameter + Bild, aber **keinen Snapshot des verwendeten Farb-Katalogs**.
|
||||||
|
- Beim Laden wird mit aktuellem globalen `_catalog` neu generiert → Ergebnis kann von ursprünglichem Snapshot abweichen.
|
||||||
|
|
||||||
|
**Code-Hinweis**
|
||||||
|
- `lib/project_codec.dart` (`MosaicProjectData` ohne Katalogdaten)
|
||||||
|
- `lib/main.dart:316` (`_generate()` nach Load nutzt aktuellen `_catalog`)
|
||||||
|
|
||||||
|
**Repro-Schritte**
|
||||||
|
1. Mit Katalog-Set A ein Projekt erzeugen/speichern.
|
||||||
|
2. Katalogfarben ändern (hinzufügen/löschen/umfärben).
|
||||||
|
3. Projekt laden.
|
||||||
|
4. Ergebnisfarben/Zuordnung unterscheiden sich vom ursprünglichen Stand.
|
||||||
|
|
||||||
|
**Akzeptanzkriterium**
|
||||||
|
- Projektspeicherung enthält eine Palette/Katalog-Snapshot-Version (mind. Name + Farbe pro Eintrag).
|
||||||
|
- Load nutzt standardmäßig den gespeicherten Snapshot (mit klarer UX bei Konflikten/Optionen).
|
||||||
|
- Reproduzierbarkeit des Mosaiks ist gewährleistet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue 4 (P2) – Export JSON nicht klar reproduzierbar ohne Ergebnis
|
||||||
|
**Bereich:** JSON-Export
|
||||||
|
|
||||||
|
**Beobachtung**
|
||||||
|
- Export enthält `project` immer, `result` aber nur falls bereits generiert.
|
||||||
|
- Wenn vor Export keine Generierung lief, fehlt Kerninformation (`assignments`/`palette`) ohne klaren Nutzerhinweis.
|
||||||
|
|
||||||
|
**Code-Hinweis**
|
||||||
|
- `lib/main.dart:378-392`
|
||||||
|
|
||||||
|
**Repro-Schritte**
|
||||||
|
1. Bild laden, aber nicht generieren.
|
||||||
|
2. „Export JSON“ klicken.
|
||||||
|
3. Exportdatei enthält kein `result`-Objekt.
|
||||||
|
|
||||||
|
**Akzeptanzkriterium**
|
||||||
|
- UX-Entscheidung explizit umsetzen:
|
||||||
|
- entweder Export-Button nur mit vorhandenem Ergebnis aktivieren, oder
|
||||||
|
- vor Export automatisch generieren, oder
|
||||||
|
- deutlichen Hinweisdialog anzeigen („Export ohne Ergebnisdaten“).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue 5 (P3) – „Fertig“-CTA im letzten Step ohne klaren Effekt
|
||||||
|
**Bereich:** 4-Step-Flow UX
|
||||||
|
|
||||||
|
**Beobachtung**
|
||||||
|
- Im letzten Step zeigt der Continue-Button „Fertig“, führt aber funktional zu keiner sichtbaren Aktion.
|
||||||
|
|
||||||
|
**Code-Hinweis**
|
||||||
|
- `lib/main.dart:1071-1093` (`onStepContinue` erhöht nur bis max. letztem Step)
|
||||||
|
|
||||||
|
**Repro-Schritte**
|
||||||
|
1. Bis Schritt 4 navigieren.
|
||||||
|
2. „Fertig“ klicken.
|
||||||
|
3. Kein Abschluss-Feedback/State-Change.
|
||||||
|
|
||||||
|
**Akzeptanzkriterium**
|
||||||
|
- Letzter CTA hat klare Bedeutung (z. B. „Generieren“, „Abschließen“, „Zum Export“) oder wird im letzten Step ausgeblendet.
|
||||||
|
- Nutzer erhält eindeutiges Abschlussfeedback.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue 6 (P3) – Lösch-Flow ohne Hinweis auf betroffenen Arbeitsstand
|
||||||
|
**Bereich:** Delete-Flow UX
|
||||||
|
|
||||||
|
**Beobachtung**
|
||||||
|
- Beim Löschen eines Snapshots gibt es nur Dateiname + Bestätigung.
|
||||||
|
- Kein Hinweis, ob gerade geladener Stand betroffen ist bzw. wie sich das auf „Letzten Stand laden“ auswirkt.
|
||||||
|
|
||||||
|
**Code-Hinweis**
|
||||||
|
- `lib/main.dart:333-367`
|
||||||
|
|
||||||
|
**Repro-Schritte**
|
||||||
|
1. Snapshot laden.
|
||||||
|
2. In Projekte denselben Snapshot löschen.
|
||||||
|
3. Nutzer bleibt ohne Kontext, ob aktiver Stand/Latest-Verhalten beeinflusst ist.
|
||||||
|
|
||||||
|
**Akzeptanzkriterium**
|
||||||
|
- Dialog/Feedback benennt Auswirkungen klar (z. B. „Aktuell geladener Zustand bleibt im Speicher bis Wechsel/Neustart“).
|
||||||
|
- Optional: Markierung des aktuell geladenen Snapshots in der Liste.
|
||||||
52
docs/review/review-20260224.md
Normal file
52
docs/review/review-20260224.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Code Review – Änderungen von `814705c` bis `HEAD` (`ab7aa62`)
|
||||||
|
|
||||||
|
## 1) High-risk Findings
|
||||||
|
|
||||||
|
### Keine blocker-kritischen Defekte gefunden
|
||||||
|
Ich habe im betrachteten Commit keine eindeutigen Crash-/Datenverlust-Blocker gefunden, die einen sofortigen Release-Stopp erzwingen.
|
||||||
|
|
||||||
|
> Hinweis: Es gibt aber eine **relevante funktionale Regression** (siehe Nitpicks #1), die je nach Produktanforderung als Go/No-Go-Kriterium gewertet werden kann.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2) Nitpicks / Verbesserungen
|
||||||
|
|
||||||
|
1. **Funktionale Regression in der UI (ehemals 4 Style-Regler, jetzt nur noch 1 sichtbar)**
|
||||||
|
In `lib/main.dart` zeigt `_buildColorStep()` nur noch den Slider `fidelityStructure`.
|
||||||
|
Die bisherigen Feineinstellungen `ditheringStrength`, `edgeEmphasis`, `colorVariation` sind weiter im State/Codec vorhanden, aber nicht mehr direkt im UI editierbar.
|
||||||
|
**Impact:** Nutzer verlieren granulare Qualitätskontrolle; gespeicherte Werte bleiben zwar erhalten, sind aber kaum noch aktiv manipulierbar.
|
||||||
|
|
||||||
|
2. **Stepper-Logik ohne Validierung / Guidance**
|
||||||
|
`onStepContinue` lässt das Voranschreiten ohne notwendige Preconditions zu (z. B. ohne Bildauswahl bis Schritt 4). Das ist technisch robust (Generate guarded), aber UX-seitig irreführend.
|
||||||
|
**Vorschlag:** Continue-Button je Schritt konditional deaktivieren oder klare Inline-Hinweise anzeigen.
|
||||||
|
|
||||||
|
3. **`Fertig`-Button im letzten Step ohne echte Aktion**
|
||||||
|
In `controlsBuilder` wird im letzten Schritt der Label-Text auf `Fertig` gesetzt, aber `onStepContinue` führt dort effektiv nichts mehr aus.
|
||||||
|
**Vorschlag:** Auf letzter Stufe Button ausblenden oder mit sinnvoller Aktion belegen (z. B. Export, Speichern, Zur Projektliste).
|
||||||
|
|
||||||
|
4. **Projekt-Ladesemantik setzt immer auf Step `result`**
|
||||||
|
Beim Laden mit Bild springt der Flow direkt auf `MosaicFlowStep.result`. Das ist für „Quick resume“ okay, nimmt aber ggf. den Guided-Flow-Charakter.
|
||||||
|
**Vorschlag:** Optionales Verhalten (z. B. Restore des letzten Steps oder Konfiguration `resumeAtResult`).
|
||||||
|
|
||||||
|
5. **Testabdeckung für neue Kernpfade noch dünn**
|
||||||
|
Es gibt gute Basis-Tests (`project_codec_test`, einfacher Stepper-Smoke-Test), aber es fehlen Tests für:
|
||||||
|
- latest/autosave-Verhalten (`latest_project.json`),
|
||||||
|
- manuell vs. automatisch speichern,
|
||||||
|
- Export-JSON-Struktur inkl. Result-Payload,
|
||||||
|
- Laden aus Snapshot vs. latest.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3) Go/No-Go Empfehlung
|
||||||
|
|
||||||
|
**Empfehlung: GO mit Auflagen (kein Hard No-Go).**
|
||||||
|
|
||||||
|
Begründung:
|
||||||
|
- Die neuen Features (4-Step-Flow, autosave/latest, JSON-Export, Bestätigungsdialog beim Löschen) sind grundsätzlich sinnvoll umgesetzt.
|
||||||
|
- Kein klarer, reproduzierbarer Blocker im Diff erkennbar.
|
||||||
|
- Vor Release sollten jedoch mindestens die UX-/Funktionsregressionspunkte (insb. fehlende 3 Style-Regler) bewusst entschieden/fixiert werden.
|
||||||
|
|
||||||
|
**Release-Auflagen (kurz):**
|
||||||
|
1. Entscheiden/fixen, ob die 3 fehlenden Style-Regler absichtliche Scope-Reduktion oder Regression sind.
|
||||||
|
2. Stepper-UX (Continue/Finish-Verhalten) konsistenter machen.
|
||||||
|
3. 2–3 gezielte Tests für Save/Load/Export-Pfade ergänzen.
|
||||||
755
lib/main.dart
755
lib/main.dart
File diff suppressed because it is too large
Load Diff
95
lib/project_codec.dart
Normal file
95
lib/project_codec.dart
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
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 {
|
||||||
|
final bool useCapSize;
|
||||||
|
final String gridWidth;
|
||||||
|
final String gridHeight;
|
||||||
|
final String capSize;
|
||||||
|
final double fidelityStructure;
|
||||||
|
final double ditheringStrength;
|
||||||
|
final double edgeEmphasis;
|
||||||
|
final double colorVariation;
|
||||||
|
final String selectedPreset;
|
||||||
|
final Uint8List? sourceImageBytes;
|
||||||
|
final List<MosaicPaletteSnapshotEntry> catalogSnapshot;
|
||||||
|
final DateTime savedAt;
|
||||||
|
|
||||||
|
const MosaicProjectData({
|
||||||
|
required this.useCapSize,
|
||||||
|
required this.gridWidth,
|
||||||
|
required this.gridHeight,
|
||||||
|
required this.capSize,
|
||||||
|
required this.fidelityStructure,
|
||||||
|
required this.ditheringStrength,
|
||||||
|
required this.edgeEmphasis,
|
||||||
|
required this.colorVariation,
|
||||||
|
required this.selectedPreset,
|
||||||
|
required this.sourceImageBytes,
|
||||||
|
required this.catalogSnapshot,
|
||||||
|
required this.savedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'useCapSize': useCapSize,
|
||||||
|
'gridWidth': gridWidth,
|
||||||
|
'gridHeight': gridHeight,
|
||||||
|
'capSize': capSize,
|
||||||
|
'fidelityStructure': fidelityStructure,
|
||||||
|
'ditheringStrength': ditheringStrength,
|
||||||
|
'edgeEmphasis': edgeEmphasis,
|
||||||
|
'colorVariation': colorVariation,
|
||||||
|
'selectedPreset': selectedPreset,
|
||||||
|
'sourceImageBase64':
|
||||||
|
sourceImageBytes == null ? null : base64Encode(sourceImageBytes!),
|
||||||
|
'catalogSnapshot': catalogSnapshot.map((entry) => entry.toJson()).toList(),
|
||||||
|
'savedAt': savedAt.toIso8601String(),
|
||||||
|
};
|
||||||
|
|
||||||
|
factory MosaicProjectData.fromJson(Map<String, dynamic> json) {
|
||||||
|
final sourceB64 = json['sourceImageBase64'] as String?;
|
||||||
|
final snapshotRaw = (json['catalogSnapshot'] as List?) ?? const [];
|
||||||
|
return MosaicProjectData(
|
||||||
|
useCapSize: json['useCapSize'] as bool? ?? false,
|
||||||
|
gridWidth: json['gridWidth'] as String? ?? '40',
|
||||||
|
gridHeight: json['gridHeight'] as String? ?? '30',
|
||||||
|
capSize: json['capSize'] as String? ?? '12',
|
||||||
|
fidelityStructure: (json['fidelityStructure'] as num?)?.toDouble() ?? 0.5,
|
||||||
|
ditheringStrength:
|
||||||
|
(json['ditheringStrength'] as num?)?.toDouble() ?? 0.35,
|
||||||
|
edgeEmphasis: (json['edgeEmphasis'] as num?)?.toDouble() ?? 0.4,
|
||||||
|
colorVariation: (json['colorVariation'] as num?)?.toDouble() ?? 0.3,
|
||||||
|
selectedPreset: json['selectedPreset'] as String? ?? 'ausgewogen',
|
||||||
|
sourceImageBytes: sourceB64 == null ? null : base64Decode(sourceB64),
|
||||||
|
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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
53
test/project_codec_test.dart
Normal file
53
test/project_codec_test.dart
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:korken_mosaic/project_codec.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('MosaicProjectData json roundtrip keeps values including catalog snapshot', () {
|
||||||
|
final original = MosaicProjectData(
|
||||||
|
useCapSize: true,
|
||||||
|
gridWidth: '50',
|
||||||
|
gridHeight: '40',
|
||||||
|
capSize: '10',
|
||||||
|
fidelityStructure: 0.3,
|
||||||
|
ditheringStrength: 0.2,
|
||||||
|
edgeEmphasis: 0.4,
|
||||||
|
colorVariation: 0.5,
|
||||||
|
selectedPreset: 'realistisch',
|
||||||
|
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'),
|
||||||
|
);
|
||||||
|
|
||||||
|
final decoded = MosaicProjectData.fromJson(original.toJson());
|
||||||
|
|
||||||
|
expect(decoded.useCapSize, isTrue);
|
||||||
|
expect(decoded.gridWidth, '50');
|
||||||
|
expect(decoded.gridHeight, '40');
|
||||||
|
expect(decoded.capSize, '10');
|
||||||
|
expect(decoded.selectedPreset, 'realistisch');
|
||||||
|
expect(decoded.sourceImageBytes, isNotNull);
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
35
test/widget_test.dart
Normal file
35
test/widget_test.dart
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:korken_mosaic/main.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('stepper blocks forward navigation without image and shows hint',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(const KorkenMosaicApp());
|
||||||
|
|
||||||
|
expect(find.text('1) Bild'), findsOneWidget);
|
||||||
|
expect(find.text('2) Größe'), findsOneWidget);
|
||||||
|
expect(find.text('3) Farben'), findsOneWidget);
|
||||||
|
expect(find.text('4) Ergebnis'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Weiter'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Stays on step 1 because no source image is available yet.
|
||||||
|
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