diff --git a/dev/conductor/ui/lib/main.dart b/dev/conductor/ui/lib/main.dart index 89324c280fb..d57d3bf67ee 100644 --- a/dev/conductor/ui/lib/main.dart +++ b/dev/conductor/ui/lib/main.dart @@ -55,7 +55,7 @@ class MyApp extends StatelessWidget { const SelectableText( 'Desktop app for managing a release of the Flutter SDK, currently in development', ), - const SizedBox(height: 20.0), + const SizedBox(height: 10.0), MainProgression( releaseState: state, stateFilePath: _stateFilePath, diff --git a/dev/conductor/ui/lib/widgets/conductor_status.dart b/dev/conductor/ui/lib/widgets/conductor_status.dart index b8abab0b205..c5d22004a6b 100644 --- a/dev/conductor/ui/lib/widgets/conductor_status.dart +++ b/dev/conductor/ui/lib/widgets/conductor_status.dart @@ -28,6 +28,22 @@ class ConductorStatus extends StatefulWidget { 'Release Updated at', 'Dart SDK Revision', ]; + + static final List engineRepoElements = [ + 'Engine Candidate Branch', + 'Engine Starting Git HEAD', + 'Engine Current Git HEAD', + 'Engine Path to Checkout', + 'Engine LUCI Dashboard', + ]; + + static final List frameworkRepoElements = [ + 'Framework Candidate Branch', + 'Framework Starting Git HEAD', + 'Framework Current Git HEAD', + 'Framework Path to Checkout', + 'Framework LUCI Dashboard', + ]; } class ConductorStatusState extends State { @@ -86,7 +102,6 @@ class ConductorStatusState extends State { Table( columnWidths: const { 0: FixedColumnWidth(200.0), - 1: FixedColumnWidth(400.0), }, children: [ for (String headerElement in ConductorStatus.headerElements) @@ -105,19 +120,23 @@ class ConductorStatusState extends State { children: [ Column( children: [ + RepoInfoExpansion(engineOrFramework: 'engine', currentStatus: currentStatus), + const SizedBox(height: 10.0), CherrypickTable(engineOrFramework: 'engine', currentStatus: currentStatus), ], ), const SizedBox(width: 20.0), Column( children: [ + RepoInfoExpansion(engineOrFramework: 'framework', currentStatus: currentStatus), + const SizedBox(height: 10.0), CherrypickTable(engineOrFramework: 'framework', currentStatus: currentStatus), ], ), ], ) ], - ) + ), ], ); } @@ -208,3 +227,80 @@ class CherrypickTableState extends State { ); } } + +/// Widget to display repo info related to the engine and framework. +/// +/// Click to show/hide the repo info in a dropdown fashion. By default the section is hidden. +class RepoInfoExpansion extends StatefulWidget { + const RepoInfoExpansion({ + Key? key, + required this.engineOrFramework, + required this.currentStatus, + }) : super(key: key); + + final String engineOrFramework; + final Map currentStatus; + + @override + RepoInfoExpansionState createState() => RepoInfoExpansionState(); +} + +class RepoInfoExpansionState extends State { + bool _isExpanded = false; + + /// Show/hide [ExpansionPanel]. + void showHide() { + setState(() { + _isExpanded = !_isExpanded; + }); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 500.0, + child: ExpansionPanelList( + expandedHeaderPadding: EdgeInsets.zero, + expansionCallback: (int index, bool isExpanded) { + showHide(); + }, + children: [ + ExpansionPanel( + isExpanded: _isExpanded, + headerBuilder: (BuildContext context, bool isExpanded) { + return ListTile( + key: Key('${widget.engineOrFramework}RepoInfoDropdown'), + title: Text('${widget.engineOrFramework == 'engine' ? 'Engine' : 'Framework'} Repo Info'), + onTap: () { + showHide(); + }); + }, + body: Padding( + padding: const EdgeInsets.all(15.0), + child: Table( + columnWidths: const { + 0: FixedColumnWidth(240.0), + }, + children: [ + for (String repoElement in widget.engineOrFramework == 'engine' + ? ConductorStatus.engineRepoElements + : ConductorStatus.frameworkRepoElements) + TableRow( + decoration: const BoxDecoration(border: Border(top: BorderSide(color: Colors.grey))), + children: [ + Text('$repoElement:'), + SelectableText( + (widget.currentStatus[repoElement] == null || widget.currentStatus[repoElement] == '') + ? 'Unknown' + : widget.currentStatus[repoElement]! as String), + ], + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/dev/conductor/ui/lib/widgets/progression.dart b/dev/conductor/ui/lib/widgets/progression.dart index d6cc87bd695..91e0fb05f3b 100644 --- a/dev/conductor/ui/lib/widgets/progression.dart +++ b/dev/conductor/ui/lib/widgets/progression.dart @@ -37,7 +37,7 @@ class MainProgression extends StatefulWidget { class MainProgressionState extends State { int _completedStep = 0; - // Move forward the stepper to the next step of the release. + /// Move forward the stepper to the next step of the release. void nextStep() { if (_completedStep < MainProgression._stepTitles.length - 1) { setState(() { @@ -74,6 +74,7 @@ class MainProgressionState extends State { releaseState: widget.releaseState, stateFilePath: widget.stateFilePath, ), + const SizedBox(height: 20.0), Stepper( controlsBuilder: (BuildContext context, ControlsDetails details) => Row(), physics: const ScrollPhysics(), diff --git a/dev/conductor/ui/test/widgets/conductor_status_test.dart b/dev/conductor/ui/test/widgets/conductor_status_test.dart index 79d3726534c..1d0ab8466c1 100644 --- a/dev/conductor/ui/test/widgets/conductor_status_test.dart +++ b/dev/conductor/ui/test/widgets/conductor_status_test.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:conductor_core/conductor_core.dart'; import 'package:conductor_core/proto.dart' as pb; import 'package:conductor_ui/widgets/conductor_status.dart'; import 'package:flutter/gestures.dart'; @@ -10,6 +11,57 @@ import 'package:flutter_test/flutter_test.dart'; void main() { group('conductor_status', () { + late pb.ConductorState state; + + const String testPath = './testPath'; + const String conductorVersion = 'v1.0'; + const String releaseChannel = 'beta'; + const String releaseVersion = '1.2.0-3.4.pre'; + const String engineCandidateBranch = 'flutter-1.2-candidate.3'; + const String frameworkCandidateBranch = 'flutter-1.2-candidate.4'; + const String workingBranch = 'cherrypicks-$engineCandidateBranch'; + const String dartRevision = 'fe9708ab688dcda9923f584ba370a66fcbc3811f'; + const String engineCherrypick1 = 'a5a25cd702b062c24b2c67b8d30b5cb33e0ef6f0'; + const String engineCherrypick2 = '94d06a2e1d01a3b0c693b94d70c5e1df9d78d249'; + const String frameworkCherrypick = '768cd702b691584b2c67b8d30b5cb33e0ef6f0'; + const String engineStartingGitHead = '083049e6cae311910c6a6619a6681b7eba4035b4'; + const String engineCurrentGitHead = '23otn2o3itn2o3int2oi3tno23itno2i3tn'; + const String engineCheckoutPath = '/Users/alexchen/Desktop/flutter_conductor_checkouts/engine'; + const String frameworkStartingGitHead = 'df6981e98rh49er8h149er8h19er8h1'; + const String frameworkCurrentGitHead = '239tnint023t09j2039tj0239tn'; + const String frameworkCheckoutPath = '/Users/alexchen/Desktop/flutter_conductor_checkouts/framework'; + final String engineLUCIDashboard = luciConsoleLink(releaseChannel, 'engine'); + final String frameworkLUCIDashboard = luciConsoleLink(releaseChannel, 'flutter'); + + setUp(() { + state = pb.ConductorState( + engine: pb.Repository( + candidateBranch: engineCandidateBranch, + cherrypicks: [ + pb.Cherrypick(trunkRevision: engineCherrypick1), + pb.Cherrypick(trunkRevision: engineCherrypick2), + ], + dartRevision: dartRevision, + workingBranch: workingBranch, + startingGitHead: engineStartingGitHead, + currentGitHead: engineCurrentGitHead, + checkoutPath: engineCheckoutPath, + ), + framework: pb.Repository( + candidateBranch: frameworkCandidateBranch, + cherrypicks: [ + pb.Cherrypick(trunkRevision: frameworkCherrypick), + ], + workingBranch: workingBranch, + startingGitHead: frameworkStartingGitHead, + currentGitHead: frameworkCurrentGitHead, + checkoutPath: frameworkCheckoutPath, + ), + conductorVersion: conductorVersion, + releaseChannel: releaseChannel, + releaseVersion: releaseVersion, + ); + }); testWidgets('Conductor_status displays nothing found when there is no state file', (WidgetTester tester) async { const String testPath = './testPath'; await tester.pumpWidget( @@ -17,7 +69,7 @@ void main() { builder: (BuildContext context, StateSetter setState) { return MaterialApp( home: Material( - child: Column( + child: ListView( children: const [ ConductorStatus( stateFilePath: testPath, @@ -35,57 +87,12 @@ void main() { }); testWidgets('Conductor_status displays correct status with a state file', (WidgetTester tester) async { - const String testPath = './testPath'; - const String conductorVersion = 'v1.0'; - const String releaseChannel = 'beta'; - const String releaseVersion = '1.2.0-3.4.pre'; - const String candidateBranch = 'flutter-1.2-candidate.3'; - const String workingBranch = 'cherrypicks-$candidateBranch'; - const String dartRevision = 'fe9708ab688dcda9923f584ba370a66fcbc3811f'; - const String engineCherrypick1 = 'a5a25cd702b062c24b2c67b8d30b5cb33e0ef6f0'; - const String engineCherrypick2 = '94d06a2e1d01a3b0c693b94d70c5e1df9d78d249'; - const String frameworkCherrypick = '768cd702b691584b2c67b8d30b5cb33e0ef6f0'; - const String engineStartingGitHead = '083049e6cae311910c6a6619a6681b7eba4035b4'; - const String engineCurrentGitHead = '083049e6cae311910c6a6619a6681b7eba4035b4'; - const String engineCheckoutPath = '/Users/alexchen/Desktop/flutter_conductor_checkouts/engine'; - const String frameworkStartingGitHead = '083049e6cae311910c6a6619a6681b7eba4035b4'; - const String frameworkCurrentGitHead = '083049e6cae311910c6a6619a6681b7eba4035b4'; - const String frameworkCheckoutPath = '/Users/alexchen/Desktop/flutter_conductor_checkouts/framework'; - - final pb.ConductorState state = pb.ConductorState( - engine: pb.Repository( - candidateBranch: candidateBranch, - cherrypicks: [ - pb.Cherrypick(trunkRevision: engineCherrypick1), - pb.Cherrypick(trunkRevision: engineCherrypick2), - ], - dartRevision: dartRevision, - workingBranch: workingBranch, - startingGitHead: engineStartingGitHead, - currentGitHead: engineCurrentGitHead, - checkoutPath: engineCheckoutPath, - ), - framework: pb.Repository( - candidateBranch: candidateBranch, - cherrypicks: [ - pb.Cherrypick(trunkRevision: frameworkCherrypick), - ], - workingBranch: workingBranch, - startingGitHead: frameworkStartingGitHead, - currentGitHead: frameworkCurrentGitHead, - checkoutPath: frameworkCheckoutPath, - ), - conductorVersion: conductorVersion, - releaseChannel: releaseChannel, - releaseVersion: releaseVersion, - ); - await tester.pumpWidget( StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return MaterialApp( home: Material( - child: Column( + child: ListView( children: [ ConductorStatus( releaseState: state, @@ -130,9 +137,7 @@ void main() { testWidgets('Conductor_status displays correct status with a null state file except a releaseChannel', (WidgetTester tester) async { - const String testPath = './testPath'; - const String releaseChannel = 'beta'; - final pb.ConductorState state = pb.ConductorState( + final pb.ConductorState stateIncomplete = pb.ConductorState( releaseChannel: releaseChannel, ); @@ -141,10 +146,10 @@ void main() { builder: (BuildContext context, StateSetter setState) { return MaterialApp( home: Material( - child: Column( + child: ListView( children: [ ConductorStatus( - releaseState: state, + releaseState: stateIncomplete, stateFilePath: testPath, ), ], @@ -160,7 +165,7 @@ void main() { expect(find.text('$headerElement:'), findsOneWidget); } expect(find.text(releaseChannel), findsOneWidget); - expect(find.text('Unknown'), findsNWidgets(3)); + expect(find.text('Unknown'), findsNWidgets(11)); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); addTearDown(gesture.removePointer); @@ -176,5 +181,52 @@ void main() { await tester.pumpAndSettle(); expect(find.textContaining('PENDING: The cherrypick has not yet been applied.'), findsOneWidget); }); + + testWidgets('Repo Info section displays corresponding info in a dropdown fashion', (WidgetTester tester) async { + await tester.pumpWidget( + StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return MaterialApp( + home: Material( + child: ListView( + children: [ + ConductorStatus( + releaseState: state, + stateFilePath: testPath, + ), + ], + ), + ), + ); + }, + ), + ); + + expect(find.text('No persistent state file found at $testPath'), findsNothing); + for (final String repoElement in ConductorStatus.engineRepoElements) { + expect(find.text('$repoElement:'), findsOneWidget); + } + for (final String repoElement in ConductorStatus.frameworkRepoElements) { + expect(find.text('$repoElement:'), findsOneWidget); + } + expect(find.text(engineCandidateBranch), findsOneWidget); + expect(find.text(engineStartingGitHead), findsOneWidget); + expect(find.text(engineCurrentGitHead), findsOneWidget); + expect(find.text(engineCheckoutPath), findsOneWidget); + expect(find.text(engineLUCIDashboard), findsOneWidget); + + expect(find.text(frameworkCandidateBranch), findsOneWidget); + expect(find.text(frameworkStartingGitHead), findsOneWidget); + expect(find.text(frameworkCurrentGitHead), findsOneWidget); + expect(find.text(frameworkCheckoutPath), findsOneWidget); + expect(find.text(frameworkLUCIDashboard), findsOneWidget); + + expect(tester.widget(find.byType(ExpansionPanelList).first).children[0].isExpanded, + equals(false)); + await tester.tap(find.byKey(const Key('engineRepoInfoDropdown'))); + await tester.pumpAndSettle(); + expect(tester.widget(find.byType(ExpansionPanelList).first).children[0].isExpanded, + equals(true)); + }); }); }