mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Conductor UI step1 (#91903)
* finished dropdown widget * removed mock state * Added step 1 substeps * stepper tests fail * tests still fail * removed redundant checkbox * removed test data * full fixes based on Chris' comments * changed widget's name * changed statefulwidget, added test * fixes based on Casey's comments * empty commit to kick pending checks
This commit is contained in:
parent
cf443a7eae
commit
a6e91c368a
@ -18,7 +18,7 @@ class ConductorStatus extends StatefulWidget {
|
||||
final String stateFilePath;
|
||||
|
||||
@override
|
||||
ConductorStatusState createState() => ConductorStatusState();
|
||||
State<ConductorStatus> createState() => ConductorStatusState();
|
||||
|
||||
static final List<String> headerElements = <String>[
|
||||
'Conductor Version',
|
||||
@ -194,7 +194,7 @@ class CherrypickTable extends StatefulWidget {
|
||||
final Map<String, Object> currentStatus;
|
||||
|
||||
@override
|
||||
CherrypickTableState createState() => CherrypickTableState();
|
||||
State<CherrypickTable> createState() => CherrypickTableState();
|
||||
}
|
||||
|
||||
class CherrypickTableState extends State<CherrypickTable> {
|
||||
@ -242,7 +242,7 @@ class RepoInfoExpansion extends StatefulWidget {
|
||||
final Map<String, Object> currentStatus;
|
||||
|
||||
@override
|
||||
RepoInfoExpansionState createState() => RepoInfoExpansionState();
|
||||
State<RepoInfoExpansion> createState() => RepoInfoExpansionState();
|
||||
}
|
||||
|
||||
class RepoInfoExpansionState extends State<RepoInfoExpansion> {
|
||||
|
184
dev/conductor/ui/lib/widgets/create_release_substeps.dart
Normal file
184
dev/conductor/ui/lib/widgets/create_release_substeps.dart
Normal file
@ -0,0 +1,184 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Displays all substeps related to the 1st step.
|
||||
///
|
||||
/// Uses input fields and dropdowns to capture all the parameters of the conductor start command.
|
||||
class CreateReleaseSubsteps extends StatefulWidget {
|
||||
const CreateReleaseSubsteps({
|
||||
Key? key,
|
||||
required this.nextStep,
|
||||
}) : super(key: key);
|
||||
|
||||
final VoidCallback nextStep;
|
||||
|
||||
@override
|
||||
State<CreateReleaseSubsteps> createState() => CreateReleaseSubstepsState();
|
||||
|
||||
static const List<String> substepTitles = <String>[
|
||||
'Candidate Branch',
|
||||
'Release Channel',
|
||||
'Framework Mirror',
|
||||
'Engine Mirror',
|
||||
'Engine Cherrypicks (if necessary)',
|
||||
'Framework Cherrypicks (if necessary)',
|
||||
'Dart Revision (if necessary)',
|
||||
'Increment',
|
||||
];
|
||||
}
|
||||
|
||||
class CreateReleaseSubstepsState extends State<CreateReleaseSubsteps> {
|
||||
// Initialize a public state so it could be accessed in the test file.
|
||||
@visibleForTesting
|
||||
late Map<String, String?> releaseData = <String, String?>{};
|
||||
|
||||
/// Updates the corresponding [field] in [releaseData] with [data].
|
||||
void setReleaseData(String field, String data) {
|
||||
setState(() {
|
||||
releaseData = <String, String?>{
|
||||
...releaseData,
|
||||
field: data,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
InputAsSubstep(
|
||||
index: 0,
|
||||
setReleaseData: setReleaseData,
|
||||
hintText: 'The candidate branch the release will be based on.',
|
||||
),
|
||||
CheckboxListTileDropdown(
|
||||
index: 1,
|
||||
releaseData: releaseData,
|
||||
setReleaseData: setReleaseData,
|
||||
options: const <String>['dev', 'beta', 'stable'],
|
||||
),
|
||||
InputAsSubstep(
|
||||
index: 2,
|
||||
setReleaseData: setReleaseData,
|
||||
hintText: "Git remote of the Conductor user's Framework repository mirror.",
|
||||
),
|
||||
InputAsSubstep(
|
||||
index: 3,
|
||||
setReleaseData: setReleaseData,
|
||||
hintText: "Git remote of the Conductor user's Engine repository mirror.",
|
||||
),
|
||||
InputAsSubstep(
|
||||
index: 4,
|
||||
setReleaseData: setReleaseData,
|
||||
hintText: 'Engine cherrypick hashes to be applied. Multiple hashes delimited by a comma, no spaces.',
|
||||
),
|
||||
InputAsSubstep(
|
||||
index: 5,
|
||||
setReleaseData: setReleaseData,
|
||||
hintText: 'Framework cherrypick hashes to be applied. Multiple hashes delimited by a comma, no spaces.',
|
||||
),
|
||||
InputAsSubstep(
|
||||
index: 6,
|
||||
setReleaseData: setReleaseData,
|
||||
hintText: 'New Dart revision to cherrypick.',
|
||||
),
|
||||
CheckboxListTileDropdown(
|
||||
index: 7,
|
||||
releaseData: releaseData,
|
||||
setReleaseData: setReleaseData,
|
||||
options: const <String>['y', 'z', 'm', 'n'],
|
||||
),
|
||||
const SizedBox(height: 20.0),
|
||||
Center(
|
||||
// TODO(Yugue): Add regex validation for each parameter input
|
||||
// before Continue button is enabled, https://github.com/flutter/flutter/issues/91925.
|
||||
child: ElevatedButton(
|
||||
key: const Key('step1continue'),
|
||||
onPressed: () {
|
||||
widget.nextStep();
|
||||
},
|
||||
child: const Text('Continue'),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
typedef SetReleaseData = void Function(String name, String data);
|
||||
|
||||
/// Captures the input values and updates the corresponding field in [releaseData].
|
||||
class InputAsSubstep extends StatelessWidget {
|
||||
const InputAsSubstep({
|
||||
Key? key,
|
||||
required this.index,
|
||||
required this.setReleaseData,
|
||||
this.hintText,
|
||||
}) : super(key: key);
|
||||
|
||||
final int index;
|
||||
final SetReleaseData setReleaseData;
|
||||
final String? hintText;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormField(
|
||||
key: Key(CreateReleaseSubsteps.substepTitles[index]),
|
||||
decoration: InputDecoration(
|
||||
labelText: CreateReleaseSubsteps.substepTitles[index],
|
||||
hintText: hintText,
|
||||
),
|
||||
onChanged: (String data) {
|
||||
setReleaseData(CreateReleaseSubsteps.substepTitles[index], data);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Captures the chosen option and updates the corresponding field in [releaseData].
|
||||
class CheckboxListTileDropdown extends StatelessWidget {
|
||||
const CheckboxListTileDropdown({
|
||||
Key? key,
|
||||
required this.index,
|
||||
required this.releaseData,
|
||||
required this.setReleaseData,
|
||||
required this.options,
|
||||
}) : super(key: key);
|
||||
|
||||
final int index;
|
||||
final Map<String, String?> releaseData;
|
||||
final SetReleaseData setReleaseData;
|
||||
final List<String> options;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
CreateReleaseSubsteps.substepTitles[index],
|
||||
style: Theme.of(context).textTheme.subtitle1!.copyWith(color: Colors.grey[700]),
|
||||
),
|
||||
const SizedBox(width: 20.0),
|
||||
DropdownButton<String>(
|
||||
hint: const Text('-'), // Dropdown initially displays the hint when no option is selected.
|
||||
key: Key(CreateReleaseSubsteps.substepTitles[index]),
|
||||
value: releaseData[CreateReleaseSubsteps.substepTitles[index]],
|
||||
icon: const Icon(Icons.arrow_downward),
|
||||
items: options.map<DropdownMenuItem<String>>((String value) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(value),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (String? newValue) {
|
||||
setReleaseData(CreateReleaseSubsteps.substepTitles[index], newValue!);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ import 'package:conductor_core/proto.dart' as pb;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'conductor_status.dart';
|
||||
import 'create_release_substeps.dart';
|
||||
import 'substeps.dart';
|
||||
|
||||
/// Displays the progression and each step of the release from the conductor.
|
||||
@ -23,7 +24,7 @@ class MainProgression extends StatefulWidget {
|
||||
final String stateFilePath;
|
||||
|
||||
@override
|
||||
MainProgressionState createState() => MainProgressionState();
|
||||
State<MainProgression> createState() => MainProgressionState();
|
||||
|
||||
static const List<String> _stepTitles = <String>[
|
||||
'Initialize a New Flutter Release',
|
||||
@ -85,7 +86,7 @@ class MainProgressionState extends State<MainProgression> {
|
||||
title: Text(MainProgression._stepTitles[0]),
|
||||
content: Column(
|
||||
children: <Widget>[
|
||||
ConductorSubsteps(nextStep: nextStep),
|
||||
CreateReleaseSubsteps(nextStep: nextStep),
|
||||
],
|
||||
),
|
||||
isActive: true,
|
||||
|
@ -16,7 +16,7 @@ class ConductorSubsteps extends StatefulWidget {
|
||||
final VoidCallback nextStep;
|
||||
|
||||
@override
|
||||
ConductorSubstepsState createState() => ConductorSubstepsState();
|
||||
State<ConductorSubsteps> createState() => ConductorSubstepsState();
|
||||
|
||||
static const List<String> _substepTitles = <String>[
|
||||
'Substep 1',
|
||||
|
@ -0,0 +1,78 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:conductor_ui/widgets/create_release_substeps.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Widget should save all parameters correctly', (WidgetTester tester) async {
|
||||
const String candidateBranch = 'flutter-1.2-candidate.3';
|
||||
const String releaseChannel = 'dev';
|
||||
const String frameworkMirror = 'git@github.com:test/flutter.git';
|
||||
const String engineMirror = 'git@github.com:test/engine.git';
|
||||
const String engineCherrypick = 'a5a25cd702b062c24b2c67b8d30b5cb33e0ef6f0,94d06a2e1d01a3b0c693b94d70c5e1df9d78d249';
|
||||
const String frameworkCherrypick = '768cd702b691584b2c67b8d30b5cb33e0ef6f0';
|
||||
const String dartRevision = 'fe9708ab688dcda9923f584ba370a66fcbc3811f';
|
||||
const String increment = 'y';
|
||||
|
||||
await tester.pumpWidget(
|
||||
StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
return MaterialApp(
|
||||
home: Material(
|
||||
child: ListView(
|
||||
children: <Widget>[
|
||||
CreateReleaseSubsteps(
|
||||
nextStep: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await tester.enterText(find.byKey(const Key('Candidate Branch')), candidateBranch);
|
||||
|
||||
final StatefulElement createReleaseSubsteps = tester.element(find.byType(CreateReleaseSubsteps));
|
||||
final CreateReleaseSubstepsState createReleaseSubstepsState =
|
||||
createReleaseSubsteps.state as CreateReleaseSubstepsState;
|
||||
|
||||
/// Tests the Release Channel dropdown menu.
|
||||
await tester.tap(find.byKey(const Key('Release Channel')));
|
||||
await tester.pumpAndSettle(); // finish the menu animation
|
||||
expect(createReleaseSubstepsState.releaseData['Release Channel'], equals(null));
|
||||
await tester.tap(find.text(releaseChannel).last);
|
||||
await tester.pumpAndSettle(); // finish the menu animation
|
||||
|
||||
await tester.enterText(find.byKey(const Key('Framework Mirror')), frameworkMirror);
|
||||
await tester.enterText(find.byKey(const Key('Engine Mirror')), engineMirror);
|
||||
await tester.enterText(find.byKey(const Key('Engine Cherrypicks (if necessary)')), engineCherrypick);
|
||||
await tester.enterText(find.byKey(const Key('Framework Cherrypicks (if necessary)')), frameworkCherrypick);
|
||||
await tester.enterText(find.byKey(const Key('Dart Revision (if necessary)')), dartRevision);
|
||||
|
||||
/// Tests the Increment dropdown menu.
|
||||
await tester.tap(find.byKey(const Key('Increment')));
|
||||
await tester.pumpAndSettle(); // finish the menu animation
|
||||
expect(createReleaseSubstepsState.releaseData['Increment'], equals(null));
|
||||
await tester.tap(find.text(increment).last);
|
||||
await tester.pumpAndSettle(); // finish the menu animation
|
||||
|
||||
expect(
|
||||
createReleaseSubstepsState.releaseData,
|
||||
equals(<String, String>{
|
||||
'Candidate Branch': candidateBranch,
|
||||
'Release Channel': releaseChannel,
|
||||
'Framework Mirror': frameworkMirror,
|
||||
'Engine Mirror': engineMirror,
|
||||
'Engine Cherrypicks (if necessary)': engineCherrypick,
|
||||
'Framework Cherrypicks (if necessary)': frameworkCherrypick,
|
||||
'Dart Revision (if necessary)': dartRevision,
|
||||
'Increment': increment,
|
||||
}));
|
||||
});
|
||||
}
|
@ -7,50 +7,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets(
|
||||
'All substeps of the current step must be checked before able to continue to the next step',
|
||||
(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
return MaterialApp(
|
||||
home: Material(
|
||||
child: Column(
|
||||
children: const <Widget>[
|
||||
MainProgression(
|
||||
stateFilePath: './testPath',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(Stepper), findsOneWidget);
|
||||
expect(find.text('Initialize a New Flutter Release'), findsOneWidget);
|
||||
expect(find.text('Continue'), findsNWidgets(0));
|
||||
|
||||
await tester.tap(find.text('Substep 1').first);
|
||||
await tester.tap(find.text('Substep 2').first);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Continue'), findsNWidgets(0));
|
||||
|
||||
await tester.tap(find.text('Substep 3').first);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Continue'), findsOneWidget);
|
||||
expect(tester.widget<Stepper>(find.byType(Stepper)).steps[0].state, equals(StepState.indexed));
|
||||
expect(tester.widget<Stepper>(find.byType(Stepper)).steps[1].state, equals(StepState.disabled));
|
||||
|
||||
await tester.tap(find.text('Continue'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.widget<Stepper>(find.byType(Stepper)).steps[0].state,
|
||||
equals(StepState.complete));
|
||||
expect(tester.widget<Stepper>(find.byType(Stepper)).steps[1].state,
|
||||
equals(StepState.indexed));
|
||||
});
|
||||
|
||||
testWidgets('When user clicks on a previously completed step, Stepper does not navigate back.',
|
||||
(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
@ -71,11 +27,10 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Substep 1').first);
|
||||
await tester.tap(find.text('Substep 2').first);
|
||||
await tester.tap(find.text('Substep 3').first);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.widget<Stepper>(find.byType(Stepper)).currentStep, equals(0));
|
||||
|
||||
await tester.tap(find.text('Continue'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Initialize a New Flutter Release'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user