Модульное тестирование в библиотеке Awesomplete применяется для проверки отдельных частей логики автодополнения без необходимости запускать полноценное браузерное окружение. Тестируются:
replace, filter,
sort;Главная цель модульных тестов — гарантировать стабильность поведения компонента при изменении кода.
Большинство тестов для подобных UI-библиотек делится на несколько уровней:
| Тип теста | Проверяемая часть |
|---|---|
| Unit tests | Отдельные методы и функции |
| Integration tests | Взаимодействие компонентов |
| DOM tests | Работа с HTML |
| Event tests | Обработка событий |
| Async tests | Асинхронная логика |
Для Awesomplete наиболее важны именно unit- и DOM-тесты, поскольку библиотека активно взаимодействует с:
<input>;<ul>;Чаще всего используются:
| Инструмент | Назначение |
|---|---|
| Jest | Тестовый раннер |
| Mocha | Альтернативный раннер |
| Chai | Assertions |
| Sinon | Моки и шпионы |
| jsdom | Эмуляция DOM |
| Vitest | Современная альтернатива Jest |
npm install --save-dev jest jsdom
Настройка package.json:
{
"scripts": {
"test": "jest"
}
}
Настройка среды:
{
"jest": {
"testEnvironment": "jsdom"
}
}
import Awesomplete from "awesomplete";
describe("Awesomplete initialization", () => {
test("creates instance", () => {
document.body.innerHTML =
'<input id="city" />';
const input = document.getElementById("city");
const instance = new Awesomplete(input);
expect(instance).toBeDefined();
});
});
Проверяются:
test("assigns list correctly", () => {
document.body.innerHTML =
'<input id="lang" />';
const input = document.getElementById("lang");
const awesomplete = new Awesomplete(input, {
list: ["JavaScript", "Python", "Rust"]
});
expect(awesomplete._list.length).toBe(3);
});
Awesomplete использует функцию FILTER_CONTAINS и другие
механизмы фильтрации.
test("filters matching values", () => {
document.body.innerHTML =
'<input id="test" />';
const input = document.getElementById("test");
const awesomplete = new Awesomplete(input, {
list: ["JavaScript", "Java", "Python"]
});
input.value = "Java";
const result = awesomplete._list.filter(item =>
Awesomplete.FILTER_CONTAINS(item, input.value)
);
expect(result.length).toBe(2);
});
test("uses custom filter", () => {
document.body.innerHTML =
'<input id="custom" />';
const input = document.getElementById("custom");
const awesomplete = new Awesomplete(input, {
list: ["Cat", "Dog", "Bird"],
filter: (text, input) => {
return text.startsWith(input);
}
});
input.value = "C";
const result = awesomplete._list.filter(item =>
awesomplete.filter(item, input.value)
);
expect(result[0]).toBe("Cat");
});
test("sorts items alphabetically", () => {
const items = ["Python", "Java", "C++"];
items.sort(Awesomplete.SORT_BYLENGTH);
expect(items[0]).toBe("Java");
});
Awesomplete динамически создаёт элементы <li>.
test("renders suggestion list", () => {
document.body.innerHTML =
'<input id="render" />';
const input = document.getElementById("render");
const awesomplete = new Awesomplete(input, {
list: ["Apple", "Orange"]
});
input.value = "A";
awesomplete.evaluate();
const items =
awesomplete.ul.children;
expect(items.length).toBe(1);
});
test("renders correct text", () => {
document.body.innerHTML =
'<input id="fruit" />';
const input = document.getElementById("fruit");
const awesomplete = new Awesomplete(input, {
list: ["Apple"]
});
input.value = "A";
awesomplete.evaluate();
const text =
awesomplete.ul.children[0].textContent;
expect(text).toContain("Apple");
});
Awesomplete генерирует несколько важных событий:
| Событие | Назначение |
|---|---|
awesomplete-open |
Открытие списка |
awesomplete-close |
Закрытие |
awesomplete-select |
Выбор элемента |
awesomplete-highlight |
Наведение |
test("fires sel ect event", () => {
document.body.innerHTML =
'<input id="event" />';
const input = document.getElementById("event");
const awesomplete = new Awesomplete(input, {
list: ["JavaScript"]
});
const handler = jest.fn();
input.addEventListener(
"awesomplete-select",
handler
);
input.value = "J";
awesomplete.evaluate();
awesomplete.select(
awesomplete.ul.children[0]
);
expect(handler).toHaveBeenCalled();
});
test("opens suggestion panel", () => {
document.body.innerHTML =
'<input id="open" />';
const input = document.getElementById("open");
const awesomplete = new Awesomplete(input, {
list: ["One"]
});
input.value = "O";
awesomplete.evaluate();
expect(
awesomplete.ul.hasAttribute("hidden")
).toBe(false);
});
test("closes suggestion panel", () => {
document.body.innerHTML =
'<input id="close" />';
const input = document.getElementById("close");
const awesomplete = new Awesomplete(input, {
list: ["One"]
});
awesomplete.close();
expect(
awesomplete.ul.hasAttribute("hidden")
).toBe(true);
});
Awesomplete активно использует:
test("moves selection with keyboard", () => {
document.body.innerHTML =
'<input id="keyboard" />';
const input = document.getElementById("keyboard");
const awesomplete = new Awesomplete(input, {
list: ["One", "Two"]
});
input.value = "O";
awesomplete.evaluate();
awesomplete.next();
expect(
awesomplete.index
).toBe(0);
});
test("selects item with enter", () => {
document.body.innerHTML =
'<input id="enter" />';
const input = document.getElementById("enter");
const awesomplete = new Awesomplete(input, {
list: ["JavaScript"]
});
input.value = "J";
awesomplete.evaluate();
awesomplete.next();
awesomplete.select();
expect(input.value).toBe("JavaScript");
});
test("uses custom replace", () => {
document.body.innerHTML =
'<input id="replace" />';
const input = document.getElementById("replace");
const awesomplete = new Awesomplete(input, {
list: ["JavaScript"],
replace: function(text) {
this.input.value =
text.toUpperCase();
}
});
awesomplete.replace("JavaScript");
expect(input.value)
.toBe("JAVASCRIPT");
});
test("updates async list", async () => {
document.body.innerHTML =
'<input id="async" />';
const input = document.getElementById("async");
const awesomplete =
new Awesomplete(input);
const data =
await Promise.resolve([
"Alpha",
"Beta"
]);
awesomplete.list = data;
expect(
awesomplete._list.length
).toBe(2);
});
test("calls callback", () => {
const callback = jest.fn();
callback();
expect(callback)
.toHaveBeenCalledTimes(1);
});
test("spies on evaluate", () => {
document.body.innerHTML =
'<input id="spy" />';
const input =
document.getElementById("spy");
const awesomplete =
new Awesomplete(input);
const spy =
jest.spyOn(awesomplete, "evaluate");
awesomplete.evaluate();
expect(spy)
.toHaveBeenCalled();
});
Каждый тест должен работать независимо.
afterEach(() => {
document.body.innerHTML = "";
});
Без очистки:
Особое внимание уделяется нестандартным ситуациям.
test("handles empty list", () => {
document.body.innerHTML =
'<input id="empty" />';
const input = document.getElementById("empty");
const awesomplete =
new Awesomplete(input, {
list: []
});
input.value = "A";
awesomplete.evaluate();
expect(
awesomplete.ul.children.length
).toBe(0);
});
test("handles null values", () => {
document.body.innerHTML =
'<input id="null" />';
const input =
document.getElementById("null");
expect(() => {
new Awesomplete(input, {
list: null
});
}).not.toThrow();
});
test("works with large dataset", () => {
document.body.innerHTML =
'<input id="large" />';
const input =
document.getElementById("large");
const data =
Array.fr om(
{ length: 10000 },
(_, i) => `Item ${i}`
);
const awesomplete =
new Awesomplete(input, {
list: data
});
expect(
awesomplete._list.length
).toBe(10000);
});
Для UI-компонентов важно контролировать:
test("filters quickly", () => {
const data =
Array.from(
{ length: 5000 },
(_, i) => `Element ${i}`
);
const start = performance.now();
data.filter(item =>
item.includes("100")
);
const end = performance.now();
expect(end - start)
.toBeLessThan(10);
});
В UI-библиотеках возможны проблемы:
test("removes references", () => {
document.body.innerHTML =
'<input id="memory" />';
const input =
document.getElementById("memory");
let awesomplete =
new Awesomplete(input);
awesomplete = null;
expect(awesomplete)
.toBeNull();
});
Jest позволяет сохранять DOM-состояния.
test("matches snapshot", () => {
document.body.innerHTML =
'<input id="snap" />';
const input =
document.getElementById("snap");
const awesomplete =
new Awesomplete(input, {
list: ["One"]
});
input.value = "O";
awesomplete.evaluate();
expect(
awesomplete.ul.innerHTML
).toMatchSnapshot();
});
Типичная структура:
tests/
├── initialization.test.js
├── filtering.test.js
├── rendering.test.js
├── events.test.js
├── keyboard.test.js
├── async.test.js
└── performance.test.js
Хорошее имя теста:
test("opens dropdown after typing")
Плохое имя:
test("works")
Название должно описывать:
Структура большинства тестов:
| Этап | Описание |
|---|---|
| Arrange | Подготовка |
| Act | Выполнение |
| Assert | Проверка |
test("selects suggestion", () => {
// Arrange
document.body.innerHTML =
'<input id="aaa" />';
const input =
document.getElementById("aaa");
const awesomplete =
new Awesomplete(input, {
list: ["Alpha"]
});
// Act
input.value = "A";
awesomplete.evaluate();
awesomplete.select();
// Assert
expect(input.value)
.toBe("Alpha");
});
Без DOM-среды тесты падают:
document is not defined
Тестирование _list и других приватных полей делает тесты
хрупкими.
Лучше проверять:
Один тест должен проверять одну вещь.
Плохо:
test("everything works")
Хорошо:
test("opens dropdown")
test("filters items")
test("selects value")
Модульные тесты обычно запускаются:
name: tests
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 20
- run: npm install
- run: npm test
Jest поддерживает coverage-анализ.
npm test -- --coverage
| Метрика | Описание |
|---|---|
| Statements | Выполненные инструкции |
| Branches | Ветвления |
| Functions | Функции |
| Lines | Строки |
Наиболее эффективный подход включает: