Release v1.61.0-beta.7 candidate

This commit is contained in:
Matysh
2026-08-11 02:49:42 +03:00
parent d2bc908280
commit c237baaffd
50 changed files with 4432 additions and 2328 deletions
+5 -1
View File
@@ -1,7 +1,9 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { screenRgb, svgScreenBlendSupported } from '../test-build/glow-blend.js';
import {
resolvedSvgScreenBlend, screenRgb, svgScreenBlendSupported,
} from '../test-build/glow-blend.js';
test('screenRgb implements rounded 8-bit screen blending', () => {
assert.deepEqual(screenRgb([0, 0, 0], [12, 34, 56]), [12, 34, 56]);
@@ -16,5 +18,7 @@ test('capability result is one cached Promise per document', async () => {
const first = svgScreenBlendSupported(document);
const second = svgScreenBlendSupported(document);
assert.equal(first, second);
assert.equal(resolvedSvgScreenBlend(document), undefined);
assert.equal(await first, false);
assert.equal(resolvedSvgScreenBlend(document), false);
});
+49 -3
View File
@@ -14,6 +14,13 @@ test('golden matrix has stable unique ids and bounded comparison thresholds', ()
assert.equal(scenario.viewport.width > 0 && scenario.viewport.height > 0, true, scenario.id);
assert.equal(scenario.threshold.maxChannelDelta >= 0 && scenario.threshold.maxChannelDelta <= 32, true, scenario.id);
assert.equal(scenario.threshold.maxDiffRatio >= 0 && scenario.threshold.maxDiffRatio <= 0.01, true, scenario.id);
if (scenario.warmPixelRegion) {
const region = scenario.warmPixelRegion;
assert.equal(region.x >= 0 && region.y >= 0 && region.w > 0 && region.h > 0, true, scenario.id);
assert.equal(region.x + region.w <= 1 && region.y + region.h <= 1, true, scenario.id);
assert.equal(Number.isInteger(region.minPixels) && region.minPixels > 0, true, scenario.id);
assert.equal(region.minRedBlueDelta > 0, true, scenario.id);
}
}
});
@@ -42,8 +49,10 @@ test('doorway spill golden exposes the opaque-fill failure mode from issue 71',
assert.equal(scenario.extraOpenings?.filter((opening) => opening.type === 'door').length, 1);
assert.equal(scenario.stateOverrides?.['light.golden_light_one']?.state, 'on');
assert.deepEqual(scenario.layoutOverrides?.['golden-light-one'], {
s: 'golden-lighting', x: 0.22, y: 0.48,
s: 'golden-lighting', x: 0.40, y: 0.48,
});
assert.equal(scenario.warmPixelRegion?.minPixels >= 2500, true);
assert.equal(scenario.warmPixelRegion?.x >= 0.5, true);
});
test('golden harness applies doorway, state and layout overrides to a cloned fixture', () => {
@@ -53,12 +62,29 @@ test('golden harness applies doorway, state and layout overrides to a cloned fix
assert.equal(space.openings.filter((opening) => opening.type === 'door').length, 2);
assert.equal(fixture.states['light.golden_light_one'].state, 'on');
assert.deepEqual(fixture.layout['golden-light-one'], {
s: 'golden-lighting', x: 0.22, y: 0.48,
s: 'golden-lighting', x: 0.40, y: 0.48,
});
assert.equal(space.settings.sun_rays, false);
assert.equal(space.settings.custom_fill.a, 1);
});
test('golden overrides fail closed on misspelled fixture references', () => {
const base = {
id: 'invalid', fixture: 'visual', space: 'golden-lighting', mode: 'view',
theme: 'dark', viewport: { width: 1000, height: 900 }, capture: 'stage',
threshold: { maxChannelDelta: 10, maxDiffRatio: 0.001 },
};
assert.throws(() => prepareGoldenFixture({
...base, stateOverrides: { 'light.golden_light_onee': { state: 'on' } },
}), /missing entity/);
assert.throws(() => prepareGoldenFixture({
...base, layoutOverrides: { 'golden-light-onee': { x: 0.4, y: 0.5 } },
}), /missing item/);
assert.throws(() => prepareGoldenFixture({
...base, roomGlow: { 'light-rightt': true },
}), /missing room/);
});
test('a light source paints exactly one region: the floor it can see', () => {
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
// One region per source, and it is the visibility polygon clipped to floor.
@@ -75,11 +101,31 @@ test('a light source paints exactly one region: the floor it can see', () => {
// Exactly one blur, for the whole layer, sized in screen pixels.
assert.equal((glow.match(/<feGaussianBlur/g) || []).length, 1);
assert.doesNotMatch(glow, /<mask /);
assert.match(source, /GLOW_EDGE_FEATHER_PX \/ 2 \/ \(perUnit > 0 \? perUnit : 1\)/);
assert.match(source, /const nextFeather = GLOW_EDGE_FEATHER_PX \/ 2 \/ \(perUnit > 0 \? perUnit : 1\)/);
assert.match(source, /const featherEnabled = !this\._pinchStart && !this\._panStart[\s\S]*_glowFeatherSuspendUntil/);
assert.match(source, /filter=\$\{featherEnabled \? 'url\(#hp-glowfeather\)' : nothing\}/,
'the expensive whole-layer filter must be bypassed during a viewport gesture/transition');
// Barriers are keyed by their own content: `_cfgEpoch` lags behind geometry
// edited in place, and a stale barrier set lights straight through a wall.
assert.doesNotMatch(glow, /_cfgEpoch/);
assert.match(source, /const fingerprint = hash\.toString\(36\)/);
assert.match(source, /for \(const point of body\) \{ mix\(point\[0\]\); mix\(point\[1\]\); \}/);
assert.match(source, /mix\(wall\.b\?\.\[0\] \?\? 0\); mix\(wall\.b\?\.\[1\] \?\? 0\)/);
assert.match(source, /const cacheKey = `\$\{space\.id\}\|\$\{fingerprint\}`/);
assert.match(source, /mix\(this\._cellCm\)[\s\S]*mix\(this\._gridPitch\)/);
});
test('all destructive editor dialogs use the shared responsive footer groups', () => {
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
for (const method of ['_renderOpeningDialog', '_renderPhysicalDialog', '_renderSpaceDialog']) {
const start = source.indexOf(`private ${method}`);
assert.notEqual(start, -1, method);
const end = source.indexOf('\n private ', start + 10);
const body = source.slice(start, end < 0 ? undefined : end);
assert.match(body, /dialog-action-footer/, method);
assert.match(body, /dialog-action-danger/, method);
assert.match(body, /dialog-action-commit/, method);
}
});
test('editor tray golden contract covers every adaptive width in English and Russian', () => {
+14 -3
View File
@@ -66,11 +66,22 @@ test('an occluder outside the radius changes nothing', () => {
}
});
test('an edge through the lamp itself is ignored instead of collapsing the ring', () => {
test('a source on an opaque edge is rejected instead of deleting the wall', () => {
const source = [0, 0];
const ring = visibilityPolygon(source, RAD, [[-50, 0, 50, 0]]);
assert.ok(ring.length >= 12);
assert.ok(reachAt(ring, source, 90) > RAD * 0.98);
assert.deepEqual(ring, []);
});
test('the angular seam at pi does not cut a wedge out of the visible region', () => {
const source = [25, 12];
const room = [[0, 0], [40, 0], [40, 30], [0, 30]];
const wall = [15, 12, 15, 30];
const ring = visibilityPolygon(source, 40, [...polygonSegments(room), wall]);
const area = Math.abs(ring.reduce((sum, point, index) => {
const next = ring[(index + 1) % ring.length];
return sum + point[0] * next[1] - next[0] * point[1];
}, 0)) / 2;
assert.ok(area > 925, `the -x seam must not lose a full angular wedge (${area})`);
});
test('a corner made by two crossing barriers is lit right up to the corner', () => {
+2 -71
View File
@@ -5,9 +5,9 @@ import {
fitView, declump, safeUrl, resolveTapAction, floorsOf, subst, spaceDisplayOf, roomFillColor,
splitRoomPath, polyContainsPoly, islandsOf,
kelvinToRgb, glowColorOf, normalizeGlowColorOverride, liveGlowBrightness,
resolveGlowValues, resolveGlowAppearance, glowAlpha, doorSector, doorSpillRange, hasRoomBehind,
resolveGlowValues, resolveGlowAppearance, glowAlpha,
controlsAction, isControllable,
sharedBoundary, openZoneOf, distToSegment,
sharedBoundary, distToSegment,
outlineWithout,
alignGuides, segmentAngle, is45,
swipeTarget, clampScale,
@@ -902,63 +902,6 @@ test('live Glow brightness and perceptual alpha keep the documented bounds', ()
assert.ok(glowAlpha(0.5, 0.85) < glowAlpha(1, 0.85));
});
test('doorSector: sector through a door, clamped and guarded', () => {
const s = [0, 0];
const sec = doorSector(s, [10, -2], [10, 2], 50);
assert.ok(sec && sec.length === 10 + 0); // вершина + 9 точек дуги
assert.deepEqual(sec[0], [0, 0]);
for (const p of sec.slice(1)) {
const d = Math.hypot(p[0], p[1]);
assert.ok(Math.abs(d - 50) < 1e-6);
assert.ok(p[0] > 0); // сектор смотрит в сторону двери
}
// дверь за радиусом
assert.equal(doorSector(s, [60, -2], [60, 2], 50), null);
// источник на краю двери
assert.equal(doorSector(s, [0, 0], [10, 2], 50), null);
});
test('doorSector: thick doorway is clipped by both tunnel jamb faces', () => {
const src = [0, 3];
const a = [10, -2];
const b = [10, 2];
const thin = doorSector(src, a, b, 50);
const thick = doorSector(src, a, b, 50, 170, 4);
assert.ok(thin && thick);
assert.deepEqual(doorSector(src, a, b, 50, 170, 0), thin);
// The vertical wall occupies x=8..12. Every limiting ray must remain inside
// the clear opening y=-2..2 at BOTH faces, not only at the centreline.
const yAtX = (p, x) => src[1] + (p[1] - src[1]) * ((x - src[0]) / (p[0] - src[0]));
for (const p of [thick[1], thick.at(-1)]) {
for (const x of [8, 12]) assert.ok(Math.abs(yAtX(p, x)) <= 2 + 1e-9);
}
const spreadAtFarFace = (sector) => Math.abs(yAtX(sector[1], 12) - yAtX(sector.at(-1), 12));
assert.ok(spreadAtFarFace(thick) < spreadAtFarFace(thin));
});
test('doorSpillRange: near and distant apertures retain only bounded post-aperture reach', () => {
const near = doorSpillRange(250, 60, 95);
assert.deepEqual(near, { reach: 95, outerRadius: 155 });
assert.ok(near.reach < 250);
const distant = doorSpillRange(250, 230, 95);
assert.deepEqual(distant, { reach: 20, outerRadius: 250 });
assert.ok(distant.reach < 250);
assert.equal(doorSpillRange(250, 250, 95), null);
assert.equal(doorSpillRange(250, 100, 0), null);
});
test('hasRoomBehind: neighbour room yes, street no', () => {
const neighbour = [[10, -5], [20, -5], [20, 5], [10, 5]];
// дверь в стене x=10 (стена вертикальна: угол 90°), источник слева в (5,0)
assert.ok(hasRoomBehind([10, 0], 90, [5, 0], [neighbour], 1));
// за дверью пусто
assert.ok(!hasRoomBehind([10, 0], 90, [5, 0], [], 1));
assert.ok(!hasRoomBehind([10, 0], 90, [5, 0], [[[30, 30], [40, 30], [40, 40], [30, 40]]], 1));
});
test('controlsAction: HA-group semantics', () => {
assert.equal(controlsAction(['off', 'off']), 'turn_on');
assert.equal(controlsAction(['off', 'on']), 'turn_off');
@@ -993,18 +936,6 @@ test('sharedBoundary: exact, partial-collinear and none', () => {
assert.equal(sharedBoundary(A, [[2, 2], [3, 2], [3, 3], [2, 3]]).filter((s) => len(s) > 1e-6).length, 0);
});
test('openZoneOf: transitive, either-direction links', () => {
const rooms = [
{ id: 'a', open_to: ['b'] },
{ id: 'b' }, // связь только со стороны a
{ id: 'c', open_to: ['b'] }, // b↔c со стороны c
{ id: 'd' }, // не связан
];
const z = openZoneOf('a', rooms);
assert.deepEqual([...z].sort(), ['a', 'b', 'c']);
assert.deepEqual([...openZoneOf('d', rooms)], ['d']);
});
test('distToSegment', () => {
assert.equal(distToSegment([0, 5], [0, 0, 0, 10]), 0);
assert.equal(distToSegment([3, 5], [0, 0, 0, 10]), 3);
+18 -20
View File
@@ -2,9 +2,12 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import {
canonicalColumnAngle, columnBody, floorMinusBodies, geometryArea,
directionalOccluders, partitionBody, pointInPhysicalBody, radialOccluders,
directionalOccluders, intersectionPaths, partitionBody, pointInPhysicalBody,
sameColumnPlacement,
} from '../test-build/physical-geometry.js';
import {
polygonSegments, splitAtIntersections, visibilityPolygon,
} from '../test-build/light-visibility.js';
const closeTo = (got, want, tol = 1e-6) =>
assert.ok(Math.abs(got - want) <= tol, `expected ${want}, got ${got}`);
@@ -40,25 +43,20 @@ test('physical bodies are removed from clean floor area', () => {
closeTo(geometryArea(floorMinusBodies(floor, [obstacle])), 3);
});
test('radial occluder extends a wall away from the source', () => {
const body = [[1, -0.1], [1.2, -0.1], [1.2, 0.1], [1, 0.1]];
const shadows = radialOccluders([body], [0, 0], 10);
assert.ok(Math.max(...shadows.flat().map((p) => p[0])) > 9);
});
test('radial shadow covers the far side of a long nearby partition', () => {
const body = partitionBody([1, -3], [1, 3], 10, 5, 0.25);
assert.ok(body);
const shadows = radialOccluders([body], [0, 0], 10);
assert.ok(shadows.some((poly) => pointInPhysicalBody([5, 0], poly)),
'the far side of the wall must remain occluded inside the glow radius');
});
test('a source inside a physical body is fully blocked', () => {
const body = [[-1, -1], [1, -1], [1, 1], [-1, 1]];
const shadows = radialOccluders([body], [0, 0], 10);
for (const point of [[5, 0], [-5, 0], [0, 5], [0, -5]])
assert.ok(shadows.some((poly) => pointInPhysicalBody(point, poly)));
test('intersection failure is fail-dark and never returns the unclipped fan', () => {
const rooms = [
[[100, 70], [160, 70], [160, 120], [100, 120]],
[[10, 150], [60, 150], [60, 170], [10, 170]],
[[240, 20], [250, 20], [250, 80], [240, 80]],
[[180, 150], [230, 150], [230, 220], [180, 220]],
[[180, 110], [240, 110], [240, 170], [180, 170]],
];
// This exact sweep used to make polyclip throw on nudge-generated decimals.
const barriers = splitAtIntersections(rooms.flatMap(polygonSegments));
const fan = visibilityPolygon([170, 380], 285, barriers);
const paths = intersectionPaths([fan], rooms);
assert.deepEqual(paths, [], 'a source outside every room must never return its raw visibility fan');
assert.deepEqual(intersectionPaths([fan], []), [], 'missing floor bounds are dark, not unbounded');
});
test('overlapping physical bodies are subtracted from floor only once', () => {
+32 -7
View File
@@ -163,7 +163,7 @@ test('resign prunes to the referenced set and gives everything a fresh chance',
assert.deepEqual(calls[1].paths, [URL_A]);
});
test('dispose(): a late answer neither renders nor throws, start() revives it', async () => {
test('dispose(): a late answer warms the shared cache without rendering the old card', async () => {
const { hass, calls } = makeHass();
const { s, updates } = signer();
s.display(hass, URL_A);
@@ -174,16 +174,41 @@ test('dispose(): a late answer neither renders nor throws, start() revives it',
assert.equal(updates(), 0, 'no re-render after teardown');
s.start(() => hass, () => new Set([URL_A]));
s.display(hass, URL_A);
assert.equal(s.display(hass, URL_A), URL_A + '?authSig=LATE');
await tick();
assert.equal(calls.length, 2, 'a reconnected card asks again');
calls[1].res({ urls: { [URL_A]: URL_A + '?authSig=NEW' } });
await tick();
assert.equal(updates(), 1);
assert.equal(s.display(hass, URL_A), URL_A + '?authSig=NEW');
assert.equal(calls.length, 1, 'a reconnected card reuses the late shared result');
assert.equal(updates(), 0);
s.dispose();
});
test('two placements dedupe the same authority request and share readiness', async () => {
const { hass, calls } = makeHass();
const first = signer();
const second = signer();
first.s.display(hass, URL_A);
second.s.display(hass, URL_A);
await tick();
assert.equal(calls.length, 1);
calls[0].res({ urls: { [URL_A]: URL_A + '?authSig=SHARED' } });
await tick();
assert.equal(first.s.display(hass, URL_A), URL_A + '?authSig=SHARED');
assert.equal(second.s.display(hass, URL_A), URL_A + '?authSig=SHARED');
assert.equal(first.s.isReady(hass, URL_A), false, 'signed is not yet paintable');
second.s.markLoaded(hass, URL_A);
assert.equal(first.s.isReady(hass, URL_A), true, 'loaded state is authority-scoped');
});
test('prepareImage waits for a protected structural asset before adoption', async () => {
const { hass, calls } = makeHass();
const { s } = signer();
const prepared = s.prepareImage(hass, URL_A);
await tick();
assert.equal(calls.length, 1);
calls[0].res({ urls: { [URL_A]: URL_A + '?authSig=PREPARED' } });
assert.equal(await prepared, true);
assert.equal(s.isReady(hass, URL_A), true);
});
test('R5-1: an empty but successful answer still backs off', async () => {
let t = 1_000_000;
const { hass, calls } = makeHass();
+198
View File
@@ -0,0 +1,198 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
PAINT_BARRIER_MAX_MS,
VisualContinuityController,
contentFingerprint,
visualFrameFingerprint,
} from '../test-build/visual-continuity.js';
class FakeClock {
at = 10_000;
nextId = 1;
timers = new Map();
frames = new Map();
now = () => this.at;
setTimeout = (callback, delay) => {
const id = this.nextId++;
this.timers.set(id, { at: this.at + delay, callback });
return id;
};
clearTimeout = (id) => this.timers.delete(id);
requestAnimationFrame = (callback) => {
const id = this.nextId++;
this.frames.set(id, callback);
return id;
};
cancelAnimationFrame = (id) => this.frames.delete(id);
advance(ms) {
const end = this.at + ms;
while (true) {
const due = [...this.timers.entries()]
.filter(([, timer]) => timer.at <= end)
.sort((left, right) => left[1].at - right[1].at)[0];
if (!due) break;
this.at = due[1].at;
this.timers.delete(due[0]);
due[1].callback();
}
this.at = end;
}
frame() {
this.at += 16;
const callbacks = [...this.frames.values()];
this.frames.clear();
for (const callback of callbacks) callback(this.at);
}
}
async function commit(controller, clock, token, fingerprint = 'next') {
assert.equal(controller.candidateReady(token), true);
const result = controller.commitAfterPaint(token, {
updateComplete: async () => undefined,
stageValid: () => true,
assetsReady: () => true,
frameFingerprint: () => fingerprint,
});
await Promise.resolve();
clock.frame();
await Promise.resolve();
clock.frame();
return result;
}
test('quick return is a strict no-op; long return holds the complete frame', () => {
const clock = new FakeClock();
let updates = 0;
const controller = new VisualContinuityController(() => updates++, clock);
controller.markCompleteFrame('complete');
const baselineUpdates = updates;
const token = controller.visibility({
kind: 'visible', token: 1, at: clock.now(), hiddenFor: 2000, long: false,
});
assert.equal(token, 0);
assert.equal(controller.state, 'steady');
assert.equal(updates, baselineUpdates, 'quick return schedules no render');
controller.visibility({
kind: 'visible', token: 2, at: clock.now(), hiddenFor: 20_000, long: true,
});
assert.equal(controller.state, 'holding');
clock.advance(1000);
assert.equal(controller.overlayVisible, false);
assert.equal(controller.frameFingerprint, 'complete');
});
test('state-only frame identity refreshes without scheduling another render', () => {
const clock = new FakeClock();
let updates = 0;
const controller = new VisualContinuityController(() => updates++, clock);
controller.markCompleteFrame('state-1');
const baselineUpdates = updates;
controller.refreshCompleteFrame('state-2');
assert.equal(controller.frameFingerprint, 'state-2');
assert.equal(updates, baselineUpdates);
});
test('a candidate commits only after two paint opportunities', async () => {
const clock = new FakeClock();
const controller = new VisualContinuityController(() => undefined, clock);
controller.markCompleteFrame('old');
const token = controller.beginCandidate('resume');
const result = commit(controller, clock, token, 'new');
assert.equal(await result, true);
assert.equal(controller.state, 'steady');
assert.equal(controller.frameFingerprint, 'new');
});
test('overlay is delayed, never covers a complete stale frame, and fades after commit', async () => {
const clock = new FakeClock();
const controller = new VisualContinuityController(() => undefined, clock);
const token = controller.beginCandidate('cold-recovery', 'connection');
assert.equal(controller.overlayVisible, false);
clock.advance(149);
assert.equal(controller.overlayVisible, false);
clock.advance(1);
assert.equal(controller.overlayPhase, 'entering');
clock.frame();
assert.equal(controller.overlayPhase, 'fading-in');
clock.advance(150);
assert.equal(controller.overlayPhase, 'opaque');
assert.equal(await commit(controller, clock, token), true);
clock.advance(250);
assert.equal(controller.overlayPhase, 'leaving');
clock.advance(180);
assert.equal(controller.state, 'steady');
assert.equal(controller.overlayVisible, false);
});
test('a candidate that wins during fade-in cancels the overlay without a minimum hold', async () => {
const clock = new FakeClock();
const controller = new VisualContinuityController(() => undefined, clock);
const token = controller.beginCandidate('cold-recovery');
clock.advance(150);
clock.frame();
assert.equal(controller.overlayPhase, 'fading-in');
assert.equal(await commit(controller, clock, token), true);
assert.equal(controller.state, 'steady');
assert.equal(controller.overlayVisible, false);
});
test('a stale recovery token can never commit', async () => {
const clock = new FakeClock();
const controller = new VisualContinuityController(() => undefined, clock);
controller.markCompleteFrame('old');
const stale = controller.beginCandidate('first');
controller.beginCandidate('second');
assert.equal(controller.candidateReady(stale), false);
assert.equal(await controller.commitAfterPaint(stale, {
updateComplete: async () => undefined,
stageValid: () => true,
assetsReady: () => true,
frameFingerprint: () => 'stale',
}), false);
assert.equal(controller.frameFingerprint, 'old');
});
test('a readiness loss rejects recoverably without masquerading as a timeout', async () => {
const clock = new FakeClock();
const controller = new VisualContinuityController(() => undefined, clock);
controller.markCompleteFrame('old');
const token = controller.beginCandidate('asset-refresh', 'asset');
controller.candidateReady(token);
const committed = await controller.commitAfterPaint(token, {
updateComplete: async () => undefined,
stageValid: () => true,
assetsReady: () => false,
frameFingerprint: () => 'bad',
});
assert.equal(committed, false);
assert.equal(controller.state, 'holding');
assert.equal(controller.frameFingerprint, 'old');
assert.equal(controller.trace.at(-1)?.event, 'paint-barrier-rejected');
});
test('a real paint timeout without a complete frame becomes a recoverable error', async () => {
const clock = new FakeClock();
const controller = new VisualContinuityController(() => undefined, clock);
const token = controller.beginCandidate('cold-recovery');
controller.candidateReady(token);
const result = controller.commitAfterPaint(token, {
updateComplete: async () => undefined,
stageValid: () => true,
assetsReady: () => true,
frameFingerprint: () => 'late',
});
await Promise.resolve();
clock.advance(PAINT_BARRIER_MAX_MS);
assert.equal(await result, false);
assert.equal(controller.state, 'recovery-error');
assert.equal(controller.overlayPhase, 'opaque');
});
test('content fingerprints are stable across object key order and frame parts', () => {
assert.equal(contentFingerprint({ b: 2, a: 1 }), contentFingerprint({ a: 1, b: 2 }));
assert.notEqual(contentFingerprint({ a: 1 }), contentFingerprint({ a: 2 }));
assert.equal(visualFrameFingerprint([1, 'x']), visualFrameFingerprint([1, 'x']));
});