Ты делаешь корзину товаров: по клику на кнопку "Добавить" нужно найти ближайшую карточку, достать из неё цену и id, обновить счётчик в шапке. Всё это начинается с поиска нужных элементов в DOM. Неправильный метод поиска — и ты получаешь "живую" коллекцию, которая меняется прямо во время итерации.
parentNode, children, nextSiblingArray.from(), forEach, spread для конвертации коллекцийСамый быстрый способ найти элемент по уникальному идентификатору:
const modal = document.getElementById('checkout-modal')
const form = document.getElementById('login-form')
// Возвращает Element | nullВозвращают живую HTMLCollection — она автоматически обновляется при изменении DOM:
const cards = document.getElementsByClassName('product-card')
const inputs = document.getElementsByTagName('input')
console.log(cards.length) // 5
// если добавить новую карточку в DOM → cards.length сразу станет 6Живая коллекция может вызывать неожиданное поведение в циклах — лучше конвертировать в массив:
const arr = Array.from(document.getElementsByClassName('item'))Принимает любой CSS-селектор и возвращает первый подходящий элемент или null:
document.querySelector('.product-card') // первая карточка
document.querySelector('#nav > a.active') // активная ссылка в #nav
document.querySelector('[data-role="submit"]') // по data-атрибуту
document.querySelector('form input[required]') // первый required input в формеВозвращает статичный NodeList — снимок DOM на момент вызова:
const allCards = document.querySelectorAll('.product-card')
const allLinks = document.querySelectorAll('nav a')
const checkedBoxes = document.querySelectorAll('input[type="checkbox"]:checked')
// NodeList — итерируемый, поддерживает forEach и for...of
allCards.forEach(card => card.classList.add('loaded'))
// Но spread нужен для методов массива:
const hrefs = [...allLinks].map(a => a.getAttribute('href'))| Метод | Тип | Обновляется при изменении DOM |
|---|---|---|
| getElementsByClassName | HTMLCollection | Да (живая) |
| getElementsByTagName | HTMLCollection | Да (живая) |
| querySelectorAll | NodeList | Нет (статичная) |
Любой элемент также имеет методы querySelector и querySelectorAll — поиск ведётся только среди его потомков:
const sidebar = document.querySelector('.sidebar')
const sidebarLinks = sidebar.querySelectorAll('a') // только ссылки внутри sidebar
const firstBtn = sidebar.querySelector('button') // первая кнопка в sidebar// Карточка товара по data-id
const card = document.querySelector('[data-product-id="42"]')
// Первый невалидный input
const invalid = document.querySelector('input:invalid')
// Кнопки с data-action
const actionBtns = document.querySelectorAll('[data-action]')
// Элемент с aria-expanded="true"
const expanded = document.querySelector('[aria-expanded="true"]')1. Итерация живой HTMLCollection с удалением элементов:
const items = document.getElementsByClassName('item')
// Плохо: живая коллекция меняется при удалении
for (let i = 0; i < items.length; i++) {
items[i].remove() // после удаления items.length уменьшается — пропускаем элементы!
}
// Хорошо: конвертируй в статичный массив сначала
Array.from(items).forEach(item => item.remove())2. Забыть проверить на null перед использованием:
// Плохо: querySelector вернёт null если элемент не найден
const btn = document.querySelector('.submit-btn')
btn.addEventListener('click', handler) // TypeError: Cannot read properties of null
// Хорошо: optional chaining
document.querySelector('.submit-btn')?.addEventListener('click', handler)3. NodeList — не массив, у него нет .map() и .filter():
const links = document.querySelectorAll('a')
// Плохо:
links.map(a => a.href) // TypeError: links.map is not a function
// Хорошо:
Array.from(links).map(a => a.href)
[...links].map(a => a.href)closest()querySelectorAll('[data-component]') для массовой инициализацииform.querySelectorAll('input:invalid') после submitscreen.getByRole, screen.getByText работают по тем же принципамПоскольку код выполняется в изолированном окружении без настоящего DOM, примеры ниже симулируют поведение браузерных методов через JavaScript-объекты. Принципы работы — те же самые, что и в браузере.
Симуляция querySelectorAll через обход дерева mock-объектов
// Симулируем узлы DOM как обычные объекты
function el(tag, attrs = {}, children = []) {
return { tag, attrs, children }
}
// Дерево: ul > [li.item, li.item.active, li.item, div.promo]
const root = el('ul', { id: 'menu' }, [
el('li', { class: 'item' }, [
el('a', { href: '/home', 'data-section': 'home' }),
]),
el('li', { class: 'item active' }, [
el('a', { href: '/catalog', 'data-section': 'catalog' }),
]),
el('li', { class: 'item' }, [
el('a', { href: '/cart', 'data-section': 'cart' }),
]),
el('div', { class: 'promo' }),
])
// Рекурсивный обход — аналог querySelectorAll по тегу
function queryAllByTag(node, tag, results = []) {
if (node.tag === tag) results.push(node)
node.children.forEach(child => queryAllByTag(child, tag, results))
return results
}
// Рекурсивный обход — аналог querySelectorAll по классу
function queryAllByClass(node, cls, results = []) {
const classes = (node.attrs.class || '').split(' ')
if (classes.includes(cls)) results.push(node)
node.children.forEach(child => queryAllByClass(child, cls, results))
return results
}
// querySelector — первый совпавший
function querySelector(node, tag) {
const all = queryAllByTag(node, tag)
return all[0] ?? null
}
const allLinks = queryAllByTag(root, 'a')
console.log('Все ссылки:', allLinks.length) // 3
console.log('href первой:', allLinks[0].attrs.href) // '/home'
const activeItems = queryAllByClass(root, 'active')
console.log('Активных элементов:', activeItems.length) // 1
console.log('Тег активного:', activeItems[0].tag) // 'li'
const firstLink = querySelector(root, 'a')
console.log('Первая ссылка ведёт на:', firstLink?.attrs.href) // '/home'
// Поиск по data-атрибуту
function queryByDataAttr(node, attr, value, results = []) {
if (node.attrs[attr] === value) results.push(node)
node.children.forEach(child => queryByDataAttr(child, attr, value, results))
return results
}
const catalogLink = queryByDataAttr(root, 'data-section', 'catalog')
console.log('Ссылка каталога:', catalogLink[0]?.attrs.href) // '/catalog'Ты делаешь корзину товаров: по клику на кнопку "Добавить" нужно найти ближайшую карточку, достать из неё цену и id, обновить счётчик в шапке. Всё это начинается с поиска нужных элементов в DOM. Неправильный метод поиска — и ты получаешь "живую" коллекцию, которая меняется прямо во время итерации.
parentNode, children, nextSiblingArray.from(), forEach, spread для конвертации коллекцийСамый быстрый способ найти элемент по уникальному идентификатору:
const modal = document.getElementById('checkout-modal')
const form = document.getElementById('login-form')
// Возвращает Element | nullВозвращают живую HTMLCollection — она автоматически обновляется при изменении DOM:
const cards = document.getElementsByClassName('product-card')
const inputs = document.getElementsByTagName('input')
console.log(cards.length) // 5
// если добавить новую карточку в DOM → cards.length сразу станет 6Живая коллекция может вызывать неожиданное поведение в циклах — лучше конвертировать в массив:
const arr = Array.from(document.getElementsByClassName('item'))Принимает любой CSS-селектор и возвращает первый подходящий элемент или null:
document.querySelector('.product-card') // первая карточка
document.querySelector('#nav > a.active') // активная ссылка в #nav
document.querySelector('[data-role="submit"]') // по data-атрибуту
document.querySelector('form input[required]') // первый required input в формеВозвращает статичный NodeList — снимок DOM на момент вызова:
const allCards = document.querySelectorAll('.product-card')
const allLinks = document.querySelectorAll('nav a')
const checkedBoxes = document.querySelectorAll('input[type="checkbox"]:checked')
// NodeList — итерируемый, поддерживает forEach и for...of
allCards.forEach(card => card.classList.add('loaded'))
// Но spread нужен для методов массива:
const hrefs = [...allLinks].map(a => a.getAttribute('href'))| Метод | Тип | Обновляется при изменении DOM |
|---|---|---|
| getElementsByClassName | HTMLCollection | Да (живая) |
| getElementsByTagName | HTMLCollection | Да (живая) |
| querySelectorAll | NodeList | Нет (статичная) |
Любой элемент также имеет методы querySelector и querySelectorAll — поиск ведётся только среди его потомков:
const sidebar = document.querySelector('.sidebar')
const sidebarLinks = sidebar.querySelectorAll('a') // только ссылки внутри sidebar
const firstBtn = sidebar.querySelector('button') // первая кнопка в sidebar// Карточка товара по data-id
const card = document.querySelector('[data-product-id="42"]')
// Первый невалидный input
const invalid = document.querySelector('input:invalid')
// Кнопки с data-action
const actionBtns = document.querySelectorAll('[data-action]')
// Элемент с aria-expanded="true"
const expanded = document.querySelector('[aria-expanded="true"]')1. Итерация живой HTMLCollection с удалением элементов:
const items = document.getElementsByClassName('item')
// Плохо: живая коллекция меняется при удалении
for (let i = 0; i < items.length; i++) {
items[i].remove() // после удаления items.length уменьшается — пропускаем элементы!
}
// Хорошо: конвертируй в статичный массив сначала
Array.from(items).forEach(item => item.remove())2. Забыть проверить на null перед использованием:
// Плохо: querySelector вернёт null если элемент не найден
const btn = document.querySelector('.submit-btn')
btn.addEventListener('click', handler) // TypeError: Cannot read properties of null
// Хорошо: optional chaining
document.querySelector('.submit-btn')?.addEventListener('click', handler)3. NodeList — не массив, у него нет .map() и .filter():
const links = document.querySelectorAll('a')
// Плохо:
links.map(a => a.href) // TypeError: links.map is not a function
// Хорошо:
Array.from(links).map(a => a.href)
[...links].map(a => a.href)closest()querySelectorAll('[data-component]') для массовой инициализацииform.querySelectorAll('input:invalid') после submitscreen.getByRole, screen.getByText работают по тем же принципамПоскольку код выполняется в изолированном окружении без настоящего DOM, примеры ниже симулируют поведение браузерных методов через JavaScript-объекты. Принципы работы — те же самые, что и в браузере.
Симуляция querySelectorAll через обход дерева mock-объектов
// Симулируем узлы DOM как обычные объекты
function el(tag, attrs = {}, children = []) {
return { tag, attrs, children }
}
// Дерево: ul > [li.item, li.item.active, li.item, div.promo]
const root = el('ul', { id: 'menu' }, [
el('li', { class: 'item' }, [
el('a', { href: '/home', 'data-section': 'home' }),
]),
el('li', { class: 'item active' }, [
el('a', { href: '/catalog', 'data-section': 'catalog' }),
]),
el('li', { class: 'item' }, [
el('a', { href: '/cart', 'data-section': 'cart' }),
]),
el('div', { class: 'promo' }),
])
// Рекурсивный обход — аналог querySelectorAll по тегу
function queryAllByTag(node, tag, results = []) {
if (node.tag === tag) results.push(node)
node.children.forEach(child => queryAllByTag(child, tag, results))
return results
}
// Рекурсивный обход — аналог querySelectorAll по классу
function queryAllByClass(node, cls, results = []) {
const classes = (node.attrs.class || '').split(' ')
if (classes.includes(cls)) results.push(node)
node.children.forEach(child => queryAllByClass(child, cls, results))
return results
}
// querySelector — первый совпавший
function querySelector(node, tag) {
const all = queryAllByTag(node, tag)
return all[0] ?? null
}
const allLinks = queryAllByTag(root, 'a')
console.log('Все ссылки:', allLinks.length) // 3
console.log('href первой:', allLinks[0].attrs.href) // '/home'
const activeItems = queryAllByClass(root, 'active')
console.log('Активных элементов:', activeItems.length) // 1
console.log('Тег активного:', activeItems[0].tag) // 'li'
const firstLink = querySelector(root, 'a')
console.log('Первая ссылка ведёт на:', firstLink?.attrs.href) // '/home'
// Поиск по data-атрибуту
function queryByDataAttr(node, attr, value, results = []) {
if (node.attrs[attr] === value) results.push(node)
node.children.forEach(child => queryByDataAttr(child, attr, value, results))
return results
}
const catalogLink = queryByDataAttr(root, 'data-section', 'catalog')
console.log('Ссылка каталога:', catalogLink[0]?.attrs.href) // '/catalog'В тестовом фреймворке нужна функция findAll(mockDom, selector). Она принимает корневой узел mock-дерева и строку selector в формате "tag" (например "li"), ".class" (например ".active") или "[attr=value]" (например "[data-id=2]"). Функция должна вернуть массив всех узлов, соответствующих селектору.
selector.slice(1) даст имя класса без точки. Для атрибута: selector.slice(1, -1) уберёт скобки, затем split("=") разделит на ключ и значение. Не забудь рекурсию по node.children.