diff --git a/packages/wyatt_form_bloc/test/form_input_test.dart b/packages/wyatt_form_bloc/test/form_input_test.dart
new file mode 100644
index 00000000..3c567ab3
--- /dev/null
+++ b/packages/wyatt_form_bloc/test/form_input_test.dart
@@ -0,0 +1,79 @@
+// Copyright (C) 2022 WYATT GROUP
+// Please see the AUTHORS file for details.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// 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 General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
+
+void main() {
+ group('FormInput', () {
+ test('contains key', () {
+ final input = FormInput('key', const Name.pure());
+ expect(input.key, 'key');
+ });
+
+ test('contains validator', () {
+ final input = FormInput('key', const Name.pure());
+ expect(input.validator, const Name.pure());
+ });
+
+ test('contains metadata', () {
+ final input = FormInput(
+ 'key',
+ const Name.pure(),
+ metadata: const FormInputMetadata(
+ extra: 42,
+ ),
+ );
+ expect(input.metadata.extra, 42);
+ });
+
+ test('== to another form input', () {
+ final input1 = FormInput(
+ 'key',
+ const Name.pure(),
+ metadata: const FormInputMetadata(
+ extra: 42,
+ ),
+ );
+ final input2 = FormInput(
+ 'key',
+ const Name.pure(),
+ metadata: const FormInputMetadata(
+ extra: 42,
+ ),
+ );
+ expect(input1 == input2, isTrue);
+ });
+
+ test('!= to a different form input', () {
+ final input1 = FormInput(
+ 'key',
+ const Name.pure(),
+ metadata: const FormInputMetadata(
+ extra: 43, // little difference here
+ ),
+ );
+ final input2 = FormInput(
+ 'key',
+ const Name.pure(),
+ metadata: const FormInputMetadata(
+ extra: 42,
+ ),
+ );
+ expect(input1 == input2, isFalse);
+ });
+ });
+}
diff --git a/packages/wyatt_form_bloc/test/form_input_validator_test.dart b/packages/wyatt_form_bloc/test/form_input_validator_test.dart
new file mode 100644
index 00000000..e7abbf91
--- /dev/null
+++ b/packages/wyatt_form_bloc/test/form_input_validator_test.dart
@@ -0,0 +1,158 @@
+// Copyright (C) 2022 WYATT GROUP
+// Please see the AUTHORS file for details.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// 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 General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
+
+void main() {
+ group('FormInputValidator', () {
+ group('Boolean', () {
+ test('is pure at the beginning', () {
+ const validator = Boolean.pure();
+ expect(validator.pure, true);
+ });
+
+ test('contains `false` at the beginning', () {
+ const validator = Boolean.pure();
+ expect(validator.pure, true);
+ expect(validator.value, false);
+ });
+
+ test('contains `true` when intialized with', () {
+ const validator = Boolean.pure(defaultValue: true);
+ expect(validator.pure, true);
+ expect(validator.value, true);
+ });
+
+ test('returns ValidationStandardError.invalid on null', () {
+ const validator = Boolean.dirty();
+ expect(validator.error, ValidationStandardError.invalid);
+ });
+
+ test('is invalid and contains `null` when update with `null`', () {
+ const validator = Boolean.dirty();
+ expect(validator.invalid, true);
+ expect(validator.value, null);
+ });
+
+ test('is valid and contains `true` when update with `true`', () {
+ const validator = Boolean.dirty(value: true);
+ expect(validator.valid, true);
+ expect(validator.value, true);
+ });
+
+ test('is valid and contains `false` when update with `false`', () {
+ const validator = Boolean.dirty(value: false);
+ expect(validator.valid, true);
+ expect(validator.value, false);
+ });
+
+ test('== to another boolean validator with the same attributes', () {
+ const validator1 = Boolean.dirty(value: false);
+ const validator2 = Boolean.dirty(value: false);
+ expect(validator1 == validator2, isTrue);
+ });
+
+ test('!= to another boolean validator with different attributes', () {
+ const validator1 = Boolean.dirty(value: false);
+ const validator2 = Boolean.dirty(value: true);
+ expect(validator1 == validator2, isFalse);
+ });
+ });
+
+ group('ConfirmedPassword', () {
+ test('is pure at the beginning', () {
+ const validator = ConfirmedPassword.pure();
+ expect(validator.pure, true);
+ });
+
+ test('contains `null` at the beginning', () {
+ const validator = ConfirmedPassword.pure();
+ expect(validator.pure, true);
+ expect(validator.value, null);
+ });
+
+ test('contains `pwd` when intialized with', () {
+ const validator = ConfirmedPassword.pure(defaultValue: 'pwd');
+ expect(validator.pure, true);
+ expect(validator.value, 'pwd');
+ });
+
+ test('returns ValidationStandardError.invalid on null', () {
+ const validator = ConfirmedPassword.dirty(password: 'pwd');
+ expect(validator.error, ValidationStandardError.invalid);
+ });
+
+ test('returns ValidationStandardError.notEqual on different values', () {
+ const validator =
+ ConfirmedPassword.dirty(password: 'pwd', value: 'incorrect');
+ expect(validator.error, ValidationStandardError.notEqual);
+ });
+
+ test('is invalid and contains `null` when update with `null`', () {
+ const validator = ConfirmedPassword.dirty(password: 'pwd');
+ expect(validator.invalid, true);
+ expect(validator.value, null);
+ });
+
+ test('is invalid and contains `incorrect` when update with `incorrect`',
+ () {
+ const validator =
+ ConfirmedPassword.dirty(password: 'pwd', value: 'incorrect');
+ expect(validator.invalid, true);
+ expect(validator.value, 'incorrect');
+ });
+
+ test('is valid and contains `pwd` when update with `pwd`', () {
+ const validator =
+ ConfirmedPassword.dirty(password: 'pwd', value: 'pwd');
+ expect(validator.valid, true);
+ expect(validator.value, 'pwd');
+ });
+
+ test(
+ '== to another confirmed password validator with the same attributes',
+ () {
+ const validator1 =
+ ConfirmedPassword.dirty(password: 'pwd', value: 'pwd');
+ const validator2 =
+ ConfirmedPassword.dirty(password: 'pwd', value: 'pwd');
+ expect(validator1 == validator2, isTrue);
+ });
+
+ test(
+ '!= to another confirmed password validator '
+ 'with different attributes', () {
+ const validator1 =
+ ConfirmedPassword.dirty(password: 'pwd', value: 'pwd1');
+ const validator2 =
+ ConfirmedPassword.dirty(password: 'pwd', value: 'pwd2');
+ expect(validator1 == validator2, isFalse);
+ });
+
+ test('!= to another confirmed password validator with different password',
+ () {
+ const validator1 =
+ ConfirmedPassword.dirty(password: 'pwd1', value: 'pwd');
+ const validator2 =
+ ConfirmedPassword.dirty(password: 'pwd2', value: 'pwd');
+ expect(validator1 == validator2, isFalse);
+ });
+ });
+ });
+
+ // TODO(hpcl): add tests for all validators...
+}
diff --git a/packages/wyatt_form_bloc/test/form_operation_test.dart b/packages/wyatt_form_bloc/test/form_operation_test.dart
new file mode 100644
index 00000000..825374b1
--- /dev/null
+++ b/packages/wyatt_form_bloc/test/form_operation_test.dart
@@ -0,0 +1,144 @@
+// Copyright (C) 2022 WYATT GROUP
+// Please see the AUTHORS file for details.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// 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 General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
+
+void main() {
+ final WyattForm formA = WyattFormImpl(
+ [
+ FormInput(
+ 'email',
+ const Email.dirty('test@test.fr'),
+ ),
+ FormInput(
+ 'password',
+ const Password.pure(),
+ ),
+ ],
+ name: 'A',
+ );
+
+ final WyattForm formB = WyattFormImpl(
+ [
+ FormInput(
+ 'email',
+ const Email.dirty('test2@test.fr'),
+ ),
+ FormInput(
+ 'password',
+ const Password.dirty('password1234'),
+ ),
+ FormInput(
+ 'bool',
+ const Boolean.pure(),
+ ),
+ FormInput(
+ 'phone',
+ const Phone.dirty('0000000000'),
+ ),
+ ],
+ name: 'B',
+ );
+
+ group('FormReplace', () {
+ test('returns B when called on (A,B)', () {
+ final result = const FormReplace().call(formA, formB);
+ expect(result, formB);
+ });
+ });
+
+ group('FormUnion', () {
+ final WyattForm union = WyattFormImpl(
+ [
+ FormInput(
+ 'email',
+ const Email.dirty('test@test.fr'),
+ ),
+ FormInput(
+ 'password',
+ const Password.dirty('password1234'),
+ ),
+ FormInput(
+ 'bool',
+ const Boolean.pure(),
+ ),
+ FormInput(
+ 'phone',
+ const Phone.dirty('0000000000'),
+ ),
+ ],
+ name: 'A',
+ );
+
+ test('returns A union B when called on (A,B), with A name', () {
+ final result = const FormUnion().call(formA, formB);
+ expect(result, union);
+ });
+ });
+
+ group('FormIntersection', () {
+ final WyattForm intersection = WyattFormImpl(
+ [
+ FormInput(
+ 'email',
+ const Email.dirty('test@test.fr'),
+ ),
+ FormInput(
+ 'password',
+ const Password.dirty('password1234'),
+ ),
+ ],
+ name: 'A',
+ );
+
+ test('returns A intersection B when called on (A,B), with A name', () {
+ final result = const FormIntersection().call(formA, formB);
+ expect(result, intersection);
+ });
+ });
+
+ group('FormDifference', () {
+ test('returns A - B when called on (A,B), with A name', () {
+ final result = const FormDifference().call(formA, formB);
+ expect(
+ result,
+ WyattFormImpl(
+ const [],
+ name: 'A',
+ ),
+ );
+ });
+ final WyattForm difference = WyattFormImpl(
+ [
+ FormInput(
+ 'bool',
+ const Boolean.pure(),
+ ),
+ FormInput(
+ 'phone',
+ const Phone.dirty('0000000000'),
+ ),
+ ],
+ name: 'B',
+ );
+
+ test('returns B - A when called on (B,A), with B name', () {
+ final result = const FormDifference().call(formB, formA);
+ expect(result, difference);
+ });
+ });
+}
diff --git a/packages/wyatt_form_bloc/test/wyatt_form_bloc_test.dart b/packages/wyatt_form_bloc/test/wyatt_form_bloc_test.dart
deleted file mode 100644
index e69de29b..00000000
diff --git a/packages/wyatt_form_bloc/test/wyatt_form_test.dart b/packages/wyatt_form_bloc/test/wyatt_form_test.dart
new file mode 100644
index 00000000..f927df6b
--- /dev/null
+++ b/packages/wyatt_form_bloc/test/wyatt_form_test.dart
@@ -0,0 +1,167 @@
+// Copyright (C) 2022 WYATT GROUP
+// Please see the AUTHORS file for details.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// 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 General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
+
+void main() {
+ group('WyattForm', () {
+ final WyattForm form = WyattFormImpl(
+ [
+ FormInput(
+ 'email',
+ const Email.pure(),
+ ),
+ FormInput(
+ 'password',
+ const Password.pure(),
+ ),
+ ],
+ name: 'signInForm',
+ );
+
+ test('returns inputs', () {
+ final inputs = <
+ FormInput,
+ dynamic>>[
+ FormInput(
+ 'email',
+ const Email.pure(),
+ ),
+ FormInput(
+ 'password',
+ const Password.pure(),
+ ),
+ ];
+
+ expect(form.inputs, inputs);
+ });
+
+ test('returns validation strategy', () {
+ final strategy = form.formValidationStrategy;
+ expect(strategy, const EveryInputValidator());
+ });
+
+ test('returns validator list', () {
+ final validators = [
+ const Email.pure(),
+ const Password.pure(),
+ ];
+ expect(form.asValidatorList(), validators);
+ });
+
+ test('returns name', () {
+ final name = form.name;
+ expect(name, 'signInForm');
+ });
+
+ group('containsKey', () {
+ test('returns `true` on key `email`', () {
+ expect(form.containsKey('email'), true);
+ });
+
+ test('returns `true` on key `password`', () {
+ expect(form.containsKey('password'), true);
+ });
+
+ test('return `false` on key `incorrect`', () {
+ expect(form.containsKey('incorrect'), false);
+ });
+ });
+
+ group('inputOf', () {
+ test('returns email input', () {
+ final input = FormInput, dynamic>(
+ 'email',
+ const Email.pure(),
+ );
+
+ expect(form.inputOf('email'), input);
+ });
+
+ test('returns password input', () {
+ final input = FormInput, dynamic>(
+ 'password',
+ const Password.pure(),
+ );
+
+ expect(form.inputOf('password'), input);
+ });
+
+ test('throw on unknown key', () {
+ expect(() => form.inputOf('incorrect'), throwsException);
+ });
+ });
+
+ group('errorOf', () {
+ final WyattForm errorForm = WyattFormImpl(
+ [
+ FormInput(
+ 'email',
+ const Email.dirty('incorrect'),
+ ),
+ FormInput(
+ 'password',
+ const Password.dirty('toto1234'),
+ ),
+ ],
+ name: 'signInForm',
+ );
+
+ test('returns `invalid` on invalid input', () {
+ expect(errorForm.errorOf('email'), ValidationStandardError.invalid);
+ });
+
+ test('returns `null` on valid input', () {
+ expect(errorForm.errorOf('password'), isNull);
+ });
+ });
+
+ group('errorOf', () {
+ final WyattForm metadataForm = WyattFormImpl(
+ [
+ FormInput(
+ 'email',
+ const Email.pure(),
+ metadata: const FormInputMetadata(
+ extra: 1,
+ ),
+ ),
+ FormInput(
+ 'password',
+ const Password.pure(),
+ metadata: const FormInputMetadata(
+ extra: 2,
+ ),
+ ),
+ ],
+ name: 'signInForm',
+ );
+
+ test('returns `1` on email extra', () {
+ expect(metadataForm.metadataOf('email').extra, 1);
+ });
+
+ test('returns `2` on password extra', () {
+ expect(metadataForm.metadataOf('password').extra, 2);
+ });
+ });
+ });
+
+ // TODO(hpcl): add more tests on WyattForm
+}