Fixed tests for Tablet/Desktop layout

This commit is contained in:
Gianfranco Gasbarri 2020-06-11 19:30:24 +01:00
parent fad460dc7b
commit 4557c7e1ec
10 changed files with 368 additions and 299 deletions

View File

@ -61,3 +61,29 @@ class ActionMenuButton extends StatelessWidget {
); );
} }
} }
/// This class is used to differentiate List and View ActionMenuButtons
/// during tests
class ViewActionMenuButton extends StatelessWidget {
const ViewActionMenuButton({
@required this.entity,
@required this.onSelected,
this.isSaving = false,
this.entityActions,
});
final BaseEntity entity;
final List<EntityAction> entityActions;
final Function(BuildContext, EntityAction) onSelected;
final bool isSaving;
@override
Widget build(BuildContext context) {
return ActionMenuButton(
entity: entity,
onSelected: onSelected,
isSaving: isSaving,
entityActions: entityActions
);
}
}

View File

@ -335,6 +335,7 @@ class _AppBottomBarState extends State<AppBottomBar> {
final prefState = store.state.prefState; final prefState = store.state.prefState;
final isList = prefState.moduleLayout == ModuleLayout.list || final isList = prefState.moduleLayout == ModuleLayout.list ||
widget.entityType.isSetting; widget.entityType.isSetting;
final company = state.company;
return BottomAppBar( return BottomAppBar(
shape: CircularNotchedRectangle(), shape: CircularNotchedRectangle(),
@ -384,7 +385,6 @@ class _AppBottomBarState extends State<AppBottomBar> {
), ),
if (widget.statuses.isNotEmpty) if (widget.statuses.isNotEmpty)
IconButton( IconButton(
tooltip: localization.filter,
icon: Icon(Icons.filter), icon: Icon(Icons.filter),
onPressed: _showFilterStatusSheet, onPressed: _showFilterStatusSheet,
color: store.state color: store.state
@ -395,7 +395,6 @@ class _AppBottomBarState extends State<AppBottomBar> {
), ),
if (widget.customValues1.isNotEmpty) if (widget.customValues1.isNotEmpty)
IconButton( IconButton(
tooltip: localization.filter,
icon: Icon(Icons.looks_one), icon: Icon(Icons.looks_one),
onPressed: _showFilterCustom1Sheet, onPressed: _showFilterCustom1Sheet,
color: store.state color: store.state
@ -406,7 +405,6 @@ class _AppBottomBarState extends State<AppBottomBar> {
), ),
if (widget.customValues2.isNotEmpty) if (widget.customValues2.isNotEmpty)
IconButton( IconButton(
tooltip: localization.filter,
icon: Icon(Icons.looks_two), icon: Icon(Icons.looks_two),
onPressed: _showFilterCustom2Sheet, onPressed: _showFilterCustom2Sheet,
color: store.state color: store.state
@ -417,7 +415,6 @@ class _AppBottomBarState extends State<AppBottomBar> {
), ),
if (widget.customValues3.isNotEmpty) if (widget.customValues3.isNotEmpty)
IconButton( IconButton(
tooltip: localization.filter,
icon: Icon(Icons.looks_two), icon: Icon(Icons.looks_two),
onPressed: _showFilterCustom3Sheet, onPressed: _showFilterCustom3Sheet,
color: store.state color: store.state
@ -428,7 +425,6 @@ class _AppBottomBarState extends State<AppBottomBar> {
), ),
if (widget.customValues4.isNotEmpty) if (widget.customValues4.isNotEmpty)
IconButton( IconButton(
tooltip: localization.filter,
icon: Icon(Icons.looks_two), icon: Icon(Icons.looks_two),
onPressed: _showFilterCustom4Sheet, onPressed: _showFilterCustom4Sheet,
color: store.state color: store.state

View File

@ -120,6 +120,7 @@ class _EntityDropdownState extends State<EntityDropdown> {
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
children: <Widget>[ children: <Widget>[
TypeAheadFormField<String>( TypeAheadFormField<String>(
validator: widget.validator,
noItemsFoundBuilder: (context) => SizedBox(), noItemsFoundBuilder: (context) => SizedBox(),
suggestionsBoxDecoration: SuggestionsBoxDecoration( suggestionsBoxDecoration: SuggestionsBoxDecoration(
constraints: BoxConstraints( constraints: BoxConstraints(

View File

@ -243,60 +243,70 @@ class MenuDrawer extends StatelessWidget {
entityType: EntityType.client, entityType: EntityType.client,
icon: getEntityIcon(EntityType.client), icon: getEntityIcon(EntityType.client),
title: localization.clients, title: localization.clients,
iconTooltip: localization.newClient,
), ),
DrawerTile( DrawerTile(
company: company, company: company,
entityType: EntityType.product, entityType: EntityType.product,
icon: getEntityIcon(EntityType.product), icon: getEntityIcon(EntityType.product),
title: localization.products, title: localization.products,
iconTooltip: localization.newProduct,
), ),
DrawerTile( DrawerTile(
company: company, company: company,
entityType: EntityType.invoice, entityType: EntityType.invoice,
icon: getEntityIcon(EntityType.invoice), icon: getEntityIcon(EntityType.invoice),
title: localization.invoices, title: localization.invoices,
iconTooltip: localization.newInvoice,
), ),
DrawerTile( DrawerTile(
company: company, company: company,
entityType: EntityType.payment, entityType: EntityType.payment,
icon: getEntityIcon(EntityType.payment), icon: getEntityIcon(EntityType.payment),
title: localization.payments, title: localization.payments,
iconTooltip: localization.newPayment,
), ),
DrawerTile( DrawerTile(
company: company, company: company,
entityType: EntityType.quote, entityType: EntityType.quote,
icon: getEntityIcon(EntityType.quote), icon: getEntityIcon(EntityType.quote),
title: localization.quotes, title: localization.quotes,
iconTooltip: localization.newQuote,
), ),
DrawerTile( DrawerTile(
company: company, company: company,
entityType: EntityType.credit, entityType: EntityType.credit,
icon: getEntityIcon(EntityType.credit), icon: getEntityIcon(EntityType.credit),
title: localization.credits, title: localization.credits,
iconTooltip: localization.newCredit,
), ),
DrawerTile( DrawerTile(
company: company, company: company,
entityType: EntityType.project, entityType: EntityType.project,
icon: getEntityIcon(EntityType.project), icon: getEntityIcon(EntityType.project),
title: localization.projects, title: localization.projects,
iconTooltip: localization.newProject,
), ),
DrawerTile( DrawerTile(
company: company, company: company,
entityType: EntityType.task, entityType: EntityType.task,
icon: getEntityIcon(EntityType.task), icon: getEntityIcon(EntityType.task),
title: localization.tasks, title: localization.tasks,
iconTooltip: localization.newTask,
), ),
DrawerTile( DrawerTile(
company: company, company: company,
entityType: EntityType.vendor, entityType: EntityType.vendor,
icon: getEntityIcon(EntityType.vendor), icon: getEntityIcon(EntityType.vendor),
title: localization.vendors, title: localization.vendors,
iconTooltip: localization.newVendor,
), ),
DrawerTile( DrawerTile(
company: company, company: company,
entityType: EntityType.expense, entityType: EntityType.expense,
icon: getEntityIcon(EntityType.expense), icon: getEntityIcon(EntityType.expense),
title: localization.expenses, title: localization.expenses,
iconTooltip: localization.newExpense,
), ),
// STARTER: menu - do not remove comment // STARTER: menu - do not remove comment
DrawerTile( DrawerTile(
@ -349,6 +359,7 @@ class DrawerTile extends StatefulWidget {
this.entityType, this.entityType,
this.onLongPress, this.onLongPress,
this.onCreateTap, this.onCreateTap,
this.iconTooltip,
}); });
final CompanyEntity company; final CompanyEntity company;
@ -358,6 +369,7 @@ class DrawerTile extends StatefulWidget {
final Function onTap; final Function onTap;
final Function onLongPress; final Function onLongPress;
final Function onCreateTap; final Function onCreateTap;
final String iconTooltip;
@override @override
_DrawerTileState createState() => _DrawerTileState(); _DrawerTileState createState() => _DrawerTileState();
@ -421,6 +433,7 @@ class _DrawerTileState extends State<DrawerTile> {
); );
} else if (userCompany.canCreate(widget.entityType)) { } else if (userCompany.canCreate(widget.entityType)) {
trailingWidget = IconButton( trailingWidget = IconButton(
tooltip: widget.iconTooltip,
icon: Icon( icon: Icon(
Icons.add_circle_outline, Icons.add_circle_outline,
color: textColor, color: textColor,

View File

@ -88,7 +88,7 @@ class ViewScaffold extends StatelessWidget {
); );
}) })
: Container(), : Container(),
ActionMenuButton( ViewActionMenuButton(
isSaving: state.isSaving, isSaving: state.isSaving,
entity: entity, entity: entity,
onSelected: (context, action) => onSelected: (context, action) =>

View File

@ -237,7 +237,7 @@ class DashboardPanels extends StatelessWidget {
currentData: currentData, currentData: currentData,
previousData: previousData, previousData: previousData,
isLoaded: isLoaded, isLoaded: isLoaded,
title: AppLocalization.of(context).invoices); title: AppLocalization.of(context).invoiced);
} }
Widget _paymentChart(BuildContext context) { Widget _paymentChart(BuildContext context) {

View File

@ -42,8 +42,6 @@ class InvoiceEditDesktopState extends State<InvoiceEditDesktop>
TabController _tabController; TabController _tabController;
FocusNode _focusNode; FocusNode _focusNode;
static final GlobalKey<FormState> _formKey =
GlobalKey<FormState>(debugLabel: '_invoiceDesktopEdit');
final _invoiceNumberController = TextEditingController(); final _invoiceNumberController = TextEditingController();
final _poNumberController = TextEditingController(); final _poNumberController = TextEditingController();
@ -173,10 +171,7 @@ class InvoiceEditDesktopState extends State<InvoiceEditDesktop>
final invoice = viewModel.invoice; final invoice = viewModel.invoice;
final company = viewModel.company; final company = viewModel.company;
return AppForm( return ListView(
formKey: _formKey,
focusNode: _focusNode,
child: ListView(
key: ValueKey('__invoice_${invoice.id}__'), key: ValueKey('__invoice_${invoice.id}__'),
children: <Widget>[ children: <Widget>[
Row( Row(
@ -286,10 +281,9 @@ class InvoiceEditDesktopState extends State<InvoiceEditDesktop>
: widget.entityType == EntityType.quote : widget.entityType == EntityType.quote
? localization.quoteNumber ? localization.quoteNumber
: localization.invoiceNumber, : localization.invoiceNumber,
validator: (String val) => validator: (String val) => val.trim().isEmpty &&
val.trim().isEmpty && invoice.isOld invoice.isOld
? AppLocalization.of(context) ? AppLocalization.of(context).pleaseEnterAnInvoiceNumber
.pleaseEnterAnInvoiceNumber
: null, : null,
), ),
DecoratedFormField( DecoratedFormField(
@ -432,16 +426,16 @@ class InvoiceEditDesktopState extends State<InvoiceEditDesktop>
), ),
if (company.settings.enableSecondInvoiceTaxRate) if (company.settings.enableSecondInvoiceTaxRate)
TaxRateDropdown( TaxRateDropdown(
onSelected: (taxRate) => viewModel.onChanged( onSelected: (taxRate) => viewModel
invoice.applyTax(taxRate, isSecond: true)), .onChanged(invoice.applyTax(taxRate, isSecond: true)),
labelText: localization.tax, labelText: localization.tax,
initialTaxName: invoice.taxName2, initialTaxName: invoice.taxName2,
initialTaxRate: invoice.taxRate2, initialTaxRate: invoice.taxRate2,
), ),
if (company.settings.enableThirdInvoiceTaxRate) if (company.settings.enableThirdInvoiceTaxRate)
TaxRateDropdown( TaxRateDropdown(
onSelected: (taxRate) => viewModel.onChanged( onSelected: (taxRate) => viewModel
invoice.applyTax(taxRate, isThird: true)), .onChanged(invoice.applyTax(taxRate, isThird: true)),
labelText: localization.tax, labelText: localization.tax,
initialTaxName: invoice.taxName3, initialTaxName: invoice.taxName3,
initialTaxRate: invoice.taxRate3, initialTaxRate: invoice.taxRate3,
@ -459,7 +453,6 @@ class InvoiceEditDesktopState extends State<InvoiceEditDesktop>
], ],
), ),
], ],
),
); );
} }
} }

View File

@ -136,6 +136,7 @@ class _InvoiceEditItemsDesktopState extends State<InvoiceEditItemsDesktop> {
Padding( Padding(
padding: const EdgeInsets.only(right: kTableColumnGap), padding: const EdgeInsets.only(right: kTableColumnGap),
child: TypeAheadFormField<String>( child: TypeAheadFormField<String>(
key: ValueKey('__line_item_${index}_name__'),
initialValue: lineItems[index].productKey, initialValue: lineItems[index].productKey,
noItemsFoundBuilder: (context) => SizedBox(), noItemsFoundBuilder: (context) => SizedBox(),
suggestionsCallback: (pattern) { suggestionsCallback: (pattern) {
@ -238,6 +239,7 @@ class _InvoiceEditItemsDesktopState extends State<InvoiceEditItemsDesktop> {
Padding( Padding(
padding: const EdgeInsets.only(right: kTableColumnGap), padding: const EdgeInsets.only(right: kTableColumnGap),
child: TextFormField( child: TextFormField(
key: ValueKey('__line_item_${index}_description__'),
initialValue: lineItems[index].notes, initialValue: lineItems[index].notes,
onChanged: (value) => viewModel.onChangedInvoiceItem( onChanged: (value) => viewModel.onChangedInvoiceItem(
lineItems[index].rebuild((b) => b..notes = value), lineItems[index].rebuild((b) => b..notes = value),
@ -346,6 +348,7 @@ class _InvoiceEditItemsDesktopState extends State<InvoiceEditItemsDesktop> {
Padding( Padding(
padding: const EdgeInsets.only(right: kTableColumnGap), padding: const EdgeInsets.only(right: kTableColumnGap),
child: TextFormField( child: TextFormField(
key: ValueKey('__line_item_${index}_cost__'),
textAlign: TextAlign.right, textAlign: TextAlign.right,
initialValue: formatNumber(lineItems[index].cost, context, initialValue: formatNumber(lineItems[index].cost, context,
formatNumberType: FormatNumberType.input, formatNumberType: FormatNumberType.input,
@ -361,6 +364,7 @@ class _InvoiceEditItemsDesktopState extends State<InvoiceEditItemsDesktop> {
Padding( Padding(
padding: const EdgeInsets.only(right: kTableColumnGap), padding: const EdgeInsets.only(right: kTableColumnGap),
child: TextFormField( child: TextFormField(
key: ValueKey('__line_item_${index}_quantity__'),
textAlign: TextAlign.right, textAlign: TextAlign.right,
initialValue: formatNumber( initialValue: formatNumber(
lineItems[index].quantity, context, lineItems[index].quantity, context,

View File

@ -18,6 +18,8 @@ void runTestSuite({bool batchMode = false}) {
final poNumber = final poNumber =
faker.randomGenerator.integer(999999, min: 100000).toString(); faker.randomGenerator.integer(999999, min: 100000).toString();
final productKey = makeUnique(faker.food.cuisine()); final productKey = makeUnique(faker.food.cuisine());
final clientKey = faker.randomGenerator.integer(999999, min: 100000)
.toString();
final description = faker.lorem.sentences(5).toString(); final description = faker.lorem.sentences(5).toString();
final cost = final cost =
faker.randomGenerator.decimal(min: 50, scale: 10).toStringAsFixed(2); faker.randomGenerator.decimal(min: 50, scale: 10).toStringAsFixed(2);
@ -67,21 +69,30 @@ void runTestSuite({bool batchMode = false}) {
// Create a new invoice // Create a new invoice
test('Add a new invoice', () async { test('Add a new invoice', () async {
print('Tap new invoice'); print('Tap new invoice');
await driver.tap(find.byTooltip(localization.newInvoice)); await driver.tap(find.byTooltip(localization.newInvoice));
print('Create new client: $clientName'); print('Create new client: $clientName');
if(await isMobile(driver)) {
await driver.tap(find.byValueKey(Keys.clientPickerEmptyKey)); await driver.tap(find.byValueKey(Keys.clientPickerEmptyKey));
}
await driver.tap(find.byTooltip(localization.createNew)); await driver.tap(find.byTooltip(localization.createNew));
print('Fill the client form'); print('Fill the client form');
await fillTextField( await fillTextFields(driver, <String, String>{
driver: driver, field: localization.name, value: clientName); localization.name: clientName,
localization.idNumber: clientKey
});
// Await for Debouncer // Await for Debouncer
await Future<dynamic>.delayed(Duration(milliseconds: 500)); await Future<dynamic>.delayed(Duration(milliseconds: 500));
await driver.tap(find.text(localization.save)); await driver.tap(find.text(localization.save));
// Await for Screen change
await driver.waitFor(find.text(localization.newInvoice));
print('Fill the invoice form'); print('Fill the invoice form');
if(await isMobile(driver)) {
await driver.tap(find.byTooltip(localization.addItem)); await driver.tap(find.byTooltip(localization.addItem));
await driver.tap(find.byTooltip(localization.createNew)); await driver.tap(find.byTooltip(localization.createNew));
@ -97,6 +108,15 @@ void runTestSuite({bool batchMode = false}) {
await driver.tap(find.text(localization.done)); await driver.tap(find.text(localization.done));
await driver.tap(find.text(localization.details)); await driver.tap(find.text(localization.details));
} else {
await fillTextFields(driver, <String, String>{
getLineItemKey('name', 0): productKey,
getLineItemKey('description', 0): description,
getLineItemKey('cost', 0): cost,
getLineItemKey('quantity', 0): '1'
});
}
await fillAndSaveForm(driver, <String, String>{ await fillAndSaveForm(driver, <String, String>{
localization.poNumber: poNumber, localization.poNumber: poNumber,
}); });
@ -140,7 +160,8 @@ void runTestSuite({bool batchMode = false}) {
await selectAction(driver, localization.enterPayment); await selectAction(driver, localization.enterPayment);
await driver.tap(find.text(localization.save)); await driver.tap(find.text(localization.save));
// "Completed" status // "Completed" status
await driver.waitFor(find.text(localization.paymentStatus4.toUpperCase())); await driver.waitFor(
find.text(localization.paymentStatus4.toUpperCase()));
if (await isMobile(driver)) { if (await isMobile(driver)) {
await driver.tap(find.pageBack()); await driver.tap(find.pageBack());

View File

@ -9,6 +9,7 @@ import 'localizations.dart';
class Keys { class Keys {
static const String openAppDrawer = 'Open navigation menu'; static const String openAppDrawer = 'Open navigation menu';
static const String clientPickerEmptyKey = '__client___'; static const String clientPickerEmptyKey = '__client___';
static const String invoiceLineItemBaseKey = '__line_item';
} }
Future<bool> isTablet(FlutterDriver driver) async { Future<bool> isTablet(FlutterDriver driver) async {
@ -129,15 +130,15 @@ Future<void> fillAndSaveForm(FlutterDriver driver, Map<String, dynamic> values,
// Await for Debouncer // Await for Debouncer
await Future<dynamic>.delayed(Duration(milliseconds: 400)); await Future<dynamic>.delayed(Duration(milliseconds: 400));
print('Check for updated values');
await checkTextFields(driver, values, except: skipCheckFor);
print('Tap save'); print('Tap save');
await driver.tap(find.text(localization.save)); await driver.tap(find.text(localization.save));
// verify snackbar // verify snackbar
//await driver.waitFor(find.text(localization.updatedProduct)); //await driver.waitFor(find.text(localization.updatedProduct));
//await driver.tap(find.pageBack()); //await driver.tap(find.pageBack());
print('Check for updated values');
await checkTextFields(driver, values, except: skipCheckFor);
} }
Future<void> testArchiveAndDelete( Future<void> testArchiveAndDelete(
@ -146,32 +147,46 @@ Future<void> testArchiveAndDelete(
String deletedMessage, String deletedMessage,
String restoredMessage}) async { String restoredMessage}) async {
final localization = TestLocalization('en'); final localization = TestLocalization('en');
final mobile = await isMobile(driver);
if (!mobile) {
// Show archived and deleted entries on tablet/web
await driver.tap(find.byTooltip(localization.filter));
await driver.tap(find.text(localization.archived));
await driver.tap(find.text(localization.deleted));
await driver.tap(find.byTooltip(localization.filter));
}
print('Archive record'); print('Archive record');
await selectAction(driver, localization.archive); await selectAction(driver, localization.archive);
await driver.waitFor(find.text(archivedMessage)); await driver.waitFor(find.text(archivedMessage));
await driver.waitFor(find.text(localization.archived)); //await driver.waitFor(find.text(localization.archived));
print('Restore record'); print('Restore record');
await selectAction(driver, localization.restore); await selectAction(driver, localization.restore);
await driver.waitFor(find.text(restoredMessage)); await driver.waitFor(find.text(restoredMessage));
await driver.waitForAbsent(find.text(localization.archived)); await driver.waitForAbsent(find.byType('Snackbar'));
print('Delete record'); print('Delete record');
await selectAction(driver, localization.delete); await selectAction(driver, localization.delete);
await driver.waitFor(find.text(deletedMessage)); await driver.waitFor(find.text(deletedMessage));
await driver.waitFor(find.text(localization.deleted)); //await driver.waitFor(find.text(localization.deleted));
print('Restore record'); print('Restore record');
await selectAction(driver, localization.restore); await selectAction(driver, localization.restore);
await driver.waitFor(find.text(restoredMessage)); await driver.waitFor(find.text(restoredMessage));
await driver.waitForAbsent(find.text(localization.deleted)); await driver.waitForAbsent(find.byType('Snackbar'));
} }
Future<void> selectAction(FlutterDriver driver, String action) async { Future<void> selectAction(FlutterDriver driver, String action) async {
await driver.tap(find.byType('ActionMenuButton')); await driver.tap(find.byType('ViewActionMenuButton'));
await driver.tap(find.text(action)); await driver.tap(find.text(action));
} }
String makeUnique(String value) => String makeUnique(String value) =>
'$value ${faker.randomGenerator.integer(999999, min: 100000)}'; '$value ${faker.randomGenerator.integer(999999, min: 100000)}';
String getLineItemKey(String key, int index) =>
'${Keys.invoiceLineItemBaseKey}_${index}_${key}__';