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;
|
final String stateFilePath;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConductorStatusState createState() => ConductorStatusState();
|
State<ConductorStatus> createState() => ConductorStatusState();
|
||||||
|
|
||||||
static final List<String> headerElements = <String>[
|
static final List<String> headerElements = <String>[
|
||||||
'Conductor Version',
|
'Conductor Version',
|
||||||
@ -194,7 +194,7 @@ class CherrypickTable extends StatefulWidget {
|
|||||||
final Map<String, Object> currentStatus;
|
final Map<String, Object> currentStatus;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
CherrypickTableState createState() => CherrypickTableState();
|
State<CherrypickTable> createState() => CherrypickTableState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class CherrypickTableState extends State<CherrypickTable> {
|
class CherrypickTableState extends State<CherrypickTable> {
|
||||||
@ -242,7 +242,7 @@ class RepoInfoExpansion extends StatefulWidget {
|
|||||||
final Map<String, Object> currentStatus;
|
final Map<String, Object> currentStatus;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RepoInfoExpansionState createState() => RepoInfoExpansionState();
|
State<RepoInfoExpansion> createState() => RepoInfoExpansionState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class RepoInfoExpansionState extends State<RepoInfoExpansion> {
|
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 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'conductor_status.dart';
|
import 'conductor_status.dart';
|
||||||
|
import 'create_release_substeps.dart';
|
||||||
import 'substeps.dart';
|
import 'substeps.dart';
|
||||||
|
|
||||||
/// Displays the progression and each step of the release from the conductor.
|
/// Displays the progression and each step of the release from the conductor.
|
||||||
@ -23,7 +24,7 @@ class MainProgression extends StatefulWidget {
|
|||||||
final String stateFilePath;
|
final String stateFilePath;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MainProgressionState createState() => MainProgressionState();
|
State<MainProgression> createState() => MainProgressionState();
|
||||||
|
|
||||||
static const List<String> _stepTitles = <String>[
|
static const List<String> _stepTitles = <String>[
|
||||||
'Initialize a New Flutter Release',
|
'Initialize a New Flutter Release',
|
||||||
@ -85,7 +86,7 @@ class MainProgressionState extends State<MainProgression> {
|
|||||||
title: Text(MainProgression._stepTitles[0]),
|
title: Text(MainProgression._stepTitles[0]),
|
||||||
content: Column(
|
content: Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
ConductorSubsteps(nextStep: nextStep),
|
CreateReleaseSubsteps(nextStep: nextStep),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
isActive: true,
|
isActive: true,
|
||||||
|
@ -16,7 +16,7 @@ class ConductorSubsteps extends StatefulWidget {
|
|||||||
final VoidCallback nextStep;
|
final VoidCallback nextStep;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConductorSubstepsState createState() => ConductorSubstepsState();
|
State<ConductorSubsteps> createState() => ConductorSubstepsState();
|
||||||
|
|
||||||
static const List<String> _substepTitles = <String>[
|
static const List<String> _substepTitles = <String>[
|
||||||
'Substep 1',
|
'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';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
void main() {
|
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.',
|
testWidgets('When user clicks on a previously completed step, Stepper does not navigate back.',
|
||||||
(WidgetTester tester) async {
|
(WidgetTester tester) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
@ -71,11 +27,10 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.tap(find.text('Substep 1').first);
|
expect(tester.widget<Stepper>(find.byType(Stepper)).currentStep, equals(0));
|
||||||
await tester.tap(find.text('Substep 2').first);
|
|
||||||
await tester.tap(find.text('Substep 3').first);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
await tester.tap(find.text('Continue'));
|
await tester.tap(find.text('Continue'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
await tester.tap(find.text('Initialize a New Flutter Release'));
|
await tester.tap(find.text('Initialize a New Flutter Release'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user