Skip to content

Commit

Permalink
Adds MemoTerminal widget and its tests - closes #219
Browse files Browse the repository at this point in the history
  • Loading branch information
ggirotto committed Nov 17, 2021
1 parent 195518e commit b067304
Show file tree
Hide file tree
Showing 3 changed files with 200 additions and 0 deletions.
14 changes: 14 additions & 0 deletions lib/application/constants/strings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import 'package:memo/domain/enums/resource_type.dart';
//
const oops = 'Oops';
const tryAgain = 'Tente Novamente';
const cancel = 'Cancelar';

//
// Collections
Expand Down Expand Up @@ -71,6 +72,19 @@ const newCollection = 'Nova Coleção';
const editCollection = 'Editar Coleção';
const saveCollection = 'Salvar Coleção';

const collectionName = 'Nome da Coleção';
const collectionDescription = 'Descrição da Coleção';

String updateMemoQuestionTitle(int memoIndex) => '### Pergunta $memoIndex';
const updateMemoQuestionPlaceholder = 'Digite a questão';

const updateMemoAnswer = '### Resposta';
const updateMemoAnswerPlaceholder = 'Digite a resposta';

const remove = 'Remover';
const removeMemoTitle = 'Remover Memo';
const removeMemoMessage = 'Você tem certeza que deseja remover este memo?';

//
// Tags Component
//
Expand Down
121 changes: 121 additions & 0 deletions lib/application/widgets/theme/memo_terminal.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:layoutr/common_layout.dart';
import 'package:memo/application/constants/dimensions.dart' as dimens;
import 'package:memo/application/constants/images.dart' as images;
import 'package:memo/application/constants/strings.dart' as strings;
import 'package:memo/application/theme/theme_controller.dart';
import 'package:memo/application/utils/bottom_sheet.dart';
import 'package:memo/application/widgets/theme/custom_button.dart';
import 'package:memo/application/widgets/theme/rich_text_field.dart';
import 'package:memo/application/widgets/theme/terminal_window.dart';

/// A terminal-styled component that presents a `Memo` question and answer that can be updated.
///
/// Use [questionController] and [answerController] to control the Memo content being edited.
class MemoTerminal extends HookWidget {
const MemoTerminal({
required this.memoIndex,
this.questionController,
this.answerController,
this.onRemove,
this.scrollController,
});

/// The index of the current memo in the `Collection` `Memo`'s list.
final int memoIndex;

/// Controls the `Memo` question content.
final TextEditingController? questionController;

/// Controls the `Memo` answer content.
final TextEditingController? answerController;

/// Triggers when the current memo should be removed from the Collection Memos.
final VoidCallback? onRemove;

final ScrollController? scrollController;

@override
Widget build(BuildContext context) {
final theme = useTheme();
final textTheme = Theme.of(context).textTheme;

final questionController = this.questionController ?? useTextEditingController();
final questionTitle = Text(
strings.updateMemoQuestionTitle(memoIndex),
style: textTheme.bodyText1?.copyWith(color: theme.secondarySwatch),
);
final questionField = Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
questionTitle,
context.verticalBox(Spacing.small),
RichTextField(
modalTitle: questionTitle,
placeholder: strings.updateMemoQuestionPlaceholder,
controller: questionController,
),
],
);

final answerController = this.answerController ?? useTextEditingController();
final answerTitle = Text(
strings.updateMemoAnswer,
style: textTheme.bodyText1?.copyWith(color: theme.primarySwatch),
);
final answerField = Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
answerTitle,
context.verticalBox(Spacing.small),
RichTextField(
modalTitle: answerTitle,
placeholder: strings.updateMemoAnswerPlaceholder,
controller: answerController,
),
],
);

Future<void> removeDialogConfirmation() => showDestructiveOperationModalBottomSheet(
context,
title: strings.removeMemoTitle,
message: strings.removeMemoMessage,
destructiveActionTitle: strings.remove.toUpperCase(),
cancelActionTitle: strings.cancel.toUpperCase(),
onDestructiveTapped: () {
onRemove!();
Navigator.of(context).pop();
},
onCancelTapped: Navigator.of(context).pop,
);

final trashButton = CustomTextButton(
color: theme.destructiveSwatch,
text: strings.remove.toUpperCase(),
leadingAsset: images.trashAsset,
onPressed: onRemove != null ? removeDialogConfirmation : null,
);

final borderColor = theme.neutralSwatch.shade700;
final fadeGradient = [theme.neutralSwatch.shade900, theme.neutralSwatch.shade900.withOpacity(0)];
return TerminalWindow(
borderColor: borderColor,
fadeGradient: fadeGradient,
body: SingleChildScrollView(
controller: scrollController,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: dimens.terminalWindowHeaderHeight),
questionField,
context.verticalBox(Spacing.large),
answerField,
context.verticalBox(Spacing.large),
trashButton,
],
).withAllPadding(context, Spacing.medium),
),
);
}
}
65 changes: 65 additions & 0 deletions test/application/widgets/theme/memo_terminal_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:memo/application/constants/strings.dart' as strings;
import 'package:memo/application/widgets/theme/custom_button.dart';
import 'package:memo/application/widgets/theme/destructive_button.dart';
import 'package:memo/application/widgets/theme/memo_terminal.dart';
import 'package:mocktail/mocktail.dart';

import '../../../utils/fakes.dart';
import '../../../utils/widget_pump.dart';

void main() {
testWidgets('should become scrollable when the content exceeds the available height', (tester) async {
const hugeFakeText = r'A\nHuge\nFake\nText\nThat\nWill\nExpand\nThe\nRich\nText\nEditor';
const encodedText = '[{"insert":"$hugeFakeText\\n"}]';
final scrollController = ScrollController();
final questionController = TextEditingController(text: encodedText);
final answerController = TextEditingController(text: encodedText);
// Wraps `MemoTerminal` in a tiny SizedBox to force its content to be scrollable
final memoTerminal = SizedBox.square(
dimension: 320,
child: MemoTerminal(
memoIndex: 0,
questionController: questionController,
answerController: answerController,
scrollController: scrollController,
),
);
const dragOffset = 500.0;

await pumpProviderScoped(tester, memoTerminal);
await tester.drag(find.byType(MemoTerminal), const Offset(0, -dragOffset));
await tester.pump();

expect(scrollController.offset, greaterThan(0));
});

testWidgets('should present confirmation modal when the remove button is tapped', (tester) async {
final onRemoveCallback = MockCallbackFunction();
final memoTerminal = MemoTerminal(memoIndex: 0, onRemove: onRemoveCallback);

await pumpProviderScoped(tester, memoTerminal);

await tester.tap(find.byType(CustomTextButton));
await tester.pumpAndSettle();

final confirmationTitle = find.text(strings.removeMemoTitle);
expect(confirmationTitle, findsOneWidget);
});

testWidgets('should trigger onRemove when confirming memo removal', (tester) async {
final onRemoveCallback = MockCallbackFunction();
final memoTerminal = MemoTerminal(memoIndex: 0, onRemove: onRemoveCallback);

await pumpProviderScoped(tester, memoTerminal);

await tester.tap(find.byType(CustomTextButton));
await tester.pumpAndSettle();

await tester.tap(find.byType(DestructiveButton));
await tester.pumpAndSettle();

verify(onRemoveCallback.call).called(1);
});
}

0 comments on commit b067304

Please sign in to comment.