mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 19:58:50 +00:00
Owner, 2026-08-04, on yesterday's attempt: «с лучами солнца ты сделал фигню —
не надо размывать их боковые грани».
They are right. 22b588e answered "the shafts run into something invisible" with
a Gaussian blur of the WHOLE wedge (`raySoftness`, filter `hp-sunsoft`), which
feathered the sides as well as the tip. A shaft of light through a window has
crisp sides; only its reach fades. The blur turned every wedge into a smudge.
GONE. `raySoftness()`, the `<filter>`/`feGaussianBlur` in <defs>, the `<g
filter clip-path>` wrapper, and with it the `hp-sunclip` clipPath — that clip
existed only so the blur could not bleed through a wall. The polygons come out
of `computeSunRays()` already intersected with the room, so a wall still stops
the light by geometry (demo/smoke_sun.mjs, wedgeClippedToRoom). The sun layer
is plain `<polygon fill="url(#hp-sun-i)">` again.
THE KERB DID NOT COME BACK, and not by luck. The old bright edge floating in
mid-floor was never about softness: the gradient's iso-alpha lines are square
to the SUN, while a parallelogram's far edge is parallel to the WALL. Head-on
they coincide; at any other angle one far corner sits at offset `1 − 0.5/k` —
0.71 of the way at a low sun, 0.11 at a high one — i.e. still lit when the
polygon ends. So `rayQuad()` no longer builds a parallelogram: each side is
extruded until it reaches the same distance `len` ALONG `dir`, which puts the
far edge on one iso-alpha line of the gradient. Combined with the untouched
`RAY_FADE_END` = 85 %, the last 15 % of every wedge is empty and its outline
has nothing left to draw. The sides stay razor-sharp on purpose.
Length (×0.7) and the live sky catch-up are untouched.
Tests: unit — `rayQuad` at six sun angles (sides exactly parallel to the ray,
both far corners at offset 1, far edge ⊥ ray, nothing past the gradient) plus
the head-on parallelogram pinned; the `raySoftness` test is gone with the
function. Smoke — demo/smoke_sun_soft.mjs keeps the reach and the "dead at
85 %" checks and flips the feather assert into its opposite: no filter on any
wedge, no `feGaussianBlur` in the tree, and at an OBLIQUE sun (230°/8° and
225°/55°) no vertex is drawn past the end of the gradient. Verified to fail on
the previous bundle on exactly those four. All 247 unit tests and all 97 smokes
green. Stills: sun_sharp_low / sun_sharp_high (demo/shot_sun_short.mjs now
takes a file prefix).
350 lines
16 KiB
JavaScript
350 lines
16 KiB
JavaScript
import test from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import {
|
||
norm360, planSunAngle, sunDirOnPlan, dayPhase,
|
||
isExteriorWall, windowWallInfo, windowLit,
|
||
rayLength, rayQuad, clipToRoom, computeSunRays,
|
||
rayAlpha, rayColor, cloudFactor, RAY_MAX_ALPHA,
|
||
raysVisible, rayPeakAlpha, RAY_ELEVATION_MIN, RAY_FADE_MS,
|
||
RAY_LENGTH_K, RAY_FADE_END, rayStops,
|
||
SKY_SNAP_DEG, skyNeedsSnap, skyElevation,
|
||
northDegOf, bgModeOf, sunRaysOn, weatherEntityOf, sunStateOf,
|
||
} from '../test-build/sun.js';
|
||
|
||
const near = (a, b, eps = 1e-9) => Math.abs(a - b) < eps;
|
||
|
||
// ---- the test house: two rooms sharing the x=500 wall, windows on all four
|
||
// outer walls plus one on the shared (interior) wall --------------------
|
||
const ROOMS = [
|
||
{ id: 'r1', poly: [[100, 100], [500, 100], [500, 500], [100, 500]] },
|
||
{ id: 'r2', poly: [[500, 100], [800, 100], [800, 500], [500, 500]] },
|
||
];
|
||
const WIN = {
|
||
north: { id: 'wN', x: 300, y: 100, angle: 0, length: 60 },
|
||
south: { id: 'wS', x: 300, y: 500, angle: 0, length: 60 },
|
||
west: { id: 'wW', x: 100, y: 300, angle: 90, length: 60 },
|
||
east: { id: 'wE', x: 800, y: 300, angle: 90, length: 60 },
|
||
inner: { id: 'wI', x: 500, y: 300, angle: 90, length: 60 },
|
||
};
|
||
const ALL = Object.values(WIN);
|
||
|
||
test('planSunAngle: plain subtraction, wraps around the circle (359→0)', () => {
|
||
assert.equal(planSunAngle(180, 0), 180);
|
||
assert.equal(planSunAngle(0, 1), 359);
|
||
assert.equal(planSunAngle(359, 359), 0);
|
||
assert.equal(planSunAngle(10, 350), 20);
|
||
assert.equal(norm360(-90), 270);
|
||
assert.equal(norm360(720), 0);
|
||
});
|
||
|
||
test('sunDirOnPlan: compass points map to canvas vectors (y grows down)', () => {
|
||
const cases = [
|
||
[0, [0, -1]], // north = canvas up
|
||
[90, [1, 0]], // east = right
|
||
[180, [0, 1]], // south = down
|
||
[270, [-1, 0]], // west = left
|
||
];
|
||
for (const [az, [x, y]] of cases) {
|
||
const d = sunDirOnPlan(az, 0);
|
||
assert.ok(near(d[0], x, 1e-12) && near(d[1], y, 1e-12), `az ${az}`);
|
||
}
|
||
// rotating the compass rotates the whole sky: east sun, north_deg=90 → up
|
||
const d = sunDirOnPlan(90, 90);
|
||
assert.ok(near(d[0], 0, 1e-12) && near(d[1], -1, 1e-12));
|
||
});
|
||
|
||
test('dayPhase: night is dark and dim, noon is white, sunrise is warm', () => {
|
||
const night = dayPhase(-20);
|
||
const dawn = dayPhase(2);
|
||
const noon = dayPhase(60);
|
||
assert.equal(night.bg, '#070c14');
|
||
assert.equal(noon.bg, '#ffffff');
|
||
assert.notEqual(dawn.bg, night.bg);
|
||
assert.notEqual(dawn.bg, noon.bg);
|
||
assert.ok(near(night.planDim, 0.1));
|
||
assert.equal(noon.planDim, 0);
|
||
assert.ok(dawn.planDim > 0 && dawn.planDim < 0.1);
|
||
assert.equal(night.warmth, 1);
|
||
assert.equal(noon.warmth, 0);
|
||
assert.ok(near(dawn.warmth, 0.8));
|
||
// garbage elevation never throws and stays inside the palette
|
||
assert.ok(dayPhase(NaN).bg.startsWith('#'));
|
||
});
|
||
|
||
test('windowWallInfo: exterior windows on all four sides get outward normals', () => {
|
||
const n = windowWallInfo(WIN.north, ROOMS);
|
||
const s = windowWallInfo(WIN.south, ROOMS);
|
||
const w = windowWallInfo(WIN.west, ROOMS);
|
||
const e = windowWallInfo(WIN.east, ROOMS);
|
||
assert.deepEqual(n.roomId, 'r1');
|
||
assert.ok(near(n.normal[0], 0, 1e-12) && near(n.normal[1], -1, 1e-12));
|
||
assert.ok(near(s.normal[0], 0, 1e-12) && near(s.normal[1], 1, 1e-12));
|
||
assert.ok(near(w.normal[0], -1, 1e-12) && near(w.normal[1], 0, 1e-12));
|
||
assert.equal(w.roomId, 'r1');
|
||
assert.ok(near(e.normal[0], 1, 1e-12) && near(e.normal[1], 0, 1e-12));
|
||
assert.equal(e.roomId, 'r2');
|
||
});
|
||
|
||
test('windowWallInfo: interior and orphan windows never participate', () => {
|
||
assert.equal(windowWallInfo(WIN.inner, ROOMS), null); // shared wall
|
||
assert.equal(windowWallInfo({ x: 300, y: 300, angle: 0 }, ROOMS), null); // mid-room
|
||
assert.equal(windowWallInfo({ x: 950, y: 950, angle: 0 }, ROOMS), null); // nowhere
|
||
});
|
||
|
||
test('isExteriorWall probes the outer side', () => {
|
||
assert.ok(isExteriorWall([300, 100], [0, -1], ROOMS));
|
||
assert.ok(!isExteriorWall([500, 300], [1, 0], ROOMS)); // r2 is outside r1 here
|
||
});
|
||
|
||
test('windowLit: above the horizon AND facing the sun', () => {
|
||
const east = [1, 0];
|
||
assert.ok(windowLit(east, sunDirOnPlan(90, 0), 10));
|
||
assert.ok(!windowLit(east, sunDirOnPlan(270, 0), 10)); // sun behind the house
|
||
assert.ok(!windowLit(east, sunDirOnPlan(90, 0), 0)); // sunset moment
|
||
assert.ok(!windowLit(east, sunDirOnPlan(90, 0), -5)); // night
|
||
});
|
||
|
||
test('rayLength: 30% shorter than v1.56 (owner 2026-08-04), same shape', () => {
|
||
// the old curve, kept here so the -30% stays a fact and not a memory
|
||
const before = (e) => 0.8 + 1.7 * Math.pow(1 - Math.min(90, Math.max(0, e)) / 90, 1.6);
|
||
assert.equal(RAY_LENGTH_K, 0.7);
|
||
assert.ok(near(rayLength(0), 1.75, 1e-9)); // was 2.5
|
||
assert.ok(near(rayLength(90), 0.56, 1e-9)); // was 0.8
|
||
for (const e of [-5, 0, 3, 10, 30, 45, 60, 89, 90, 120]) {
|
||
assert.ok(near(rayLength(e), before(e) * 0.7, 1e-12), 'exactly 70% at ' + e);
|
||
}
|
||
// the shape survives: a low sun still reaches much further than a high one
|
||
assert.ok(rayLength(10) > rayLength(30));
|
||
assert.ok(rayLength(30) > rayLength(60));
|
||
assert.ok(near(rayLength(-5), 1.75, 1e-9)); // clamped
|
||
});
|
||
|
||
test('rayStops: the shaft is fully dissolved BEFORE its own far edge', () => {
|
||
const stops = rayStops();
|
||
assert.ok(near(stops[0][0], 0) && near(stops[0][1], 1), 'brightest at the glass');
|
||
assert.equal(RAY_FADE_END, 0.85);
|
||
// offsets are sorted, alphas never rise, and the tail is a hard zero
|
||
for (let i = 1; i < stops.length; i++) {
|
||
assert.ok(stops[i][0] > stops[i - 1][0] || stops[i][0] === 1, 'offsets ascend');
|
||
assert.ok(stops[i][1] <= stops[i - 1][1], 'alpha never brightens inward');
|
||
}
|
||
assert.ok(near(stops[stops.length - 1][0], 1), 'the gradient spans the FULL wedge');
|
||
for (const [off, k] of stops) {
|
||
if (off >= RAY_FADE_END) assert.equal(k, 0, 'nothing left at/after ' + RAY_FADE_END);
|
||
else assert.ok(k > 0, 'still lit at ' + off);
|
||
}
|
||
// half gone well before the middle — the eye must not find a straight edge
|
||
const half = stops.find(([, k]) => k <= 0.5);
|
||
assert.ok(half[0] <= 0.65, 'past half-dark by two thirds of the way');
|
||
});
|
||
|
||
test('skyNeedsSnap / skyElevation: glide with the sun, jump when we were away', () => {
|
||
assert.equal(SKY_SNAP_DEG, 3);
|
||
assert.equal(skyNeedsSnap(null, 12), true); // nothing painted yet
|
||
assert.equal(skyNeedsSnap(NaN, 12), true);
|
||
assert.equal(skyNeedsSnap(12, 12), false);
|
||
assert.equal(skyNeedsSnap(12, 13), false); // a real 4-minute sun step
|
||
assert.equal(skyNeedsSnap(12, 14.9), false);
|
||
assert.equal(skyNeedsSnap(12, 15), true); // ~12 minutes unwatched
|
||
assert.equal(skyNeedsSnap(12, 9), true); // and in both directions
|
||
assert.equal(skyElevation(12.3456), 12.3);
|
||
assert.equal(skyElevation(-0.04), -0);
|
||
assert.equal(skyElevation('nonsense'), 0);
|
||
});
|
||
|
||
test('rayQuad: sharp sides, far edge square to the RAY (owner 2026-08-04)', () => {
|
||
// «не надо размывать их боковые грани» — the shaft's sides are hard lines,
|
||
// so the only thing that may dissolve it is the gradient along the ray. That
|
||
// works only if the wedge ends exactly ON an iso-alpha line: the far edge is
|
||
// perpendicular to `dir`, not parallel to the wall.
|
||
const a = [100, 100];
|
||
const b = [100, 200]; // a window along +y
|
||
const len = 300;
|
||
for (const deg of [0, 20, 45, 70, -35, -60]) {
|
||
const rad = (deg * Math.PI) / 180;
|
||
const dir = [Math.cos(rad), Math.sin(rad)]; // oblique sun in most cases
|
||
const q = rayQuad(a, b, dir, len);
|
||
assert.equal(q.length, 4);
|
||
// the near edge is still the window itself
|
||
assert.deepEqual(q[0], [100, 100]);
|
||
assert.deepEqual(q[1], [100, 200]);
|
||
// both sides run exactly along the ray — razor-sharp, never splayed
|
||
for (const [near0, far] of [[q[0], q[3]], [q[1], q[2]]]) {
|
||
const ex = far[0] - near0[0];
|
||
const ey = far[1] - near0[1];
|
||
const cross = ex * dir[1] - ey * dir[0];
|
||
assert.ok(Math.abs(cross) < 1e-9, 'side parallel to the ray at ' + deg);
|
||
assert.ok(ex * dir[0] + ey * dir[1] > 0, 'side runs away from the glass');
|
||
}
|
||
// ...and both far corners sit at the SAME distance along the ray, i.e. on
|
||
// one iso-alpha line of the gradient. This is what kills the bright kerb.
|
||
const mid = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
|
||
const t = (p) => ((p[0] - mid[0]) * dir[0] + (p[1] - mid[1]) * dir[1]) / len;
|
||
assert.ok(near(t(q[2]), 1, 1e-9), 'far corner B at offset 1 at ' + deg);
|
||
assert.ok(near(t(q[3]), 1, 1e-9), 'far corner A at offset 1 at ' + deg);
|
||
// nothing is drawn past the end of the gradient
|
||
for (const p of q) assert.ok(t(p) <= 1 + 1e-9, 'no vertex past the gradient');
|
||
// the far edge really is square to the ray
|
||
const fx = q[2][0] - q[3][0];
|
||
const fy = q[2][1] - q[3][1];
|
||
assert.ok(Math.abs(fx * dir[0] + fy * dir[1]) < 1e-9, 'far edge ⊥ ray at ' + deg);
|
||
}
|
||
// head-on sun: the classic parallelogram, unchanged
|
||
const straight = rayQuad(a, b, [1, 0], len);
|
||
assert.deepEqual(straight, [[100, 100], [100, 200], [400, 200], [400, 100]]);
|
||
});
|
||
|
||
test('rayQuad + clipToRoom: the wedge is cut by the room outline', () => {
|
||
const quad = rayQuad([100, 270], [100, 330], [1, 0], 1000); // way past the wall
|
||
const clipped = clipToRoom(quad, ROOMS[0].poly);
|
||
assert.equal(clipped.length, 1);
|
||
for (const [x, y] of clipped[0]) {
|
||
assert.ok(x >= 100 - 1e-6 && x <= 500 + 1e-6, 'x inside the room');
|
||
assert.ok(y >= 100 - 1e-6 && y <= 500 + 1e-6, 'y inside the room');
|
||
}
|
||
assert.ok(clipped[0].some(([x]) => near(x, 500, 1e-6)), 'reaches the far wall, not past it');
|
||
// a wedge fully outside the room clips to nothing
|
||
assert.equal(clipToRoom(rayQuad([900, 900], [960, 900], [0, 1], 50), ROOMS[0].poly).length, 0);
|
||
});
|
||
|
||
test('computeSunRays: morning east sun lights ONLY the east window', () => {
|
||
const rays = computeSunRays(ROOMS, ALL, 90, 5, 0);
|
||
assert.deepEqual(rays.map((r) => r.openingId), ['wE']);
|
||
assert.equal(rays[0].roomId, 'r2');
|
||
// light travels AWAY from the sun: westward into the room
|
||
assert.ok(near(rays[0].dir[0], -1, 1e-12) && near(rays[0].dir[1], 0, 1e-12));
|
||
for (const [x, y] of rays[0].polys[0]) {
|
||
assert.ok(x >= 500 - 1e-6 && x <= 800 + 1e-6 && y >= 100 - 1e-6 && y <= 500 + 1e-6);
|
||
}
|
||
});
|
||
|
||
test('computeSunRays: noon south sun → south window, short wedge', () => {
|
||
const rays = computeSunRays(ROOMS, ALL, 180, 60, 0);
|
||
assert.deepEqual(rays.map((r) => r.openingId), ['wS']);
|
||
assert.ok(near(rays[0].len, rayLength(60) * 60, 1e-9));
|
||
assert.ok(rays[0].len < computeSunRays(ROOMS, ALL, 90, 5, 0)[0].len);
|
||
});
|
||
|
||
test('computeSunRays: evening west sun → west window', () => {
|
||
const rays = computeSunRays(ROOMS, ALL, 270, 4, 0);
|
||
assert.deepEqual(rays.map((r) => r.openingId), ['wW']);
|
||
});
|
||
|
||
test('computeSunRays: night → nothing at all', () => {
|
||
assert.deepEqual(computeSunRays(ROOMS, ALL, 90, 0, 0), []);
|
||
assert.deepEqual(computeSunRays(ROOMS, ALL, 90, -10, 0), []);
|
||
});
|
||
|
||
test('computeSunRays: rotating the compass swings the light to another window', () => {
|
||
// the same morning east sun, but the plan is rotated 90°: what the canvas
|
||
// shows as "up" is now east → the NORTH-drawn window faces the sun
|
||
const rays = computeSunRays(ROOMS, ALL, 90, 5, 90);
|
||
assert.deepEqual(rays.map((r) => r.openingId), ['wN']);
|
||
// and the interior window still never lights up whatever the compass says
|
||
for (const nd of [0, 45, 90, 180, 270]) {
|
||
for (const az of [0, 90, 180, 270]) {
|
||
assert.ok(!computeSunRays(ROOMS, ALL, az, 5, nd).some((r) => r.openingId === 'wI'));
|
||
}
|
||
}
|
||
});
|
||
|
||
test('rayAlpha: nothing below 3°, full strength above (owner 2026-08-03)', () => {
|
||
// the old gradual ramp-in is gone: it is a threshold, not a fade
|
||
assert.equal(rayAlpha(-3), 0);
|
||
assert.equal(rayAlpha(0), 0);
|
||
assert.equal(rayAlpha(1), 0);
|
||
assert.equal(rayAlpha(2.99), 0);
|
||
assert.ok(near(rayAlpha(3), RAY_MAX_ALPHA)); // exactly at the threshold: on
|
||
assert.ok(near(rayAlpha(3.1), RAY_MAX_ALPHA));
|
||
assert.ok(near(rayAlpha(30), RAY_MAX_ALPHA));
|
||
assert.ok(near(rayAlpha(89), RAY_MAX_ALPHA)); // no elevation shaping at all
|
||
// clouds still scale it, rain still kills it
|
||
assert.ok(near(rayAlpha(30, 0.25), RAY_MAX_ALPHA * 0.25));
|
||
assert.equal(rayAlpha(30, 0), 0);
|
||
});
|
||
|
||
test('raysVisible / rayPeakAlpha: the threshold and the cloud-only ceiling', () => {
|
||
assert.equal(RAY_ELEVATION_MIN, 3);
|
||
assert.equal(RAY_FADE_MS, 2000); // «ровно 2 секунды», mirrored in styles.ts
|
||
assert.equal(raysVisible(2.9), false);
|
||
assert.equal(raysVisible(3), true);
|
||
assert.equal(raysVisible(45), true);
|
||
assert.equal(raysVisible(-10), false);
|
||
// the peak is what the gradient uses while the layer fades — cloud only
|
||
assert.ok(near(rayPeakAlpha(), RAY_MAX_ALPHA));
|
||
assert.ok(near(rayPeakAlpha(1), RAY_MAX_ALPHA));
|
||
assert.ok(near(rayPeakAlpha(0.4), RAY_MAX_ALPHA * 0.4));
|
||
assert.equal(rayPeakAlpha(0), 0);
|
||
});
|
||
|
||
test('RAY_MAX_ALPHA is the brighter 0.3 ceiling (owner 2026-08-03)', () => {
|
||
assert.equal(RAY_MAX_ALPHA, 0.3);
|
||
});
|
||
|
||
test('rayColor: warm at the horizon, neutral by day', () => {
|
||
assert.equal(rayColor(1), '#ff9a45');
|
||
assert.equal(rayColor(0), '#ffe9c2');
|
||
assert.notEqual(rayColor(0.5), rayColor(0));
|
||
});
|
||
|
||
test('cloudFactor: the state map, garbage-safe', () => {
|
||
assert.equal(cloudFactor('sunny'), 1);
|
||
assert.equal(cloudFactor('clear-night'), 1);
|
||
assert.equal(cloudFactor('partlycloudy'), 0.7);
|
||
assert.equal(cloudFactor('cloudy'), 0.4);
|
||
assert.equal(cloudFactor('overcast'), 0.25);
|
||
assert.equal(cloudFactor('fog'), 0.25);
|
||
assert.equal(cloudFactor('rainy'), 0);
|
||
assert.equal(cloudFactor('pouring'), 0);
|
||
assert.equal(cloudFactor('snowy'), 0);
|
||
assert.equal(cloudFactor('lightning-rainy'), 0);
|
||
assert.equal(cloudFactor('unknown'), 1);
|
||
assert.equal(cloudFactor('unavailable'), 1);
|
||
assert.equal(cloudFactor(null), 1);
|
||
assert.equal(cloudFactor(undefined), 1);
|
||
assert.equal(cloudFactor('CLOUDY'), 0.4);
|
||
});
|
||
|
||
test('northDegOf: space override wins, strict int 0–359, null = inert', () => {
|
||
assert.equal(northDegOf({ north_deg: 90 }, {}), 90);
|
||
assert.equal(northDegOf({ north_deg: 90 }, { north_deg: 0 }), 0); // 0 is a value, not "unset"
|
||
assert.equal(northDegOf({}, { north_deg: 359 }), 359);
|
||
assert.equal(northDegOf({}, {}), null);
|
||
assert.equal(northDegOf(null, undefined), null);
|
||
for (const bad of [360, -1, 1.5, '90', true, NaN]) {
|
||
assert.equal(northDegOf({ north_deg: bad }, {}), null, String(bad));
|
||
}
|
||
// a garbage override falls back to the valid global
|
||
assert.equal(northDegOf({ north_deg: 45 }, { north_deg: 999 }), 45);
|
||
});
|
||
|
||
test('bgModeOf: inherit chain with a static fallback', () => {
|
||
assert.equal(bgModeOf({}, {}), 'static');
|
||
assert.equal(bgModeOf({ bg_mode: 'daynight' }, {}), 'daynight');
|
||
assert.equal(bgModeOf({ bg_mode: 'daynight' }, { bg_mode: 'static' }), 'static');
|
||
assert.equal(bgModeOf({}, { bg_mode: 'daynight' }), 'daynight');
|
||
assert.equal(bgModeOf({ bg_mode: 'disco' }, {}), 'static');
|
||
});
|
||
|
||
test('sunRaysOn: default OFF, per-space tri-state inherit', () => {
|
||
assert.equal(sunRaysOn({}, {}), false);
|
||
assert.equal(sunRaysOn({ sun_rays: true }, {}), true);
|
||
assert.equal(sunRaysOn({ sun_rays: true }, { sun_rays: false }), false);
|
||
assert.equal(sunRaysOn({}, { sun_rays: true }), true);
|
||
assert.equal(sunRaysOn({ sun_rays: true }, { sun_rays: null }), true); // null = inherit
|
||
assert.equal(sunRaysOn({ sun_rays: 'yes' }, {}), false);
|
||
});
|
||
|
||
test('weatherEntityOf / sunStateOf: strings and hass shapes, garbage-safe', () => {
|
||
assert.equal(weatherEntityOf({ weather_entity: 'weather.home' }), 'weather.home');
|
||
assert.equal(weatherEntityOf({ weather_entity: ' ' }), null);
|
||
assert.equal(weatherEntityOf({}), null);
|
||
assert.deepEqual(
|
||
sunStateOf({ states: { 'sun.sun': { attributes: { azimuth: 120.5, elevation: -3 } } } }),
|
||
{ azimuth: 120.5, elevation: -3 },
|
||
);
|
||
assert.equal(sunStateOf({ states: {} }), null);
|
||
assert.equal(sunStateOf({ states: { 'sun.sun': { attributes: { azimuth: 'x', elevation: 1 } } } }), null);
|
||
assert.equal(sunStateOf(null), null);
|
||
});
|