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}" + > + `); + fooFormat = await fixture(html` + <${elem} + .formatter="${value => `foo: ${value}`}" + .parser="${value => value.replace('foo: ', '')}" + .serializer="${value => `[foo] ${value}`}" + .deserializer="${value => value.replace('[foo] ', '')}" + > + `); + }); + + it('fires `model-value-changed` for every change on the input', async () => { + const formatEl = await fixture(html`<${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}>`); + 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 /