UI tree diff tool for Roku SceneGraph. Compares two UiNode trees structurally and reports exactly which nodes were added, removed, changed, or moved — with attribute-level detail.
Built on @danecodes/roku-ecp.
npm install @danecodes/roku-diffimport { diffTrees } from '@danecodes/roku-diff';
import { parseUiXml } from '@danecodes/roku-ecp';
const before = parseUiXml(xmlBefore);
const after = parseUiXml(xmlAfter);
const diff = diffTrees(before, after);
// diff.added, diff.removed, diff.changed, diff.moved, diff.unchangedimport { DiffCapture } from '@danecodes/roku-diff';
import { EcpClient, Key } from '@danecodes/roku-ecp';
const client = new EcpClient('192.168.0.30');
const capture = new DiffCapture(client);
// Automatic: snapshot, run action, wait for stable, snapshot, diff
const diff = await capture.around(async () => {
await client.keypress(Key.Down);
});
// Or manual
const before = await capture.snapshot();
await client.keypress(Key.Down);
const after = await capture.snapshot();
const manualDiff = capture.diff(before, after);// Save to disk
await capture.save('./snapshots/home-screen.json');
// Diff a saved snapshot against the live device
const diff = await capture.diffFrom('./snapshots/home-screen.json');
// Diff two saved snapshots
const diff = await capture.diffFiles('./before.json', './after.json');// Ignore noisy attributes
const diff = diffTrees(before, after, {
ignoreAttrs: ['bounds', 'renderTracking', 'changeCount'],
});
// Only track specific attributes
const diff = diffTrees(before, after, {
watchAttrs: ['focused', 'text', 'visible', 'opacity'],
});
// Scope to a subtree
const diff = diffTrees(before, after, {
scope: 'HomePage > ContentArea',
});import { formatDiff } from '@danecodes/roku-diff';
console.log(formatDiff(diff));
// Tree Diff: 2 added, 1 removed, 3 changed, 0 moved, 42 unchanged
//
// Added:
// + HomePage > ContentArea > NewBanner
// text="Limited Time Offer"
//
// Changed:
// ~ HomePage > NavMenu > AppButton#homeBtn
// focused: "true" → "false"
// Compact (one line per change)
console.log(formatDiff(diff, { compact: true }));
// + ContentArea > NewBanner [text="Limited Time Offer"]
// ~ AppButton#homeBtn focused: "true" → "false"import { expectNoDiff, expectOnlyFocusChanged } from '@danecodes/roku-diff';
// Assert nothing changed
expectNoDiff(diff);
// Assert only focus-related attributes changed
expectOnlyFocusChanged(diff);# Snapshot current UI tree
roku-diff snapshot ./before.json --device 192.168.0.30
# Diff two snapshots
roku-diff diff ./before.json ./after.json
# Diff snapshot against live device
roku-diff diff ./expected.json --live
# Interactive watch mode
roku-diff watch
# Filtering
roku-diff diff ./a.json ./b.json --ignore bounds,renderTracking
roku-diff diff ./a.json ./b.json --watch focused,text,visible
roku-diff diff ./a.json ./b.json --scope "HomePage > ContentArea"
# Output formats
roku-diff diff ./a.json ./b.json --format text # default
roku-diff diff ./a.json ./b.json --format compact
roku-diff diff ./a.json ./b.json --format jsonNodes are matched by identity: same tag name + same name/id attribute (or same position if unnamed). The algorithm walks both trees in O(n), builds identity maps, and compares matched pairs for attribute differences.
MIT