Unverified Commit a6e91c36 authored by Alex's avatar Alex Committed by GitHub

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
parent cf443a7e
......@@ -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> {
......
// 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',
......
// 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,8 +7,7 @@ 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',
testWidgets('When user clicks on a previously completed step, Stepper does not navigate back.',
(WidgetTester tester) async {
await tester.pumpWidget(
StatefulBuilder(
......@@ -28,54 +27,10 @@ void main() {
),
);
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));
expect(tester.widget<Stepper>(find.byType(Stepper)).currentStep, equals(0));
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(
StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return MaterialApp(
home: Material(
child: Column(
children: const <Widget>[
MainProgression(
stateFilePath: './testPath',
),
],
),
),
);
},
),
);
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();
await tester.tap(find.text('Continue'));
await tester.tap(find.text('Initialize a New Flutter Release'));
await tester.pumpAndSettle();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment