Skip to content

Commit a6e91c3

Browse files
authored
Conductor UI step1 (flutter#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
1 parent cf443a7 commit a6e91c3

File tree

6 files changed

+271
-53
lines changed

6 files changed

+271
-53
lines changed

dev/conductor/ui/lib/widgets/conductor_status.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class ConductorStatus extends StatefulWidget {
1818
final String stateFilePath;
1919

2020
@override
21-
ConductorStatusState createState() => ConductorStatusState();
21+
State<ConductorStatus> createState() => ConductorStatusState();
2222

2323
static final List<String> headerElements = <String>[
2424
'Conductor Version',
@@ -194,7 +194,7 @@ class CherrypickTable extends StatefulWidget {
194194
final Map<String, Object> currentStatus;
195195

196196
@override
197-
CherrypickTableState createState() => CherrypickTableState();
197+
State<CherrypickTable> createState() => CherrypickTableState();
198198
}
199199

200200
class CherrypickTableState extends State<CherrypickTable> {
@@ -242,7 +242,7 @@ class RepoInfoExpansion extends StatefulWidget {
242242
final Map<String, Object> currentStatus;
243243

244244
@override
245-
RepoInfoExpansionState createState() => RepoInfoExpansionState();
245+
State<RepoInfoExpansion> createState() => RepoInfoExpansionState();
246246
}
247247

248248
class RepoInfoExpansionState extends State<RepoInfoExpansion> {
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/material.dart';
6+
7+
/// Displays all substeps related to the 1st step.
8+
///
9+
/// Uses input fields and dropdowns to capture all the parameters of the conductor start command.
10+
class CreateReleaseSubsteps extends StatefulWidget {
11+
const CreateReleaseSubsteps({
12+
Key? key,
13+
required this.nextStep,
14+
}) : super(key: key);
15+
16+
final VoidCallback nextStep;
17+
18+
@override
19+
State<CreateReleaseSubsteps> createState() => CreateReleaseSubstepsState();
20+
21+
static const List<String> substepTitles = <String>[
22+
'Candidate Branch',
23+
'Release Channel',
24+
'Framework Mirror',
25+
'Engine Mirror',
26+
'Engine Cherrypicks (if necessary)',
27+
'Framework Cherrypicks (if necessary)',
28+
'Dart Revision (if necessary)',
29+
'Increment',
30+
];
31+
}
32+
33+
class CreateReleaseSubstepsState extends State<CreateReleaseSubsteps> {
34+
// Initialize a public state so it could be accessed in the test file.
35+
@visibleForTesting
36+
late Map<String, String?> releaseData = <String, String?>{};
37+
38+
/// Updates the corresponding [field] in [releaseData] with [data].
39+
void setReleaseData(String field, String data) {
40+
setState(() {
41+
releaseData = <String, String?>{
42+
...releaseData,
43+
field: data,
44+
};
45+
});
46+
}
47+
48+
@override
49+
Widget build(BuildContext context) {
50+
return Column(
51+
crossAxisAlignment: CrossAxisAlignment.start,
52+
children: <Widget>[
53+
InputAsSubstep(
54+
index: 0,
55+
setReleaseData: setReleaseData,
56+
hintText: 'The candidate branch the release will be based on.',
57+
),
58+
CheckboxListTileDropdown(
59+
index: 1,
60+
releaseData: releaseData,
61+
setReleaseData: setReleaseData,
62+
options: const <String>['dev', 'beta', 'stable'],
63+
),
64+
InputAsSubstep(
65+
index: 2,
66+
setReleaseData: setReleaseData,
67+
hintText: "Git remote of the Conductor user's Framework repository mirror.",
68+
),
69+
InputAsSubstep(
70+
index: 3,
71+
setReleaseData: setReleaseData,
72+
hintText: "Git remote of the Conductor user's Engine repository mirror.",
73+
),
74+
InputAsSubstep(
75+
index: 4,
76+
setReleaseData: setReleaseData,
77+
hintText: 'Engine cherrypick hashes to be applied. Multiple hashes delimited by a comma, no spaces.',
78+
),
79+
InputAsSubstep(
80+
index: 5,
81+
setReleaseData: setReleaseData,
82+
hintText: 'Framework cherrypick hashes to be applied. Multiple hashes delimited by a comma, no spaces.',
83+
),
84+
InputAsSubstep(
85+
index: 6,
86+
setReleaseData: setReleaseData,
87+
hintText: 'New Dart revision to cherrypick.',
88+
),
89+
CheckboxListTileDropdown(
90+
index: 7,
91+
releaseData: releaseData,
92+
setReleaseData: setReleaseData,
93+
options: const <String>['y', 'z', 'm', 'n'],
94+
),
95+
const SizedBox(height: 20.0),
96+
Center(
97+
// TODO(Yugue): Add regex validation for each parameter input
98+
// before Continue button is enabled, https://github.com/flutter/flutter/issues/91925.
99+
child: ElevatedButton(
100+
key: const Key('step1continue'),
101+
onPressed: () {
102+
widget.nextStep();
103+
},
104+
child: const Text('Continue'),
105+
),
106+
),
107+
],
108+
);
109+
}
110+
}
111+
112+
typedef SetReleaseData = void Function(String name, String data);
113+
114+
/// Captures the input values and updates the corresponding field in [releaseData].
115+
class InputAsSubstep extends StatelessWidget {
116+
const InputAsSubstep({
117+
Key? key,
118+
required this.index,
119+
required this.setReleaseData,
120+
this.hintText,
121+
}) : super(key: key);
122+
123+
final int index;
124+
final SetReleaseData setReleaseData;
125+
final String? hintText;
126+
127+
@override
128+
Widget build(BuildContext context) {
129+
return TextFormField(
130+
key: Key(CreateReleaseSubsteps.substepTitles[index]),
131+
decoration: InputDecoration(
132+
labelText: CreateReleaseSubsteps.substepTitles[index],
133+
hintText: hintText,
134+
),
135+
onChanged: (String data) {
136+
setReleaseData(CreateReleaseSubsteps.substepTitles[index], data);
137+
},
138+
);
139+
}
140+
}
141+
142+
/// Captures the chosen option and updates the corresponding field in [releaseData].
143+
class CheckboxListTileDropdown extends StatelessWidget {
144+
const CheckboxListTileDropdown({
145+
Key? key,
146+
required this.index,
147+
required this.releaseData,
148+
required this.setReleaseData,
149+
required this.options,
150+
}) : super(key: key);
151+
152+
final int index;
153+
final Map<String, String?> releaseData;
154+
final SetReleaseData setReleaseData;
155+
final List<String> options;
156+
157+
@override
158+
Widget build(BuildContext context) {
159+
return Row(
160+
children: <Widget>[
161+
Text(
162+
CreateReleaseSubsteps.substepTitles[index],
163+
style: Theme.of(context).textTheme.subtitle1!.copyWith(color: Colors.grey[700]),
164+
),
165+
const SizedBox(width: 20.0),
166+
DropdownButton<String>(
167+
hint: const Text('-'), // Dropdown initially displays the hint when no option is selected.
168+
key: Key(CreateReleaseSubsteps.substepTitles[index]),
169+
value: releaseData[CreateReleaseSubsteps.substepTitles[index]],
170+
icon: const Icon(Icons.arrow_downward),
171+
items: options.map<DropdownMenuItem<String>>((String value) {
172+
return DropdownMenuItem<String>(
173+
value: value,
174+
child: Text(value),
175+
);
176+
}).toList(),
177+
onChanged: (String? newValue) {
178+
setReleaseData(CreateReleaseSubsteps.substepTitles[index], newValue!);
179+
},
180+
),
181+
],
182+
);
183+
}
184+
}

dev/conductor/ui/lib/widgets/progression.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:conductor_core/proto.dart' as pb;
66
import 'package:flutter/material.dart';
77

88
import 'conductor_status.dart';
9+
import 'create_release_substeps.dart';
910
import 'substeps.dart';
1011

1112
/// Displays the progression and each step of the release from the conductor.
@@ -23,7 +24,7 @@ class MainProgression extends StatefulWidget {
2324
final String stateFilePath;
2425

2526
@override
26-
MainProgressionState createState() => MainProgressionState();
27+
State<MainProgression> createState() => MainProgressionState();
2728

2829
static const List<String> _stepTitles = <String>[
2930
'Initialize a New Flutter Release',
@@ -85,7 +86,7 @@ class MainProgressionState extends State<MainProgression> {
8586
title: Text(MainProgression._stepTitles[0]),
8687
content: Column(
8788
children: <Widget>[
88-
ConductorSubsteps(nextStep: nextStep),
89+
CreateReleaseSubsteps(nextStep: nextStep),
8990
],
9091
),
9192
isActive: true,

dev/conductor/ui/lib/widgets/substeps.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class ConductorSubsteps extends StatefulWidget {
1616
final VoidCallback nextStep;
1717

1818
@override
19-
ConductorSubstepsState createState() => ConductorSubstepsState();
19+
State<ConductorSubsteps> createState() => ConductorSubstepsState();
2020

2121
static const List<String> _substepTitles = <String>[
2222
'Substep 1',
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:conductor_ui/widgets/create_release_substeps.dart';
6+
import 'package:flutter/material.dart';
7+
import 'package:flutter/widgets.dart';
8+
import 'package:flutter_test/flutter_test.dart';
9+
10+
void main() {
11+
testWidgets('Widget should save all parameters correctly', (WidgetTester tester) async {
12+
const String candidateBranch = 'flutter-1.2-candidate.3';
13+
const String releaseChannel = 'dev';
14+
const String frameworkMirror = '[email protected]:test/flutter.git';
15+
const String engineMirror = '[email protected]:test/engine.git';
16+
const String engineCherrypick = 'a5a25cd702b062c24b2c67b8d30b5cb33e0ef6f0,94d06a2e1d01a3b0c693b94d70c5e1df9d78d249';
17+
const String frameworkCherrypick = '768cd702b691584b2c67b8d30b5cb33e0ef6f0';
18+
const String dartRevision = 'fe9708ab688dcda9923f584ba370a66fcbc3811f';
19+
const String increment = 'y';
20+
21+
await tester.pumpWidget(
22+
StatefulBuilder(
23+
builder: (BuildContext context, StateSetter setState) {
24+
return MaterialApp(
25+
home: Material(
26+
child: ListView(
27+
children: <Widget>[
28+
CreateReleaseSubsteps(
29+
nextStep: () {},
30+
),
31+
],
32+
),
33+
),
34+
);
35+
},
36+
),
37+
);
38+
39+
await tester.enterText(find.byKey(const Key('Candidate Branch')), candidateBranch);
40+
41+
final StatefulElement createReleaseSubsteps = tester.element(find.byType(CreateReleaseSubsteps));
42+
final CreateReleaseSubstepsState createReleaseSubstepsState =
43+
createReleaseSubsteps.state as CreateReleaseSubstepsState;
44+
45+
/// Tests the Release Channel dropdown menu.
46+
await tester.tap(find.byKey(const Key('Release Channel')));
47+
await tester.pumpAndSettle(); // finish the menu animation
48+
expect(createReleaseSubstepsState.releaseData['Release Channel'], equals(null));
49+
await tester.tap(find.text(releaseChannel).last);
50+
await tester.pumpAndSettle(); // finish the menu animation
51+
52+
await tester.enterText(find.byKey(const Key('Framework Mirror')), frameworkMirror);
53+
await tester.enterText(find.byKey(const Key('Engine Mirror')), engineMirror);
54+
await tester.enterText(find.byKey(const Key('Engine Cherrypicks (if necessary)')), engineCherrypick);
55+
await tester.enterText(find.byKey(const Key('Framework Cherrypicks (if necessary)')), frameworkCherrypick);
56+
await tester.enterText(find.byKey(const Key('Dart Revision (if necessary)')), dartRevision);
57+
58+
/// Tests the Increment dropdown menu.
59+
await tester.tap(find.byKey(const Key('Increment')));
60+
await tester.pumpAndSettle(); // finish the menu animation
61+
expect(createReleaseSubstepsState.releaseData['Increment'], equals(null));
62+
await tester.tap(find.text(increment).last);
63+
await tester.pumpAndSettle(); // finish the menu animation
64+
65+
expect(
66+
createReleaseSubstepsState.releaseData,
67+
equals(<String, String>{
68+
'Candidate Branch': candidateBranch,
69+
'Release Channel': releaseChannel,
70+
'Framework Mirror': frameworkMirror,
71+
'Engine Mirror': engineMirror,
72+
'Engine Cherrypicks (if necessary)': engineCherrypick,
73+
'Framework Cherrypicks (if necessary)': frameworkCherrypick,
74+
'Dart Revision (if necessary)': dartRevision,
75+
'Increment': increment,
76+
}));
77+
});
78+
}

dev/conductor/ui/test/widgets/stepper_test.dart

Lines changed: 2 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ import 'package:flutter/material.dart';
77
import 'package:flutter_test/flutter_test.dart';
88

99
void main() {
10-
testWidgets(
11-
'All substeps of the current step must be checked before able to continue to the next step',
10+
testWidgets('When user clicks on a previously completed step, Stepper does not navigate back.',
1211
(WidgetTester tester) async {
1312
await tester.pumpWidget(
1413
StatefulBuilder(
@@ -28,54 +27,10 @@ void main() {
2827
),
2928
);
3029

31-
expect(find.byType(Stepper), findsOneWidget);
32-
expect(find.text('Initialize a New Flutter Release'), findsOneWidget);
33-
expect(find.text('Continue'), findsNWidgets(0));
34-
35-
await tester.tap(find.text('Substep 1').first);
36-
await tester.tap(find.text('Substep 2').first);
37-
await tester.pumpAndSettle();
38-
expect(find.text('Continue'), findsNWidgets(0));
39-
40-
await tester.tap(find.text('Substep 3').first);
41-
await tester.pumpAndSettle();
42-
expect(find.text('Continue'), findsOneWidget);
43-
expect(tester.widget<Stepper>(find.byType(Stepper)).steps[0].state, equals(StepState.indexed));
44-
expect(tester.widget<Stepper>(find.byType(Stepper)).steps[1].state, equals(StepState.disabled));
30+
expect(tester.widget<Stepper>(find.byType(Stepper)).currentStep, equals(0));
4531

4632
await tester.tap(find.text('Continue'));
4733
await tester.pumpAndSettle();
48-
expect(tester.widget<Stepper>(find.byType(Stepper)).steps[0].state,
49-
equals(StepState.complete));
50-
expect(tester.widget<Stepper>(find.byType(Stepper)).steps[1].state,
51-
equals(StepState.indexed));
52-
});
53-
54-
testWidgets('When user clicks on a previously completed step, Stepper does not navigate back.',
55-
(WidgetTester tester) async {
56-
await tester.pumpWidget(
57-
StatefulBuilder(
58-
builder: (BuildContext context, StateSetter setState) {
59-
return MaterialApp(
60-
home: Material(
61-
child: Column(
62-
children: const <Widget>[
63-
MainProgression(
64-
stateFilePath: './testPath',
65-
),
66-
],
67-
),
68-
),
69-
);
70-
},
71-
),
72-
);
73-
74-
await tester.tap(find.text('Substep 1').first);
75-
await tester.tap(find.text('Substep 2').first);
76-
await tester.tap(find.text('Substep 3').first);
77-
await tester.pumpAndSettle();
78-
await tester.tap(find.text('Continue'));
7934
await tester.tap(find.text('Initialize a New Flutter Release'));
8035
await tester.pumpAndSettle();
8136

0 commit comments

Comments
 (0)