import { expect, html, defineCE, unsafeStatic, fixture } from '@open-wc/testing';
import { LitElement } from '@lion/core';
import sinon from 'sinon';
import { FormControlMixin } from '../src/FormControlMixin.js';
import { FormRegistrarMixin } from '../src/registration/FormRegistrarMixin.js';
/**
* @typedef {import('../types/FormControlMixinTypes').FormControlHost} FormControl
*/
describe('FormControlMixin', () => {
const inputSlot = html``;
class FormControlMixinClass extends FormControlMixin(LitElement) {}
const tagString = defineCE(FormControlMixinClass);
const tag = unsafeStatic(tagString);
it('is hidden when attribute hidden is true', async () => {
const el = await fixture(html`
<${tag} hidden>
${inputSlot}
${tag}>`);
expect(el).not.to.be.displayed;
});
describe('Label and helpText api', () => {
it('has a label', async () => {
const elAttr = /** @type {FormControlMixinClass} */ (await fixture(html`
<${tag} label="Email address">${inputSlot}${tag}>
`));
expect(elAttr.label).to.equal('Email address', 'as an attribute');
const elProp = /** @type {FormControlMixinClass} */ (await fixture(html`
<${tag}
.label=${'Email address'}
>${inputSlot}
${tag}>`));
expect(elProp.label).to.equal('Email address', 'as a property');
const elElem = /** @type {FormControlMixinClass} */ (await fixture(html`
<${tag}>
${inputSlot}
${tag}>`));
expect(elElem.label).to.equal('Email address', 'as an element');
});
it('has a label that supports inner html', async () => {
const el = /** @type {FormControlMixinClass} */ (await fixture(html`
<${tag}>
${inputSlot}
${tag}>`));
expect(el.label).to.equal('Email address');
});
it('only takes label of direct child', async () => {
const el = /** @type {FormControlMixinClass} */ (await fixture(html`
<${tag}>
<${tag} label="Email address">
${inputSlot}
${tag}>
${tag}>`));
expect(el.label).to.equal('');
});
it('can have a help-text', async () => {
const elAttr = /** @type {FormControlMixinClass} */ (await fixture(html`
<${tag} help-text="We will not send you any spam">${inputSlot}${tag}>
`));
expect(elAttr.helpText).to.equal('We will not send you any spam', 'as an attribute');
const elProp = /** @type {FormControlMixinClass} */ (await fixture(html`
<${tag}
.helpText=${'We will not send you any spam'}
>${inputSlot}
${tag}>`));
expect(elProp.helpText).to.equal('We will not send you any spam', 'as a property');
const elElem = /** @type {FormControlMixinClass} */ (await fixture(html`
<${tag}>
We will not send you any spam
${inputSlot}
${tag}>`));
expect(elElem.helpText).to.equal('We will not send you any spam', 'as an element');
});
it('can have a help-text that supports inner html', async () => {
const el = /** @type {FormControlMixinClass} */ (await fixture(html`
<${tag}>
We will not send you any spam
${inputSlot}
${tag}>`));
expect(el.helpText).to.equal('We will not send you any spam');
});
it('only takes help-text of direct child', async () => {
const el = /** @type {FormControlMixinClass} */ (await fixture(html`
<${tag}>
<${tag} help-text="We will not send you any spam">
${inputSlot}
${tag}>
${tag}>`));
expect(el.helpText).to.equal('');
});
});
describe('Accessibility', () => {
it('does not duplicate aria-describedby and aria-labelledby ids on reconnect', async () => {
const wrapper = /** @type {HTMLElement} */ (await fixture(html`
<${tag} help-text="This element will be disconnected/reconnected">${inputSlot}${tag}>
This also needs to be read whenever the input has focus
Same for this
`));
const el = /** @type {FormControlMixinClass} */ (wrapper.querySelector(tagString));
// wait until the field element is done rendering
await el.updateComplete;
await el.updateComplete;
// @ts-ignore allow protected accessors in tests
const inputId = el._inputId;
// 1a. addToAriaLabelledBy()
// Check if the aria attr is filled initially
expect(/** @type {string} */ (el._inputNode.getAttribute('aria-labelledby'))).to.contain(
`label-${inputId}`,
);
const additionalLabel = /** @type {HTMLElement} */ (wrapper.querySelector(
'#additionalLabel',
));
el.addToAriaLabelledBy(additionalLabel);
await el.updateComplete;
let labelledbyAttr = /** @type {string} */ (el._inputNode.getAttribute('aria-labelledby'));
// Now check if ids are added to the end (not overridden)
expect(labelledbyAttr).to.contain(`additionalLabel`);
// Should be placed in the end
expect(
labelledbyAttr.indexOf(`label-${inputId}`) < labelledbyAttr.indexOf('additionalLabel'),
);
// 1b. removeFromAriaLabelledBy()
el.removeFromAriaLabelledBy(additionalLabel);
await el.updateComplete;
labelledbyAttr = /** @type {string} */ (el._inputNode.getAttribute('aria-labelledby'));
// Now check if ids are added to the end (not overridden)
expect(labelledbyAttr).to.not.contain(`additionalLabel`);
// 2a. addToAriaDescribedBy()
// Check if the aria attr is filled initially
expect(/** @type {string} */ (el._inputNode.getAttribute('aria-describedby'))).to.contain(
`feedback-${inputId}`,
);
});
it('sorts internal elements, and allows opt-out', async () => {
const wrapper = await fixture(html`
<${tag}>
Added to description by default
${tag}>
should go after input internals
should go after input internals
`);
const el = /** @type {FormControlMixinClass} */ (wrapper.querySelector(tagString));
// N.B. in real life we would never add the input to aria-describedby or -labelledby,
// but this example purely demonstrates dom order is respected.
// A real life scenario would be for instance when
// a Field or FormGroup would be extended and an extra slot would be added in the template
const myInput = /** @type {HTMLElement} */ (wrapper.querySelector('#myInput'));
el.addToAriaLabelledBy(myInput);
await el.updateComplete;
el.addToAriaDescribedBy(myInput);
await el.updateComplete;
expect(
/** @type {string} */ (el._inputNode.getAttribute('aria-labelledby')).split(' '),
).to.eql(['myInput', 'internalLabel']);
expect(
/** @type {string} */ (el._inputNode.getAttribute('aria-describedby')).split(' '),
).to.eql(['myInput', 'internalDescription']);
// cleanup
el.removeFromAriaLabelledBy(myInput);
await el.updateComplete;
el.removeFromAriaDescribedBy(myInput);
await el.updateComplete;
// opt-out of reorder
el.addToAriaLabelledBy(myInput, { reorder: false });
await el.updateComplete;
el.addToAriaDescribedBy(myInput, { reorder: false });
await el.updateComplete;
expect(
/** @type {string} */ (el._inputNode.getAttribute('aria-labelledby')).split(' '),
).to.eql(['internalLabel', 'myInput']);
expect(
/** @type {string} */ (el._inputNode.getAttribute('aria-describedby')).split(' '),
).to.eql(['internalDescription', 'myInput']);
});
it('respects provided order for external elements', async () => {
const wrapper = await fixture(html`