test: compare DOM nodes by identity, never through assert.equal

A failed assert.equal or deepEqual prints both operands, and a happy-dom
node reaches the whole page and window: printing one exhausted memory and
took the machine down. assertSameNode() compares identity and fails with
a short description of each node; every node comparison in the happy-dom
tests uses it.
This commit is contained in:
Sameh Khamis committed 2026-10-30 20:38:29 -07:00
1 parent e949584c0a
commit 3a2acf9b0f
8 files changed
+46 -20

No files matched your search

+19
View File
@@ -0,0 +1,19 @@
import assert from 'node:assert/strict';
const describe = (node) =>
node?.nodeType === 1
? `<${node.tagName.toLowerCase()}${node.id ? `#${node.id}` : ''}>`
: String(node);
/**
* Assert that two DOM nodes (or a node and null) are the same. Never hand
* happy-dom nodes to assert.equal or deepEqual: a failure prints both, and a
* node reaches the whole page and window, which runs the process out of
* memory.
*/
export function assertSameNode(actual, expected, message = '') {
assert.ok(
actual === expected,
`${message ? `${message}: ` : ''}${describe(actual)} is not ${describe(expected)}`,
);
}
+3 -2
View File
@@ -1,5 +1,6 @@
import { after, before, test } from 'node:test';
import assert from 'node:assert/strict';
import { assertSameNode } from '../testSupport/domAssert.mjs';
import { installVueDom } from '../testSupport/vueDom.mjs';
let PanelNavigation;
@@ -132,8 +133,8 @@ test('destroy removes the rails and stops following the stacks', async () => {
const { left, right, calls, ui, navigation } = setup();
navigation.destroy();
navigation.destroy();
assert.equal(left.querySelector('.gev-panel-rail'), null);
assert.equal(right.querySelector('.gev-panel-rail'), null);
assertSameNode(left.querySelector('.gev-panel-rail'), null);
assertSameNode(right.querySelector('.gev-panel-rail'), null);
navigation.select('data-panel');
assert.deepEqual(calls, []);
ui.destroy();
+4 -3
View File
@@ -1,6 +1,7 @@
import '../testSupport/registerDom.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
import { assertSameNode } from '../testSupport/domAssert.mjs';
import { nextTick } from 'vue';
import { createRailCards } from './railCards.js';
import { railDocumentFixture as railFixture } from './railTestFixture.mjs';
@@ -94,7 +95,7 @@ test('footer buttons retain focus and current callbacks; links use safe new tabs
},
]),
]);
assert.equal(f.document.activeElement, button);
assertSameNode(f.document.activeElement, button);
assert.equal(button.parentElement.tagName, 'FOOTER');
assert.equal(
f.find((n) => n.className === 'rail-card-action-hint').textContent,
@@ -200,7 +201,7 @@ test('setting ranges stay below presets and commit expensive filters on release'
assert.equal(slider.value, '53', 'release keeps the handle until the owner');
view.update([model(53)]);
assert.equal(slider.value, '53');
assert.equal(f.document.activeElement, slider);
assertSameNode(f.document.activeElement, slider);
slider.value = '41';
slider.dispatchEvent(new Event('input'));
await nextTick();
@@ -334,7 +335,7 @@ test('labelled rows, lists and results retain focus and dispatch current params'
const chip = f.find((n) => n.dataset.chipId === 'motion');
chip.focus();
view.update([model(true)]);
assert.equal(f.document.activeElement, chip);
assertSameNode(f.document.activeElement, chip);
chip.click();
assert.deepEqual(calls.at(-1), ['a', { paused: false }]);
const storm = f.find(
+3 -2
View File
@@ -1,6 +1,7 @@
import '../testSupport/registerDom.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
import { assertSameNode } from '../testSupport/domAssert.mjs';
import { nextTick } from 'vue';
import { createRailTimeline } from './railTimeline.js';
import { railDocumentFixture as railFixture } from './railTestFixture.mjs';
@@ -46,8 +47,8 @@ test('preview is immediate, commits are coalesced and the drag DOM and tick mapp
index: 1,
readout: 'Refresh',
});
assert.equal(slider.parentElement, row);
assert.equal(row.children[1], slider);
assertSameNode(slider.parentElement, row);
assertSameNode(row.children[1], slider);
assert.equal(slider.value, '0');
assert.equal(slider.max, '2');
assert.equal(
+5 -4
View File
@@ -1,6 +1,7 @@
import '../testSupport/registerDom.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
import { assertSameNode } from '../testSupport/domAssert.mjs';
import { readFileSync } from 'node:fs';
import { createRecentImageryPanel } from './recentImagery.js';
import { createRecentImageryLayer } from '../layers/recentImagery/index.js';
@@ -246,7 +247,7 @@ function part(f, id, name) {
// ---- tests ---------------------------------------------------------------------
test('the readout mounts in the rail body, hides while the layer is off and opens the panel once on first appearance', async () => {
const f = fixture();
assert.equal(f.root.parentNode, f.body);
assertSameNode(f.root.parentNode, f.body);
assert.equal(f.panel.hidden, true);
assert.equal(f.count.textContent, '');
await f.ready();
@@ -864,7 +865,7 @@ test('the divider exists only while a swipe is live, labelled A / B or IMAGE / B
};
assert.equal(swap.tagName, 'BUTTON');
assert.equal(swap.textContent, 'SWAP');
assert.equal(swap.parentNode, f.byId('ri-controls'));
assertSameNode(swap.parentNode, f.byId('ri-controls'));
await f.ready();
await f.settleLease();
assert.equal(f.splits.length, 0, 'one image, no divider');
@@ -993,7 +994,7 @@ test('opacity drives both images; EXPORT downloads the pin or the preview and EX
f.readout.destroy();
assert.equal(await f.readout.exportImage('a'), false);
assert.equal(f.fetches.length, fetched);
assert.equal(container.querySelector('.recent-imagery-readout'), null);
assertSameNode(container.querySelector('.recent-imagery-readout'), null);
});
test('DETAILS is a collapsed rail card holding every note, and the empty-days toggle', async () => {
@@ -1077,7 +1078,7 @@ test('destroy returns every listener, the divider and the panel state', async ()
assert.equal(split.destroyed, true);
assert.equal(f.root.listenerCount(), 0);
assert.equal(f.strip.listenerCount(), 0);
assert.equal(f.root.parentNode, null);
assertSameNode(f.root.parentNode, null);
assert.equal(f.document.getElementById('recent-imagery-panel').hidden, true);
f.layer.setMode('ab');
assert.equal(f.splits.length, 1, 'no renders after destroy');
+5 -4
View File
@@ -1,5 +1,6 @@
import test, { after, before } from 'node:test';
import assert from 'node:assert/strict';
import { assertSameNode } from '../testSupport/domAssert.mjs';
import { createStateChannel } from '../app/stateChannel.js';
import { sceneElements } from './scenePresentation.js';
import { mountSceneSharing } from './sceneSharing.js';
@@ -192,7 +193,7 @@ test('Scene controls dispatch panel and shot-row actions, and rows outlive repub
]);
f.owner.renderShotList();
await nextTick();
assert.equal(f.elements.shots.children[0], row);
assertSameNode(f.elements.shots.children[0], row);
// An owner destroyed after a newer one attached leaves the newer list.
const newer = new SceneControls({
read: () => f.state,
@@ -391,7 +392,7 @@ test('inline shot names save on Enter, cancel on Escape, and reject blank names'
label.dispatchEvent(new Event('dblclick'));
await nextTick();
const input = label.children[0];
assert.equal(document.activeElement, input);
assertSameNode(document.activeElement, input);
input.value = value;
input.dispatchEvent(key(commitKey));
input.dispatchEvent(new Event('blur'));
@@ -482,7 +483,7 @@ test('Scene controls consume current state, preserve rows on progress, and unsub
f.state.progress = 0.5;
channel.publish({ type: 'progress-changed' });
await nextTick();
assert.equal(f.elements.shots.children[0], row);
assertSameNode(f.elements.shots.children[0], row);
f.state.status = 'Project exported';
channel.publish({ type: 'project-exported' });
assert.equal(f.elements.status.textContent, 'Project exported');
@@ -619,7 +620,7 @@ test('The sharing row sits under SAVE & SHARE, after the file row', () => {
const dispose = mountSceneSharing({ edit() {}, share() {} }, panel);
const bar = inner.children[1];
assert.equal(bar.className, 'scene-controls');
assert.equal(inner.children[2], status);
assertSameNode(inner.children[2], status);
assert.ok(bar.children.every((button) => button.title));
dispose();
assert.deepEqual(inner.children, [fileRow, status]);
+3 -2
View File
@@ -1,6 +1,7 @@
import '../testSupport/registerDom.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
import { assertSameNode } from '../testSupport/domAssert.mjs';
import { createStatusBadge, updateStatusBadge } from './statusBadge.js';
import { createRailCards } from './railCards.js';
import { railDocumentFixture as railFixture } from './railTestFixture.mjs';
@@ -51,7 +52,7 @@ test('painting preserves feature state, live regions and focus with zero identic
assert.equal(badge.getAttribute('role'), 'status');
assert.equal(badge.getAttribute('aria-live'), 'polite');
assert.equal(badge.dataset.frameState, 'ready');
assert.equal(document.activeElement, focusOwner);
assertSameNode(document.activeElement, focusOwner);
});
test('coverage badges keep keyed identity, hide empty coverage and never claim feed health', () => {
@@ -74,7 +75,7 @@ test('coverage badges keep keyed identity, hide empty coverage and never claim f
badge,
);
assert.deepEqual({ ...badge.dataset }, {});
assert.equal(f.document.activeElement, header);
assertSameNode(f.document.activeElement, header);
view.destroy();
view.update([card]);
assert.equal(f.container.children.length, 0);
+4 -3
View File
@@ -1,6 +1,7 @@
import '../testSupport/registerDom.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
import { assertSameNode } from '../testSupport/domAssert.mjs';
import { setLocale } from '../i18n/index.js';
import { createWeatherPanel } from './weatherPanel.js';
import { railDocumentFixture as railFixture } from './railTestFixture.mjs';
@@ -115,7 +116,7 @@ test('ordered articles omit Controls and retain history labels, forecast dates a
view.update([radar, wind]);
const windCard = card(f, 'wind');
assert.equal(windCard.tagName, 'ARTICLE');
assert.equal(windCard.parentElement.children[0], windCard);
assertSameNode(windCard.parentElement.children[0], windCard);
assert.equal(
f.find((n) => n.dataset.weatherOpen),
null,
@@ -403,13 +404,13 @@ test('cyclones lead, and observed history owns one bordered group with only acti
['weather-cyclones', 'wind'],
);
const group = f.find((n) => n.className === 'weather-observed-group');
assert.equal(root.children[1], group);
assertSameNode(root.children[1], group);
assert.equal(group.children[0].textContent, 'Observed history');
assert.equal(
group.children[1].textContent,
'Rain radar · Satellite clouds · Lightning density',
);
assert.equal(group.children[2], timelineHost(f));
assertSameNode(group.children[2], timelineHost(f));
assert.deepEqual(
[...group.children[3].children].map((n) => n.dataset.cardId),
['weather-radar', 'weather-satellite', 'weather-lightning'],