import { LitElement } from '@lion/core';
import { parseDate } from '@lion/localize';
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 ? parseDate('5-5-2005') : parseDate('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}"
>
${elem}>
`);
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] ', '')}"
>
${elem}>
`);
});
it('fires `model-value-changed` for every change on the input', async () => {
const formatEl = /** @type {FormatClass} */ (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 = /** @type {FormatClass} */ (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 /