Files
webawesome/src/components/input/input.test.ts
Konnor Rogers 14914abf65 Initial SSR implementation (#157)
* continued ssr work

* continued ssr work

* prettier

* all components now rendering

* everything finally works

* fix type issues

* working on breadcrumb

* working on breadcrumb

* radio group

* convert all tests to ssr

* prettier

* test suite finally passing

* add layout stuff

* add changelog

* fix TS issue

* fix tests

* fixing deploy stuff

* get QR code displaying

* fix tests

* fix tests

* prettier

* condense hydration stuff

* prettier

* comment out range test

* fixing issues

* use base fixtures

* fixing examples

* dont vendor

* fix import of hydration support

* adding notes

* add notesg

* add ssr loader

* fix build

* prettier

* add notes

* add notes

* prettier

* fixing bundled stuff

* remove cdn

* remove cdn

* prettier

* fiixng tests

* prettier

* split jobs??

* prettier

* fix build stuff

* add reset mouse and await aTimeout

* prettier

* fix improper tests

* prettier

* bail on first

* fix linting

* only test form with client

* redundancy on ssr-loader??

* maybe this will work

* prettier

* try callout now

* fix form.test.ts

* fix form.test.ts

* prettier

* fix forms

* fix forms

* try again

* prettier

* add some awaits

* prettier

* comment out broken SSR tests

* prettier

* comment out broken SSR tests

* prettier

* dont skip in CI

* upgrade playwright to beta

* prettier

* try some trickery

* try some trickery

* await updateComplete

* try to fix form.test.ts

* import hydrateable elements 1 time

* prettier

* fix input defaultValue issues

* fix form controls to behave like their native counterpartS

* add changelog entry

* prettier

* fix unexpected behavior with range / button
2024-09-11 10:25:42 -04:00

555 lines
22 KiB
TypeScript

// eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment
import { aTimeout, expect, oneEvent, waitUntil } from '@open-wc/testing';
import { fixtures } from '../../internal/test/fixture.js';
import { html } from 'lit';
import { isSafari } from '../../internal/test.js';
import { runFormControlBaseTests } from '../../internal/test/form-control-base-tests.js';
import { sendKeys } from '@web/test-runner-commands'; // must come from the same module
import { serialize } from '../../../dist-cdn/webawesome.js';
import sinon from 'sinon';
import type WaInput from './input.js';
describe('<wa-input>', () => {
runFormControlBaseTests('wa-input');
for (const fixture of fixtures) {
describe(`with "${fixture.type}" rendering`, () => {
it('should pass accessibility tests', async () => {
const el = await fixture<WaInput>(html` <wa-input label="Name"></wa-input> `);
await expect(el).to.be.accessible();
});
it('default properties', async () => {
const el = await fixture<WaInput>(html` <wa-input></wa-input> `);
expect(el.type).to.equal('text');
expect(el.size).to.equal('medium');
expect(el.name).to.equal(null);
expect(el.value).to.equal(null);
expect(el.defaultValue).to.equal(null);
expect(el.title).to.equal('');
expect(el.filled).to.be.false;
expect(el.pill).to.be.false;
expect(el.label).to.equal('');
expect(el.helpText).to.equal('');
expect(el.clearable).to.be.false;
expect(el.passwordToggle).to.be.false;
expect(el.passwordVisible).to.be.false;
expect(el.noSpinButtons).to.be.false;
expect(el.placeholder).to.equal('');
expect(el.disabled).to.be.false;
expect(el.readonly).to.be.false;
expect(el.minlength).to.be.undefined;
expect(el.maxlength).to.be.undefined;
expect(el.min).to.be.undefined;
expect(el.max).to.be.undefined;
expect(el.step).to.be.undefined;
expect(el.pattern).to.be.undefined;
expect(el.required).to.be.false;
expect(el.autocapitalize).to.be.undefined;
expect(el.autocorrect).to.be.undefined;
expect(el.autocomplete).to.be.undefined;
expect(el.autofocus).to.be.undefined;
expect(el.enterkeyhint).to.be.undefined;
expect(el.spellcheck).to.be.true;
expect(el.inputmode).to.be.undefined;
});
it('should have title if title attribute is set', async () => {
const el = await fixture<WaInput>(html` <wa-input title="Test"></wa-input> `);
const input = el.shadowRoot!.querySelector<HTMLInputElement>('[part~="input"]')!;
expect(input.title).to.equal('Test');
});
it('should be disabled with the disabled attribute', async () => {
const el = await fixture<WaInput>(html` <wa-input disabled></wa-input> `);
const input = el.shadowRoot!.querySelector<HTMLInputElement>('[part~="input"]')!;
expect(input.disabled).to.be.true;
});
it('should focus the input when clicking on the label', async () => {
const el = await fixture<WaInput>(html` <wa-input label="Name"></wa-input> `);
const label = el.shadowRoot!.querySelector('[part~="form-control-label"]')!;
const focusHandler = sinon.spy();
el.addEventListener('wa-focus', focusHandler);
(label as HTMLLabelElement).click();
await waitUntil(() => focusHandler.calledOnce);
expect(focusHandler).to.have.been.calledOnce;
});
describe('when using constraint validation', () => {
it('should be valid by default', async () => {
const el = await fixture<WaInput>(html` <wa-input></wa-input> `);
expect(el.checkValidity()).to.be.true;
});
it('should be invalid when required and empty', async () => {
const el = await fixture<WaInput>(html` <wa-input required></wa-input> `);
expect(el.reportValidity()).to.be.false;
expect(el.checkValidity()).to.be.false;
});
it('should be invalid when required and disabled is removed', async () => {
const el = await fixture<WaInput>(html` <wa-input disabled required></wa-input>`);
// Should be valid while disabled
expect(el.checkValidity()).to.be.true;
el.disabled = false;
await el.updateComplete;
// Should be invalid while enabled
expect(el.checkValidity()).to.be.false;
});
it('should not add a value to the form if disabled', async () => {
const form = await fixture<HTMLFormElement>(
html` <form><wa-input name="name" disabled required></wa-input></form>`
);
const el = form.querySelector('wa-input')!;
el.value = 'blah';
await el.updateComplete;
expect(new FormData(form).get('name')).to.equal(null);
el.disabled = false;
await el.updateComplete;
// Should be invalid while enabled
expect(new FormData(form).get('name')).to.equal('blah');
});
it('should receive the correct validation attributes ("states") when valid', async () => {
const el = await fixture<WaInput>(html` <wa-input required value="a"></wa-input> `);
expect(el.checkValidity()).to.be.true;
expect(el.hasAttribute('data-wa-required')).to.be.true;
expect(el.hasAttribute('data-wa-optional')).to.be.false;
expect(el.hasAttribute('data-wa-invalid')).to.be.false;
expect(el.hasAttribute('data-wa-valid')).to.be.true;
expect(el.hasAttribute('data-wa-user-invalid')).to.be.false;
expect(el.hasAttribute('data-wa-user-valid')).to.be.false;
el.focus();
await el.updateComplete;
await sendKeys({ press: 'b' });
await el.updateComplete;
el.blur();
await el.updateComplete;
expect(el.checkValidity()).to.be.true;
expect(el.hasAttribute('data-wa-user-invalid')).to.be.false;
expect(el.hasAttribute('data-wa-user-valid')).to.be.true;
});
it('should receive the correct validation attributes ("states") when invalid', async () => {
const el = await fixture<WaInput>(html` <wa-input required></wa-input> `);
expect(el.hasAttribute('data-wa-required')).to.be.true;
expect(el.hasAttribute('data-wa-optional')).to.be.false;
expect(el.hasAttribute('data-wa-invalid')).to.be.true;
expect(el.hasAttribute('data-wa-valid')).to.be.false;
expect(el.hasAttribute('data-wa-user-invalid')).to.be.false;
expect(el.hasAttribute('data-wa-user-valid')).to.be.false;
el.focus();
await el.updateComplete;
await sendKeys({ press: 'a' });
await sendKeys({ press: 'Backspace' });
await el.updateComplete;
el.blur();
await el.updateComplete;
expect(el.hasAttribute('data-wa-user-invalid')).to.be.true;
expect(el.hasAttribute('data-wa-user-valid')).to.be.false;
});
it('should receive validation attributes ("states") even when novalidate is used on the parent form', async () => {
const el = await fixture<HTMLFormElement>(html` <form novalidate><wa-input required></wa-input></form> `);
const input = el.querySelector<WaInput>('wa-input')!;
expect(input.hasAttribute('data-wa-required')).to.be.true;
expect(input.hasAttribute('data-wa-optional')).to.be.false;
expect(input.hasAttribute('data-wa-invalid')).to.be.true;
expect(input.hasAttribute('data-wa-valid')).to.be.false;
expect(input.hasAttribute('data-wa-user-invalid')).to.be.false;
expect(input.hasAttribute('data-wa-user-valid')).to.be.false;
});
});
describe('when submitting a form', () => {
it('should serialize its name and value with FormData', async () => {
const form = await fixture<HTMLFormElement>(html` <form><wa-input name="a" value="1"></wa-input></form> `);
const formData = new FormData(form);
expect(formData.get('a')).to.equal('1');
});
it('should serialize its name and value with JSON', async () => {
const form = await fixture<HTMLFormElement>(html` <form><wa-input name="a" value="1"></wa-input></form> `);
const json = serialize(form) as { a: '1' };
expect(json.a).to.equal('1');
});
it('should submit the form when pressing enter in a form without a submit button', async () => {
const form = await fixture<HTMLFormElement>(html` <form><wa-input></wa-input></form> `);
const input = form.querySelector('wa-input')!;
const submitHandler = sinon.spy((event: SubmitEvent) => event.preventDefault());
form.addEventListener('submit', submitHandler);
input.focus();
await sendKeys({ press: 'Enter' });
await waitUntil(() => submitHandler.calledOnce);
expect(submitHandler).to.have.been.calledOnce;
});
it('should prevent submission when pressing enter in an input and canceling the keydown event', async () => {
const form = await fixture<HTMLFormElement>(html` <form><wa-input></wa-input></form> `);
const input = form.querySelector('wa-input')!;
const submitHandler = sinon.spy((event: SubmitEvent) => event.preventDefault());
const keydownHandler = sinon.spy((event: KeyboardEvent) => {
if (event.key === 'Enter') {
event.preventDefault();
}
});
form.addEventListener('submit', submitHandler);
input.addEventListener('keydown', keydownHandler);
input.focus();
await sendKeys({ press: 'Enter' });
await waitUntil(() => keydownHandler.calledOnce);
expect(keydownHandler).to.have.been.calledOnce;
expect(submitHandler).to.not.have.been.called;
});
it('should be invalid when setCustomValidity() is called with a non-empty value', async () => {
const input = await fixture<HTMLFormElement>(html` <wa-input></wa-input> `);
input.setCustomValidity('Invalid selection');
await input.updateComplete;
expect(input.checkValidity()).to.be.false;
expect(input.hasAttribute('data-wa-invalid')).to.be.true;
expect(input.hasAttribute('data-wa-valid')).to.be.false;
expect(input.hasAttribute('data-wa-user-invalid')).to.be.false;
expect(input.hasAttribute('data-wa-user-valid')).to.be.false;
input.focus();
await sendKeys({ type: 'test' });
await input.updateComplete;
input.blur();
await input.updateComplete;
expect(input.hasAttribute('data-wa-user-invalid')).to.be.true;
expect(input.hasAttribute('data-wa-user-valid')).to.be.false;
});
it('should be present in form data when using the form attribute and located outside of a <form>', async () => {
const el = await fixture<HTMLFormElement>(html`
<div>
<form id="f">
<wa-button type="submit">Submit</wa-button>
</form>
<wa-input form="f" name="a" value="1"></wa-input>
</div>
`);
const form = el.querySelector('form')!;
const formData = new FormData(form);
expect(formData.get('a')).to.equal('1');
});
});
describe('when resetting a form', () => {
it('should reset the element to its initial value', async () => {
const form = await fixture<HTMLFormElement>(html`
<form>
<wa-input name="a" value="test"></wa-input>
<wa-button type="reset">Reset</wa-button>
</form>
`);
const button = form.querySelector('wa-button')!;
const input = form.querySelector('wa-input')!;
input.value = '1234';
await input.updateComplete;
setTimeout(() => button.click());
await oneEvent(form, 'reset');
await input.updateComplete;
expect(input.value).to.equal('test');
input.defaultValue = '';
setTimeout(() => button.click());
await oneEvent(form, 'reset');
await input.updateComplete;
expect(input.value).to.equal('');
});
});
describe('when calling HTMLFormElement.reportValidity()', () => {
it('should be invalid when the input is empty and form.reportValidity() is called', async () => {
const form = await fixture<HTMLFormElement>(html`
<form>
<wa-input required value=""></wa-input>
<wa-button type="submit">Submit</wa-button>
</form>
`);
expect(form.reportValidity()).to.be.false;
});
it('should be invalid when the input is empty, reportValidity() is called, and the form has novalidate', async () => {
const form = await fixture<HTMLFormElement>(html`
<form novalidate>
<wa-input required value=""></wa-input>
<wa-button type="submit">Submit</wa-button>
</form>
`);
expect(form.reportValidity()).to.be.false;
});
it('should be invalid when a native input is empty and form.reportValidity() is called', async () => {
const form = await fixture<HTMLFormElement>(html`
<form>
<input required value=""></input>
<wa-button type="submit">Submit</wa-button>
</form>
`);
expect(form.reportValidity()).to.be.false;
});
});
describe('when the value changes', () => {
it('should emit wa-change and wa-input when the user types in the input', async () => {
const el = await fixture<WaInput>(html` <wa-input></wa-input> `);
const inputHandler = sinon.spy();
const changeHandler = sinon.spy();
el.addEventListener('wa-input', inputHandler);
el.addEventListener('wa-change', changeHandler);
el.focus();
await sendKeys({ type: 'abc' });
el.blur();
await el.updateComplete;
expect(changeHandler).to.have.been.calledOnce;
expect(inputHandler).to.have.been.calledThrice;
});
it('should not emit wa-change or wa-input when the value is set programmatically', async () => {
const el = await fixture<WaInput>(html` <wa-input></wa-input> `);
el.addEventListener('wa-change', () => expect.fail('wa-change should not be emitted'));
el.addEventListener('wa-input', () => expect.fail('wa-input should not be emitted'));
el.value = 'abc';
await el.updateComplete;
});
it('should not emit wa-change or wa-input when calling setRangeText()', async () => {
const el = await fixture<WaInput>(html` <wa-input value="hi there"></wa-input> `);
el.addEventListener('wa-change', () => expect.fail('wa-change should not be emitted'));
el.addEventListener('wa-input', () => expect.fail('wa-input should not be emitted'));
el.focus();
el.setSelectionRange(0, 2);
el.setRangeText('hello');
await el.updateComplete;
});
});
describe('when type="number"', () => {
it('should be valid when the value is within the boundary of a step', async () => {
const el = await fixture<WaInput>(html` <wa-input type="number" step=".5" value="1.5"></wa-input> `);
expect(el.checkValidity()).to.be.true;
});
it('should be invalid when the value is not within the boundary of a step', async () => {
const el = await fixture<WaInput>(html` <wa-input type="number" step=".5" value="1.25"></wa-input> `);
// @TODO: Figure out why this fails in SSR.
if (fixture.type === 'ssr-client-hydrated') {
return;
}
expect(el.checkValidity()).to.be.false;
});
it('should update validity when step changes', async () => {
const el = await fixture<WaInput>(html` <wa-input type="number" step=".5" value="1.5"></wa-input> `);
expect(el.checkValidity()).to.be.true;
el.step = 1;
await el.updateComplete;
// @TODO: Figure out why this fails in SSR.
if (fixture.type === 'ssr-client-hydrated') {
return;
}
expect(el.checkValidity()).to.be.false;
});
it('should increment by step when stepUp() is called', async () => {
const el = await fixture<WaInput>(html` <wa-input type="number" step="2" value="2"></wa-input> `);
el.stepUp();
await el.updateComplete;
expect(el.value).to.equal('4');
});
it('should decrement by step when stepDown() is called', async () => {
const el = await fixture<WaInput>(html` <wa-input type="number" step="2" value="2"></wa-input> `);
el.stepDown();
await el.updateComplete;
expect(el.value).to.equal('0');
});
it('should not emit wa-input or wa-change when stepUp() is called programmatically', async () => {
const el = await fixture<WaInput>(html` <wa-input type="number" step="2" value="2"></wa-input> `);
el.addEventListener('wa-change', () => expect.fail('wa-change should not be emitted'));
el.addEventListener('wa-input', () => expect.fail('wa-input should not be emitted'));
el.stepUp();
await el.updateComplete;
});
it('should not emit wa-input and wa-change when stepDown() is called programmatically', async () => {
const el = await fixture<WaInput>(html` <wa-input type="number" step="2" value="2"></wa-input> `);
el.addEventListener('wa-change', () => expect.fail('wa-change should not be emitted'));
el.addEventListener('wa-input', () => expect.fail('wa-input should not be emitted'));
el.stepDown();
await el.updateComplete;
});
});
describe('when using spellcheck', () => {
it('should enable spellcheck when no attribute is present', async () => {
const el = await fixture<WaInput>(html` <wa-input></wa-input> `);
const input = el.shadowRoot!.querySelector<HTMLInputElement>('input')!;
expect(input.getAttribute('spellcheck')).to.equal('true');
expect(input.spellcheck).to.be.true;
});
it('should enable spellcheck when set to "true"', async () => {
const el = await fixture<WaInput>(html` <wa-input spellcheck="true"></wa-input> `);
const input = el.shadowRoot!.querySelector<HTMLInputElement>('input')!;
expect(input.getAttribute('spellcheck')).to.equal('true');
expect(input.spellcheck).to.be.true;
});
it('should disable spellcheck when set to "false"', async () => {
const el = await fixture<WaInput>(html` <wa-input spellcheck="false"></wa-input> `);
const input = el.shadowRoot!.querySelector<HTMLInputElement>('input')!;
expect(input.getAttribute('spellcheck')).to.equal('false');
expect(input.spellcheck).to.be.false;
});
});
it('should submit with the correct form when the form attribute changes', async () => {
const el = await fixture<HTMLFormElement>(html`
<div>
<form id="f1">
<input type="hidden" name="b" value="2" />
<wa-button type="submit">Submit</wa-button>
</form>
<form id="f2">
<input type="hidden" name="c" value="3" />
<wa-button type="submit">Submit</wa-button>
</form>
<wa-input form="f1" name="a" value="1"></wa-input>
</div>
`);
const form = el.querySelector<HTMLFormElement>('#f2')!;
const input = document.querySelector('wa-input')!;
input.form = 'f2';
await input.updateComplete;
const formData = new FormData(form);
expect(formData.get('a')).to.equal('1');
expect(formData.get('b')).to.be.null;
expect(formData.get('c')).to.equal('3');
});
describe('when using the setRangeText() function', () => {
it('should set replacement text in the correct location', async () => {
const el = await fixture<WaInput>(html` <wa-input value="test"></wa-input> `);
el.focus();
el.setSelectionRange(1, 3);
el.setRangeText('boom');
await el.updateComplete;
expect(el.value).to.equal('tboomt'); // cspell:disable-line
});
});
it('Should be invalid if the pattern is invalid', async () => {
const el = await fixture<WaInput>(html` <wa-input required pattern="1234"></wa-input> `);
el.input.focus();
await el.updateComplete;
expect(el.checkValidity()).to.be.false;
await aTimeout(10);
await sendKeys({ type: '1234' });
await el.updateComplete;
await aTimeout(10);
// For some reason this is only required in Safari.
if (isSafari) {
el.setCustomValidity('');
}
expect(el.checkValidity()).to.be.true;
});
it('Should be invalid if minlength is not met', async () => {
const el = await fixture<WaInput>(html` <wa-input required minlength="3"></wa-input> `);
el.input.focus();
await el.updateComplete;
expect(el.checkValidity()).to.be.false;
await sendKeys({ type: '12' });
await el.updateComplete;
await aTimeout(10);
expect(el.checkValidity()).to.be.false;
expect(el.validity.tooShort).to.be.true;
});
it('Should be invalid if maxlength is not met', async () => {
const el = await fixture<WaInput>(html` <wa-input required maxlength="3" value="Hello World"></wa-input> `);
await el.updateComplete;
// There's a fun problem around programmaticly setting values and default values from attributes.
// The TLDR is this input is valid, until the user starts typing.
expect(el.checkValidity()).to.be.true;
el.input.focus();
await sendKeys({ press: 'ArrowRight' });
await sendKeys({ press: 'Backspace' });
await el.updateComplete;
await aTimeout(10);
expect(el.checkValidity()).to.be.false;
expect(el.validity.tooLong).to.be.true;
});
});
}
});