Choices/test/scripts/components/input.test.ts
terminalchai e467bc0de0 refactor(input): replace Intl.Segmenter with DOM-based width measurement
Remove isWideChar() and getStringWidth() entirely. Instead, measure text
width by inserting a hidden <span> that inherits the input element's CSS
classes, setting innerText, and reading clientWidth. Dividing by the span's
own 1ch pixel value converts the measurement back into ch units.

This approach correctly handles CJK, Hangul, fullwidth forms, emoji, and
any font without hard-coded code-point ranges or reliance on Intl.Segmenter.

Remove unit tests for setWidth() that depended on jsdom layout measurement
(jsdom has no layout engine). Width behaviour is verified by e2e tests.

Addresses Xon's review feedback on PR #1384.
2026-03-11 00:26:29 +05:30

314 lines
8.5 KiB
TypeScript

import { expect } from 'chai';
import { stub } from 'sinon';
import { DEFAULT_CLASSNAMES } from '../../../src';
import Input from '../../../src/scripts/components/input';
describe('components/input', () => {
let instance;
let choicesElement;
beforeEach(() => {
choicesElement = document.createElement('input');
instance = new Input({
element: choicesElement,
type: 'text',
classNames: DEFAULT_CLASSNAMES,
preventPaste: false,
});
});
afterEach(() => {
document.body.innerHTML = '';
instance = null;
});
describe('constructor', () => {
it('assigns choices element to class', () => {
expect(instance.element).to.equal(choicesElement);
});
it('assigns classnames to class', () => {
expect(instance.classNames).to.deep.equal(DEFAULT_CLASSNAMES);
});
});
describe('addEventListeners', () => {
let addEventListenerStub;
beforeEach(() => {
addEventListenerStub = stub(instance.element, 'addEventListener');
});
afterEach(() => {
addEventListenerStub.restore();
});
it('adds event listeners', () => {
instance.addEventListeners();
expect(['input', 'paste', 'focus', 'blur']).to.have.members(
Array.from({ length: addEventListenerStub.callCount }, (_, i) => addEventListenerStub.getCall(i).args[0]),
);
});
});
describe('removeEventListeners', () => {
let removeEventListenerStub;
beforeEach(() => {
removeEventListenerStub = stub(instance.element, 'removeEventListener');
});
afterEach(() => {
removeEventListenerStub.restore();
});
it('removes event listeners', () => {
instance.removeEventListeners();
expect(removeEventListenerStub.callCount).to.equal(4);
expect(removeEventListenerStub.getCall(0).args[0]).to.equal('input');
expect(removeEventListenerStub.getCall(1).args[0]).to.equal('paste');
expect(removeEventListenerStub.getCall(2).args[0]).to.equal('focus');
expect(removeEventListenerStub.getCall(3).args[0]).to.equal('blur');
});
});
describe('_onInput', () => {
let setWidthStub;
beforeEach(() => {
setWidthStub = stub(instance, 'setWidth');
});
afterEach(() => {
setWidthStub.restore();
});
describe('when element is select one', () => {
it('does not set input width', () => {
instance.type = 'select-one';
instance._onInput();
expect(setWidthStub.callCount).to.equal(0);
});
});
describe('when element is not a select one', () => {
it('sets input width', () => {
instance.type = 'text';
instance._onInput();
expect(setWidthStub.callCount).to.equal(1);
});
});
});
describe('_onPaste', () => {
let eventMock;
beforeEach(() => {
eventMock = {
preventDefault: stub(),
target: instance.element,
};
});
describe('when pasting is disabled and target is the element', () => {
it('prevents default pasting behaviour', () => {
instance.preventPaste = true;
instance._onPaste(eventMock);
expect(eventMock.preventDefault.callCount).to.equal(1);
});
});
describe('when pasting is enabled', () => {
it('does not prevent default pasting behaviour', () => {
instance.preventPaste = false;
instance._onPaste(eventMock);
expect(eventMock.preventDefault.callCount).to.equal(0);
});
});
});
describe('_onFocus', () => {
it('sets isFocussed flag to true', () => {
expect(instance.isFocussed).to.equal(false);
instance._onFocus();
expect(instance.isFocussed).to.equal(true);
});
});
describe('_onBlur', () => {
it('sets isFocussed flag to false', () => {
instance.isFocussed = true;
instance._onBlur();
expect(instance.isFocussed).to.equal(false);
});
});
describe('enable', () => {
beforeEach(() => {
instance.element.setAttribute('disabled', '');
instance.isDisabled = true;
instance.enable();
});
it('removes disabled attribute', () => {
expect(instance.element.getAttribute('disabled')).to.equal(null);
});
it('sets isDisabled flag to false', () => {
expect(instance.isDisabled).to.equal(false);
});
});
describe('disable', () => {
beforeEach(() => {
instance.element.removeAttribute('disabled', '');
instance.isDisabled = false;
instance.disable();
});
it('removes disabled attribute', () => {
expect(instance.element.getAttribute('disabled')).to.equal('');
});
it('sets isDisabled flag to false', () => {
expect(instance.isDisabled).to.equal(true);
});
});
describe('focus', () => {
let focusStub;
beforeEach(() => {
focusStub = stub(instance.element, 'focus');
});
afterEach(() => {
focusStub.restore();
});
describe('when element is not focussed', () => {
it('focuses element if isFocussed flag is set to false', () => {
instance.isFocussed = true;
instance.focus();
expect(focusStub.callCount).to.equal(0);
});
});
describe('when element is focussed', () => {
it('focuses element if isFocussed flag is set to false', () => {
instance.isFocussed = false;
instance.focus();
expect(focusStub.callCount).to.equal(1);
});
});
});
describe('blur', () => {
let blurStub;
beforeEach(() => {
blurStub = stub(instance.element, 'blur');
});
afterEach(() => {
blurStub.restore();
});
describe('when element is not focussed', () => {
it("doesn't blur element", () => {
instance.isFocussed = false;
instance.blur();
expect(blurStub.callCount).to.equal(0);
});
});
describe('when element is focussed', () => {
it('blurs element', () => {
instance.isFocussed = true;
instance.blur();
expect(blurStub.callCount).to.equal(1);
});
});
});
describe('clear', () => {
let setWidthStub;
beforeEach(() => {
setWidthStub = stub(instance, 'setWidth');
});
afterEach(() => {
setWidthStub.restore();
});
it("removes the element's value if it has one", () => {
instance.element.value = 'test';
expect(instance.element.value).to.equal('test');
instance.clear();
expect(instance.element.value).to.equal('');
});
it("sets the element's width if flag passed", () => {
expect(setWidthStub.callCount).to.equal(0);
instance.clear(true);
expect(setWidthStub.callCount).to.equal(1);
});
it('returns instance', () => {
const response = instance.clear();
expect(response).to.deep.equal(instance);
});
});
describe('placeholder setter', () => {
it('sets value of element to passed placeholder', () => {
const placeholder = 'test';
expect(instance.element.placeholder).to.equal('');
instance.placeholder = placeholder;
expect(instance.element.placeholder).to.equal(placeholder);
});
});
describe('value setter', () => {
it('sets value of element to passed value', () => {
const value = 'test';
expect(instance.element.value).to.equal('');
instance.value = value;
expect(instance.element.value).to.equal(value);
});
it('casts value to string', () => {
const value = 1234;
instance.value = value;
expect(instance.element.value).to.equal(`${value}`);
});
});
describe('value getter', () => {
it('sets value of element to passed value', () => {
const value = 'test';
instance.element.value = value;
expect(instance.value).to.equal(value);
});
});
describe('setActiveDescendant', () => {
it("sets element's aria-activedescendant attribute with passed descendant ID", () => {
const activeDescendantID = '1234';
expect(instance.element.getAttribute('aria-activedescendant')).to.equal(null);
instance.setActiveDescendant(activeDescendantID);
expect(instance.element.getAttribute('aria-activedescendant')).to.equal(activeDescendantID);
});
});
describe('removeActiveDescendant', () => {
it("remove elememnt's aria-activedescendant attribute", () => {
const activeDescendantID = '1234';
instance.element.setAttribute('aria-activedescendant', activeDescendantID);
expect(instance.element.getAttribute('aria-activedescendant')).to.equal(activeDescendantID);
instance.removeActiveDescendant();
expect(instance.element.getAttribute('aria-activedescendant')).to.equal(null);
});
});
});