mirror of
https://github.com/wger-project/flutter.git
synced 2026-02-18 00:17:48 +01:00
Some polishing, add simple test
This commit is contained in:
@@ -618,6 +618,7 @@
|
||||
},
|
||||
"edit": "Edit",
|
||||
"@edit": {},
|
||||
"preview": "Preview",
|
||||
"loadingText": "Loading...",
|
||||
"@loadingText": {
|
||||
"description": "Text to show when entries are being loaded in the background: Loading..."
|
||||
@@ -913,6 +914,13 @@
|
||||
"@baseData": {
|
||||
"description": "The base data for an exercise such as category, trained muscles, etc."
|
||||
},
|
||||
"useBasicMarkdown": "You can use basic Markdown to format the text",
|
||||
"editorBold": "Bold",
|
||||
"@editorBold": {
|
||||
"description": "Label for bold formatting"
|
||||
},
|
||||
"editorItalic": "Italic",
|
||||
"editorList": "List",
|
||||
"enterTextInLanguage": "Please enter the text in the correct language!",
|
||||
"settingsTitle": "Settings",
|
||||
"settingsCacheTitle": "Cache",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (C) 2020, 2021 wger Team
|
||||
* Copyright (c) 2020 - 2026 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -51,10 +51,15 @@ sealed class ExerciseCommentSubmissionApi with _$ExerciseCommentSubmissionApi {
|
||||
sealed class ExerciseTranslationSubmissionApi with _$ExerciseTranslationSubmissionApi {
|
||||
const factory ExerciseTranslationSubmissionApi({
|
||||
required String name,
|
||||
required String description,
|
||||
|
||||
@JsonKey(name: 'description_source') required String description,
|
||||
|
||||
required int language,
|
||||
|
||||
@JsonKey(name: 'license_author') required String author,
|
||||
|
||||
@Default([]) List<ExerciseAliasSubmissionApi> aliases,
|
||||
|
||||
@Default([]) List<ExerciseCommentSubmissionApi> comments,
|
||||
}) = _ExerciseTranslationSubmissionApi;
|
||||
|
||||
@@ -67,12 +72,19 @@ sealed class ExerciseTranslationSubmissionApi with _$ExerciseTranslationSubmissi
|
||||
sealed class ExerciseSubmissionApi with _$ExerciseSubmissionApi {
|
||||
const factory ExerciseSubmissionApi({
|
||||
required int category,
|
||||
|
||||
required List<int> muscles,
|
||||
|
||||
@JsonKey(name: 'muscles_secondary') required List<int> musclesSecondary,
|
||||
|
||||
required List<int> equipment,
|
||||
|
||||
@JsonKey(name: 'license_author') required String author,
|
||||
|
||||
@JsonKey(includeToJson: true) int? variation,
|
||||
|
||||
@JsonKey(includeToJson: true, name: 'variations_connect_to') int? variationConnectTo,
|
||||
|
||||
required List<ExerciseTranslationSubmissionApi> translations,
|
||||
}) = _ExerciseSubmissionApi;
|
||||
|
||||
|
||||
@@ -529,7 +529,7 @@ as String,
|
||||
/// @nodoc
|
||||
mixin _$ExerciseTranslationSubmissionApi {
|
||||
|
||||
String get name; String get description; int get language;@JsonKey(name: 'license_author') String get author; List<ExerciseAliasSubmissionApi> get aliases; List<ExerciseCommentSubmissionApi> get comments;
|
||||
String get name;@JsonKey(name: 'description_source') String get description; int get language;@JsonKey(name: 'license_author') String get author; List<ExerciseAliasSubmissionApi> get aliases; List<ExerciseCommentSubmissionApi> get comments;
|
||||
/// Create a copy of ExerciseTranslationSubmissionApi
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -562,7 +562,7 @@ abstract mixin class $ExerciseTranslationSubmissionApiCopyWith<$Res> {
|
||||
factory $ExerciseTranslationSubmissionApiCopyWith(ExerciseTranslationSubmissionApi value, $Res Function(ExerciseTranslationSubmissionApi) _then) = _$ExerciseTranslationSubmissionApiCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String name, String description, int language,@JsonKey(name: 'license_author') String author, List<ExerciseAliasSubmissionApi> aliases, List<ExerciseCommentSubmissionApi> comments
|
||||
String name,@JsonKey(name: 'description_source') String description, int language,@JsonKey(name: 'license_author') String author, List<ExerciseAliasSubmissionApi> aliases, List<ExerciseCommentSubmissionApi> comments
|
||||
});
|
||||
|
||||
|
||||
@@ -669,7 +669,7 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String name, String description, int language, @JsonKey(name: 'license_author') String author, List<ExerciseAliasSubmissionApi> aliases, List<ExerciseCommentSubmissionApi> comments)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String name, @JsonKey(name: 'description_source') String description, int language, @JsonKey(name: 'license_author') String author, List<ExerciseAliasSubmissionApi> aliases, List<ExerciseCommentSubmissionApi> comments)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ExerciseTranslationSubmissionApi() when $default != null:
|
||||
return $default(_that.name,_that.description,_that.language,_that.author,_that.aliases,_that.comments);case _:
|
||||
@@ -690,7 +690,7 @@ return $default(_that.name,_that.description,_that.language,_that.author,_that.a
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String name, String description, int language, @JsonKey(name: 'license_author') String author, List<ExerciseAliasSubmissionApi> aliases, List<ExerciseCommentSubmissionApi> comments) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String name, @JsonKey(name: 'description_source') String description, int language, @JsonKey(name: 'license_author') String author, List<ExerciseAliasSubmissionApi> aliases, List<ExerciseCommentSubmissionApi> comments) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ExerciseTranslationSubmissionApi():
|
||||
return $default(_that.name,_that.description,_that.language,_that.author,_that.aliases,_that.comments);}
|
||||
@@ -707,7 +707,7 @@ return $default(_that.name,_that.description,_that.language,_that.author,_that.a
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String name, String description, int language, @JsonKey(name: 'license_author') String author, List<ExerciseAliasSubmissionApi> aliases, List<ExerciseCommentSubmissionApi> comments)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String name, @JsonKey(name: 'description_source') String description, int language, @JsonKey(name: 'license_author') String author, List<ExerciseAliasSubmissionApi> aliases, List<ExerciseCommentSubmissionApi> comments)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ExerciseTranslationSubmissionApi() when $default != null:
|
||||
return $default(_that.name,_that.description,_that.language,_that.author,_that.aliases,_that.comments);case _:
|
||||
@@ -722,11 +722,11 @@ return $default(_that.name,_that.description,_that.language,_that.author,_that.a
|
||||
@JsonSerializable()
|
||||
|
||||
class _ExerciseTranslationSubmissionApi implements ExerciseTranslationSubmissionApi {
|
||||
const _ExerciseTranslationSubmissionApi({required this.name, required this.description, required this.language, @JsonKey(name: 'license_author') required this.author, final List<ExerciseAliasSubmissionApi> aliases = const [], final List<ExerciseCommentSubmissionApi> comments = const []}): _aliases = aliases,_comments = comments;
|
||||
const _ExerciseTranslationSubmissionApi({required this.name, @JsonKey(name: 'description_source') required this.description, required this.language, @JsonKey(name: 'license_author') required this.author, final List<ExerciseAliasSubmissionApi> aliases = const [], final List<ExerciseCommentSubmissionApi> comments = const []}): _aliases = aliases,_comments = comments;
|
||||
factory _ExerciseTranslationSubmissionApi.fromJson(Map<String, dynamic> json) => _$ExerciseTranslationSubmissionApiFromJson(json);
|
||||
|
||||
@override final String name;
|
||||
@override final String description;
|
||||
@override@JsonKey(name: 'description_source') final String description;
|
||||
@override final int language;
|
||||
@override@JsonKey(name: 'license_author') final String author;
|
||||
final List<ExerciseAliasSubmissionApi> _aliases;
|
||||
@@ -777,7 +777,7 @@ abstract mixin class _$ExerciseTranslationSubmissionApiCopyWith<$Res> implements
|
||||
factory _$ExerciseTranslationSubmissionApiCopyWith(_ExerciseTranslationSubmissionApi value, $Res Function(_ExerciseTranslationSubmissionApi) _then) = __$ExerciseTranslationSubmissionApiCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String name, String description, int language,@JsonKey(name: 'license_author') String author, List<ExerciseAliasSubmissionApi> aliases, List<ExerciseCommentSubmissionApi> comments
|
||||
String name,@JsonKey(name: 'description_source') String description, int language,@JsonKey(name: 'license_author') String author, List<ExerciseAliasSubmissionApi> aliases, List<ExerciseCommentSubmissionApi> comments
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ _ExerciseTranslationSubmissionApi _$ExerciseTranslationSubmissionApiFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => _ExerciseTranslationSubmissionApi(
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String,
|
||||
description: json['description_source'] as String,
|
||||
language: (json['language'] as num).toInt(),
|
||||
author: json['license_author'] as String,
|
||||
aliases:
|
||||
@@ -51,7 +51,7 @@ Map<String, dynamic> _$ExerciseTranslationSubmissionApiToJson(
|
||||
_ExerciseTranslationSubmissionApi instance,
|
||||
) => <String, dynamic>{
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
'description_source': instance.description,
|
||||
'language': instance.language,
|
||||
'license_author': instance.author,
|
||||
'aliases': instance.aliases,
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_html/flutter_html.dart';
|
||||
import 'package:markdown/markdown.dart' as md;
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
|
||||
class AddExerciseTextArea extends StatelessWidget {
|
||||
final ValueChanged<String> onChange;
|
||||
@@ -148,30 +149,52 @@ class _MarkdownEditorState extends State<MarkdownEditor> {
|
||||
final newText = '$before$left$selected$right$after';
|
||||
_controller.text = newText;
|
||||
|
||||
final cursorPos = start + left.length + selected.length + right.length;
|
||||
_controller.selection = TextSelection.collapsed(offset: cursorPos);
|
||||
// No selection: place cursor between the inserted markers
|
||||
if (selected.isEmpty) {
|
||||
final cursorPos = start + left.length;
|
||||
_controller.selection = TextSelection.collapsed(offset: cursorPos);
|
||||
} else {
|
||||
// otherwise: keep the text selected
|
||||
final base = start + left.length;
|
||||
final extent = base + selected.length;
|
||||
_controller.selection = TextSelection(baseOffset: base, extentOffset: extent);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildToolbar() {
|
||||
final i18n = AppLocalizations.of(context);
|
||||
|
||||
if (!widget.showToolbar || widget.readOnly) return const SizedBox.shrink();
|
||||
return Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.format_bold),
|
||||
tooltip: 'Bold',
|
||||
tooltip: i18n.editorBold,
|
||||
onPressed: () => _surroundSelection('**', '**'),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.format_italic),
|
||||
tooltip: 'Cursive',
|
||||
tooltip: i18n.editorItalic,
|
||||
onPressed: () => _surroundSelection('*', '*'),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.format_list_bulleted),
|
||||
tooltip: i18n.editorList,
|
||||
onPressed: () => _surroundSelection('\n- ', '\n- \n- '),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.format_list_numbered),
|
||||
tooltip: i18n.editorList,
|
||||
onPressed: () => _surroundSelection('\n1. ', '\n1. \n1. '),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final i18n = AppLocalizations.of(context);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: DefaultTabController(
|
||||
@@ -183,9 +206,9 @@ class _MarkdownEditorState extends State<MarkdownEditor> {
|
||||
TabBar(
|
||||
labelColor: Theme.of(context).colorScheme.primary,
|
||||
unselectedLabelColor: Theme.of(context).textTheme.bodySmall?.color,
|
||||
tabs: const [
|
||||
Tab(text: 'Edit'),
|
||||
Tab(text: 'Preview'),
|
||||
tabs: [
|
||||
Tab(text: i18n.edit),
|
||||
Tab(text: i18n.preview),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@@ -207,7 +230,7 @@ class _MarkdownEditorState extends State<MarkdownEditor> {
|
||||
validator: widget.validator,
|
||||
onSaved: widget.onSaved,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'You can use basic Markdown formatting here',
|
||||
hintText: i18n.useBasicMarkdown,
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
@@ -234,11 +257,7 @@ class _MarkdownEditorState extends State<MarkdownEditor> {
|
||||
child: Builder(
|
||||
builder: (ctx) {
|
||||
final raw = _controller.text;
|
||||
final html = md.markdownToHtml(raw);
|
||||
if (html.trim().isEmpty) {
|
||||
return SelectableText(raw.isEmpty ? '(empty)' : raw);
|
||||
}
|
||||
return Html(data: html);
|
||||
return Html(data: md.markdownToHtml(raw));
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -27,7 +27,6 @@ import 'package:wger/models/exercises/equipment.dart';
|
||||
import 'package:wger/models/exercises/muscle.dart';
|
||||
import 'package:wger/providers/add_exercise.dart';
|
||||
import 'package:wger/providers/exercises.dart';
|
||||
import 'package:wger/providers/user.dart';
|
||||
import 'package:wger/widgets/add_exercise/add_exercise_multiselect_button.dart';
|
||||
import 'package:wger/widgets/add_exercise/add_exercise_text_area.dart';
|
||||
import 'package:wger/widgets/exercises/exercises.dart';
|
||||
@@ -40,7 +39,6 @@ class Step1Basics extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final userProvider = context.read<UserProvider>();
|
||||
final addExerciseProvider = context.read<AddExerciseProvider>();
|
||||
final exerciseProvider = context.read<ExercisesProvider>();
|
||||
final categories = exerciseProvider.categories;
|
||||
@@ -78,7 +76,7 @@ class Step1Basics extends StatelessWidget {
|
||||
title: '${AppLocalizations.of(context).author}*',
|
||||
isMultiline: false,
|
||||
validator: (name) => validateAuthorName(name, context),
|
||||
initialValue: addExerciseProvider.author ?? userProvider.profile!.username,
|
||||
initialValue: addExerciseProvider.author,
|
||||
onChange: (v) => addExerciseProvider.author = v,
|
||||
onSaved: (String? author) => addExerciseProvider.author = author!,
|
||||
),
|
||||
|
||||
@@ -107,6 +107,7 @@ class _Step4TranslationState extends State<Step4Translation> {
|
||||
),
|
||||
Consumer<AddExerciseProvider>(
|
||||
builder: (ctx, provider, __) => AddExerciseTextArea(
|
||||
useMarkdownEditor: true,
|
||||
initialValue: provider.descriptionTrans ?? '',
|
||||
onChange: (value) => provider.descriptionTrans = value,
|
||||
title: '${i18n.description}*',
|
||||
|
||||
@@ -1,4 +1,24 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (c) 2026 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_html/flutter_html.dart';
|
||||
import 'package:markdown/markdown.dart' as md;
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/providers/add_exercise.dart';
|
||||
@@ -14,6 +34,7 @@ class Step6Overview extends StatelessWidget {
|
||||
builder: (ctx, provider, _) => Column(
|
||||
spacing: 8,
|
||||
children: [
|
||||
// Base data
|
||||
Text(i18n.baseData, style: Theme.of(context).textTheme.headlineSmall),
|
||||
Table(
|
||||
columnWidths: const {0: FlexColumnWidth(2), 1: FlexColumnWidth(3)},
|
||||
@@ -30,7 +51,12 @@ class Step6Overview extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
TableRow(children: [Text(i18n.description), Text(provider.descriptionEn ?? '...')]),
|
||||
TableRow(
|
||||
children: [
|
||||
Text(i18n.description),
|
||||
Html(data: md.markdownToHtml(provider.descriptionEn ?? '...')),
|
||||
],
|
||||
),
|
||||
TableRow(children: [Text(i18n.category), Text(provider.category?.name ?? '...')]),
|
||||
TableRow(
|
||||
children: [
|
||||
@@ -76,6 +102,8 @@ class Step6Overview extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Translation
|
||||
Text(i18n.translation, style: Theme.of(context).textTheme.headlineSmall),
|
||||
Table(
|
||||
columnWidths: const {0: FlexColumnWidth(2), 1: FlexColumnWidth(3)},
|
||||
@@ -92,7 +120,15 @@ class Step6Overview extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
children: [Text(i18n.description), Text(provider.descriptionTrans ?? '...')],
|
||||
children: [
|
||||
Text(i18n.description),
|
||||
|
||||
// for consistent formatting, the thml element has other padding, etc
|
||||
if (provider.descriptionTrans == null)
|
||||
const Text('...')
|
||||
else
|
||||
Html(data: md.markdownToHtml(provider.descriptionTrans!)),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
children: [
|
||||
@@ -102,6 +138,8 @@ class Step6Overview extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Images
|
||||
if (provider.exerciseImages.isNotEmpty)
|
||||
PreviewExerciseImages(selectedImages: provider.exerciseImages, allowEdit: false),
|
||||
InfoCard(text: i18n.checkInformationBeforeSubmitting),
|
||||
|
||||
@@ -2259,6 +2259,17 @@ class MockAppLocalizations extends _i1.Mock implements _i2.AppLocalizations {
|
||||
)
|
||||
as String);
|
||||
|
||||
@override
|
||||
String get preview =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#preview),
|
||||
returnValue: _i3.dummyValue<String>(
|
||||
this,
|
||||
Invocation.getter(#preview),
|
||||
),
|
||||
)
|
||||
as String);
|
||||
|
||||
@override
|
||||
String get loadingText =>
|
||||
(super.noSuchMethod(
|
||||
@@ -3144,6 +3155,50 @@ class MockAppLocalizations extends _i1.Mock implements _i2.AppLocalizations {
|
||||
)
|
||||
as String);
|
||||
|
||||
@override
|
||||
String get useBasicMarkdown =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#useBasicMarkdown),
|
||||
returnValue: _i3.dummyValue<String>(
|
||||
this,
|
||||
Invocation.getter(#useBasicMarkdown),
|
||||
),
|
||||
)
|
||||
as String);
|
||||
|
||||
@override
|
||||
String get editorBold =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#editorBold),
|
||||
returnValue: _i3.dummyValue<String>(
|
||||
this,
|
||||
Invocation.getter(#editorBold),
|
||||
),
|
||||
)
|
||||
as String);
|
||||
|
||||
@override
|
||||
String get editorItalic =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#editorItalic),
|
||||
returnValue: _i3.dummyValue<String>(
|
||||
this,
|
||||
Invocation.getter(#editorItalic),
|
||||
),
|
||||
)
|
||||
as String);
|
||||
|
||||
@override
|
||||
String get editorList =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#editorList),
|
||||
returnValue: _i3.dummyValue<String>(
|
||||
this,
|
||||
Invocation.getter(#editorList),
|
||||
),
|
||||
)
|
||||
as String);
|
||||
|
||||
@override
|
||||
String get enterTextInLanguage =>
|
||||
(super.noSuchMethod(
|
||||
|
||||
101
test/widgets/markdown_editor_test.dart
Normal file
101
test/widgets/markdown_editor_test.dart
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (c) 2026 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/widgets/add_exercise/add_exercise_text_area.dart';
|
||||
|
||||
void main() {
|
||||
Widget makeTestable({required Widget child}) {
|
||||
return MaterialApp(
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: const [Locale('en')],
|
||||
home: Scaffold(body: child),
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('inserts markers when no selection', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
makeTestable(
|
||||
child: MarkdownEditor(initialValue: '', onChanged: (_) {}),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final textFieldFinder = find.byType(TextFormField);
|
||||
expect(textFieldFinder, findsOneWidget);
|
||||
|
||||
// Ensure initial is empty and then tap Bold button
|
||||
await tester.enterText(textFieldFinder, '');
|
||||
await tester.pump();
|
||||
|
||||
final boldButton = find.widgetWithIcon(IconButton, Icons.format_bold);
|
||||
expect(boldButton, findsOneWidget);
|
||||
|
||||
await tester.tap(boldButton);
|
||||
await tester.pump();
|
||||
|
||||
final tf = tester.widget<TextFormField>(textFieldFinder);
|
||||
final controller = tf.controller!;
|
||||
|
||||
expect(controller.text, '****');
|
||||
expect(controller.selection.isCollapsed, isTrue);
|
||||
expect(controller.selection.baseOffset, 2);
|
||||
});
|
||||
|
||||
testWidgets('wraps selected text when selection exists', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
makeTestable(
|
||||
child: MarkdownEditor(initialValue: '', onChanged: (_) {}),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final textFieldFinder = find.byType(TextFormField);
|
||||
expect(textFieldFinder, findsOneWidget);
|
||||
|
||||
// Enter text and set selection
|
||||
await tester.enterText(textFieldFinder, 'hello');
|
||||
await tester.pump();
|
||||
|
||||
final tf = tester.widget<TextFormField>(textFieldFinder);
|
||||
final controller = tf.controller!;
|
||||
|
||||
// select whole text
|
||||
controller.selection = const TextSelection(baseOffset: 0, extentOffset: 5);
|
||||
await tester.pump();
|
||||
|
||||
final boldButton = find.widgetWithIcon(IconButton, Icons.format_bold);
|
||||
expect(boldButton, findsOneWidget);
|
||||
|
||||
await tester.tap(boldButton);
|
||||
await tester.pump();
|
||||
|
||||
expect(controller.text, '**hello**');
|
||||
// selection should cover the inner text (shifted by left.length)
|
||||
expect(controller.selection.baseOffset, 2);
|
||||
expect(controller.selection.extentOffset, 7);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user