import { LitElement } from '@lion/core'; import { aTimeout, defineCE, expect, fixture, html, unsafeStatic } from '@open-wc/testing'; import sinon from 'sinon'; import { FormatMixin } from '../src/FormatMixin.js'; import { Unparseable, Validator } from '../index.js'; /** * @typedef {ArrayConstructor | ObjectConstructor | NumberConstructor | BooleanConstructor | StringConstructor | DateConstructor | 'iban' | 'email'} modelValueType */ // @ts-expect-error base constructor same return type class FormatClass extends FormatMixin(LitElement) { get _inputNode() { return /** @type {HTMLInputElement} */ (super._inputNode); // casts type } render() { return html``; } set value(newValue) { if (this._inputNode) { this._inputNode.value = newValue; } } get value() { if (this._inputNode) { return this._inputNode.value; } return ''; } } /** * @param {FormatClass} formControl * @param {?} newViewValue */ function mimicUserInput(formControl, newViewValue) { formControl.value = newViewValue; // eslint-disable-line no-param-reassign formControl._inputNode.dispatchEvent(new CustomEvent('input', { bubbles: true })); } /** * @param {{tagString?: string, modelValueType?: modelValueType}} [customConfig] */ export function runFormatMixinSuite(customConfig) { const cfg = { tagString: null, childTagString: null, ...customConfig, }; /** * Mocks a value for you based on the data type * Optionally toggles you a different value * for needing to mimic a value-change. */ 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', async () => { /** @type {{d: any}} */ let elem; /** @type {FormatClass} */ let nonFormat; /** @type {FormatClass} */ let fooFormat; before(async () => { if (!cfg.tagString) { cfg.tagString = defineCE(FormatClass); } elem = unsafeStatic(cfg.tagString); nonFormat = await fixture(html` <${elem} .formatter="${/** @param {?} v */ v => v}" .parser="${/** @param {string} v */ v => v}" .serializer="${/** @param {?} v */ v => v}" .deserializer="${/** @param {string} v */ v => v}" > `); fooFormat = await fixture(html` <${elem} .formatter="${/** @param {string} value */ value => `foo: ${value}`}" .parser="${/** @param {string} value */ value => value.replace('foo: ', '')}" .serializer="${/** @param {string} value */ value => `[foo] ${value}`}" .deserializer="${/** @param {string} value */ value => value.replace('[foo] ', '')}" > `); }); it('fires `model-value-changed` for every change on the input', async () => { const formatEl = /** @type {FormatClass} */ (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 = /** @type {FormatClass} */ (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 /