Библиотека D3.js тесно связана с DOM: большинство её операций опираются на выборки, атрибуты и манипуляции SVG/HTML-узлами. Это создаёт ключевую проблему при модульном тестировании — необходимость воспроизводимого DOM-окружения вне браузера.
В серверной среде отсутствует полноценный DOM, поэтому используется jsdom, которая эмулирует браузерную модель документа. В связке с d3-selection это позволяет тестировать поведение компонентов без реального рендеринга.
Основная цель тестирования D3-компонентов заключается не в проверке визуального результата, а в валидации:
Инициализация виртуального документа требует создания глобальных
объектов window и document, которые ожидаются
D3.
import { JSDOM } from "jsdom";
const dom = new JSDOM(`<!DOCTYPE html><body></body>`);
global.window = dom.window;
global.document = dom.window.document;
После этого d3-selection начинает работать так же, как в браузере,
поскольку получает доступ к DOM API через document.
Важный момент: многие методы D3 используют
document.createElementNS, особенно при работе с SVG. jsdom
поддерживает SVG namespace, но иногда требует явного использования
createElementNS.
d3-selection предоставляет механизм связывания данных с DOM через
select, selectAll, append,
attr, text.
Пример теста создания элемента:
import * as d3 from "d3";
document.body.innerHTML = "<div id='root'></div>";
d3.select("#root")
.append("p")
.text("Hello D3");
const result = document.querySelector("p").textContent;
expect(result).toBe("Hello D3");
Здесь проверяется не визуальный результат, а факт изменения DOM-структуры.
Механизм связывания данных с элементами — основа D3. Он тестируется через проверку количества созданных DOM-узлов и их соответствия входному массиву.
const data = [10, 20, 30];
d3.select("#root")
.selectAll("div")
.data(data)
.enter()
.append("div")
.text(d => d);
Проверка:
const nodes = document.querySelectorAll("div");
expect(nodes.length).toBe(3);
Ключевой аспект: проверяется не только количество элементов, но и
корректность привязки данных через __data__.
expect(nodes[0].__data__).toBe(10);
Это внутренний механизм D3, который сохраняет данные прямо в DOM-узле.
D3 активно использует паттерн enter–update–exit. Проверка этого поведения особенно важна при изменении данных.
Пример обновления:
let selection = d3.select("#root")
.selectAll("p")
.data([1, 2, 3]);
selection.enter()
.append("p")
.text(d => d);
selection = d3.select("#root")
.selectAll("p")
.data([2, 3, 4]);
selection.text(d => d);
Проверка:
const texts = [...document.querySelectorAll("p")].map(el => el.textContent);
expect(texts).toEqual(["2", "3", "4"]);
При тестировании обновлений важно учитывать:
Ключевые функции (key function) критичны для стабильных
обновлений DOM.
const data = [
{ id: "a", value: 10 },
{ id: "b", value: 20 }
];
d3.select("#root")
.selectAll("div")
.data(data, d => d.id)
.enter()
.append("div")
.text(d => d.value);
Тестирование стабильности:
const firstNodes = document.querySelectorAll("div");
d3.select("#root")
.selectAll("div")
.data([{ id: "b", value: 200 }], d => d.id)
.text(d => d.value);
const updatedNodes = document.querySelectorAll("div");
expect(updatedNodes.length).toBe(1);
expect(updatedNodes[0].textContent).toBe("200");
Ключ гарантирует, что DOM-узлы не пересоздаются без необходимости.
SVG требует namespace:
const svg = d3.select("#root")
.append("svg")
.attr("width", 100)
.attr("height", 100);
svg.append("circle")
.attr("cx", 50)
.attr("cy", 50)
.attr("r", 10);
Проверка:
const circle = document.querySelector("circle");
expect(circle.getAttribute("cx")).toBe("50");
expect(circle.getAttribute("r")).toBe("10");
jsdom корректно хранит SVG-узлы, но не выполняет рендеринг, поэтому тестируется только структура.
d3-selection активно управляет атрибутами и стилями DOM-элементов.
d3.select("#root")
.append("div")
.style("color", "red")
.attr("data-type", "test");
Проверка:
const el = document.querySelector("div");
expect(el.style.color).toBe("red");
expect(el.getAttribute("data-type")).toBe("test");
Важно учитывать, что style() в D3 изменяет inline
styles, а не CSS классы.
D3 поддерживает привязку событий через .on().
let clicked = false;
d3.select("#root")
.append("button")
.text("Click")
.on("click", () => {
clicked = true;
});
Тестирование:
const button = document.querySelector("button");
button.dispatchEvent(new window.Event("click"));
expect(clicked).toBe(true);
jsdom позволяет диспатчить события вручную, что делает возможным проверку интерактивной логики.
Несмотря на функциональность, jsdom не реализует полноценный браузерный рендеринг:
Это влияет на тестирование:
getBBox() для SVG часто недоступен;getComputedTextLength() может отсутствовать;Поэтому тесты ограничиваются проверкой DOM-состояния после синхронного выполнения D3-кода.
D3 transitions используют таймеры и requestAnimationFrame. В jsdom это поведение нужно эмулировать через фейковые таймеры.
jest.useFakeTimers();
d3.select("#root")
.append("div")
.transition()
.duration(1000)
.style("opacity", 0);
Продвижение времени:
jest.advanceTimersByTime(1000);
Проверка:
const el = document.querySelector("div");
expect(el.style.opacity).toBe("0");
Важно: переходы в D3 зависят от внутреннего scheduler, поэтому тесты должны учитывать необходимость симуляции времени.
Компоненты, построенные на D3, обычно тестируются по слоям:
Слой данных
DOM слой
Поведенческий слой
Интеграционный слой
Типичный компонент можно структурировать так, чтобы его тестирование не зависело от глобального состояния:
export function chart(selection, data) {
const root = selection.append("g");
root.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("width", d => d)
.attr("height", 10);
}
Тест:
const root = d3.select(document.body).append("svg");
chart(root, [10, 20]);
expect(document.querySelectorAll("rect").length).toBe(2);
Такой подход отделяет визуальную логику от окружения, делая её воспроизводимой в jsdom.
d3-selection строится на цепочках вызовов. Ошибки часто возникают при неправильном порядке операций.
const sel = d3.select("#root")
.append("div")
.selectAll("span")
.data([1, 2, 3])
.enter()
.append("span")
.text(d => d);
Тестирование здесь сводится к проверке конечного DOM:
expect(document.querySelectorAll("span").length).toBe(3);
При этом важно учитывать, что промежуточные selection-объекты не всегда имеют смысл тестировать напрямую — они служат только для построения цепочки.
Некоторые D3-модули могут использовать:
performance.nowrequestAnimationFramewindow.getComputedStyleВ jsdom их часто требуется замещать:
global.requestAnimationFrame = (cb) => setTimeout(cb, 0);
или
global.performance = {
now: () => Date.now()
};
Без таких замен тесты transitions и таймингов становятся нестабильными.
Для стабильности тестирования D3-компонентов используется изоляция:
document.body.innerHTML;Типичная схема:
beforeEach(() => {
document.body.innerHTML = "<div id='root'></div>";
});
Exit-паттерн важен для динамических визуализаций.
let data = [1, 2, 3];
let sel = d3.select("#root")
.selectAll("p")
.data(data);
sel.enter().append("p").text(d => d);
data = [1];
sel = d3.select("#root")
.selectAll("p")
.data(data);
sel.exit().remove();
Тест:
expect(document.querySelectorAll("p").length).toBe(1);
Каждый вызов selectAll создаёт новую selection-обёртку,
не мутируя DOM напрямую.
const s1 = d3.select("#root").selectAll("div");
const s2 = d3.select("#root").selectAll("div");
Обе переменные независимы, и тестирование должно опираться на DOM, а не на ссылки selection-объектов.
D3 активно использует функции обратного вызова.
d3.select("#root")
.selectAll("p")
.data([1, 2, 3])
.enter()
.append("p")
.text((d, i) => `${i}:${d}`);
Тест:
const texts = [...document.querySelectorAll("p")].map(d => d.textContent);
expect(texts).toEqual(["0:1", "1:2", "2:3"]);
Проверяется корректность индексов и передаваемых значений.
Многократные обновления данных часто выявляют скрытые ошибки.
function update(data) {
const sel = d3.select("#root")
.selectAll("div")
.data(data);
sel.enter().append("div");
sel.exit().remove();
}
Тестирование:
update([1, 2]);
update([1, 2, 3]);
update([1]);
expect(document.querySelectorAll("div").length).toBe(1);
Такие тесты фиксируют корректность работы join-механизма.