diff --git a/packages/field/test-suites/FormatMixin.suite.js b/packages/field/test-suites/FormatMixin.suite.js
new file mode 100644
index 000000000..dc54013e6
--- /dev/null
+++ b/packages/field/test-suites/FormatMixin.suite.js
@@ -0,0 +1,401 @@
+import { expect, fixture, html, aTimeout, defineCE, unsafeStatic } from '@open-wc/testing';
+import sinon from 'sinon';
+
+import { LitElement } from '@lion/core';
+import { Unparseable } from '@lion/validate';
+import { FormatMixin } from '../src/FormatMixin.js';
+
+function mimicUserInput(formControl, newViewValue) {
+ formControl.value = newViewValue; // eslint-disable-line no-param-reassign
+ formControl.inputElement.dispatchEvent(new CustomEvent('input', { bubbles: true }));
+}
+
+export function runFormatMixinSuite(customConfig) {
+ // TODO: Maybe remove suffix
+ const cfg = {
+ tagString: null,
+ modelValueType: String,
+ suffix: '',
+ ...customConfig,
+ };
+
+ /**
+ * Mocks a value for you based on the data type
+ * Optionally toggles you a different value
+ * for needing to mimic a value-change.
+ *
+ * TODO: The FormatMixin can know about platform types like
+ * Date, but not about modelValue of input-iban etc.
+ * Make this concept expandable by allowing 'non standard'
+ * modelValues to hook into this.
+ */
+ function generateValueBasedOnType(opts = {}) {
+ const options = { type: cfg.modelValueType, toggleValue: false, viewValue: false, ...opts };
+
+ switch (options.type) {
+ case Number:
+ return !options.toggleValue ? '123' : '456';
+ case Date:
+ // viewValue instead of modelValue, since a Date object is unparseable.
+ // Note: this viewValue needs to point to the same date as the
+ // default returned modelValue.
+ if (options.viewValue) {
+ return !options.toggleValue ? '5-5-2005' : '10-10-2010';
+ }
+ return !options.toggleValue ? new Date('5-5-2005') : new Date('10-10-2010');
+ case Array:
+ return !options.toggleValue ? ['foo', 'bar'] : ['baz', 'yay'];
+ case Object:
+ return !options.toggleValue ? { foo: 'bar' } : { bar: 'foo' };
+ case Boolean:
+ return !options.toggleValue;
+ case 'email':
+ return !options.toggleValue ? 'some-user@ing.com' : 'another-user@ing.com';
+ case 'iban':
+ return !options.toggleValue ? 'SE3550000000054910000003' : 'CH9300762011623852957';
+ default:
+ return !options.toggleValue ? 'foo' : 'bar';
+ }
+ }
+
+ describe(`FormatMixin ${cfg.suffix ? `(${cfg.suffix})` : ''}`, async () => {
+ let elem;
+ let nonFormat;
+ let fooFormat;
+
+ before(async () => {
+ if (!cfg.tagString) {
+ cfg.tagString = defineCE(
+ class extends FormatMixin(LitElement) {
+ render() {
+ return html`
+
+ `;
+ }
+
+ set value(newValue) {
+ this.inputElement.value = newValue;
+ }
+
+ get value() {
+ return this.inputElement.value;
+ }
+
+ get inputElement() {
+ return this.querySelector('input');
+ }
+ },
+ );
+ }
+
+ elem = unsafeStatic(cfg.tagString);
+ nonFormat = await fixture(html`<${elem}
+ .formatter="${v => v}"
+ .parser="${v => v}"
+ .serializer="${v => v}"
+ .deserializer="${v => v}"
+ >
+ ${elem}>`);
+ fooFormat = await fixture(html`
+ <${elem}
+ .formatter="${value => `foo: ${value}`}"
+ .parser="${value => value.replace('foo: ', '')}"
+ .serializer="${value => `[foo] ${value}`}"
+ .deserializer="${value => value.replace('[foo] ', '')}"
+ >
+ ${elem}>`);
+ });
+
+ it('fires `model-value-changed` for every change on the input', async () => {
+ const formatEl = await fixture(html`<${elem}>${elem}>`);
+
+ let counter = 0;
+ formatEl.addEventListener('model-value-changed', () => {
+ counter += 1;
+ });
+
+ mimicUserInput(formatEl, generateValueBasedOnType());
+ expect(counter).to.equal(1);
+
+ // Counter offset +1 for Date because parseDate created a new Date object
+ // when the user changes the value.
+ // This will result in a model-value-changed trigger even if the user value was the same
+ // TODO: a proper solution would be to add `hasChanged` to input-date, like isSameDate()
+ // from calendar utils
+ const counterOffset = cfg.modelValueType === Date ? 1 : 0;
+
+ mimicUserInput(formatEl, generateValueBasedOnType());
+ expect(counter).to.equal(1 + counterOffset);
+
+ mimicUserInput(formatEl, generateValueBasedOnType({ toggleValue: true }));
+ expect(counter).to.equal(2 + counterOffset);
+ });
+
+ it('fires `model-value-changed` for every modelValue change', async () => {
+ const el = await fixture(html`<${elem}>${elem}>`);
+ let counter = 0;
+ el.addEventListener('model-value-changed', () => {
+ counter += 1;
+ });
+
+ el.modelValue = 'one';
+ expect(counter).to.equal(1);
+
+ // no change means no event
+ el.modelValue = 'one';
+ expect(counter).to.equal(1);
+
+ el.modelValue = 'two';
+ expect(counter).to.equal(2);
+ });
+
+ it('has modelValue, formattedValue and serializedValue which are computed synchronously', async () => {
+ expect(nonFormat.modelValue).to.equal('', 'modelValue initially');
+ expect(nonFormat.formattedValue).to.equal('', 'formattedValue initially');
+ expect(nonFormat.serializedValue).to.equal('', 'serializedValue initially');
+ const generatedValue = generateValueBasedOnType();
+ nonFormat.modelValue = generatedValue;
+ expect(nonFormat.modelValue).to.equal(generatedValue, 'modelValue as provided');
+ expect(nonFormat.formattedValue).to.equal(generatedValue, 'formattedValue synchronized');
+ expect(nonFormat.serializedValue).to.equal(generatedValue, 'serializedValue synchronized');
+ });
+
+ it('has an input node (like /