fix(directives): handle future dates in $format and warn on $math NaN coercion

$format relative dates now support future timestamps ("2h from now")
and return "just now" for zero diff. $math emits a console.warn in
dev mode when a non-numeric value is silently coerced to 0.
This commit is contained in:
Chris Tate
2026-05-06 21:12:32 -05:00
parent b76997fb4e
commit aafb4fae6f
3 changed files with 58 additions and 7 deletions
+43 -1
View File
@@ -1,4 +1,4 @@
import { describe, it, expect } from "vitest";
import { describe, it, expect, vi } from "vitest";
import {
resolvePropValue,
createDirectiveRegistry,
@@ -87,6 +87,36 @@ describe("$format", () => {
);
expect(result).toBe("3h ago");
});
it("formats a future relative date", () => {
const ctx = makeCtx();
const baseDate = new Date("2024-06-15T12:00:00Z").getTime();
const result = resolvePropValue(
{
$format: "date",
value: "2024-06-15T12:00:00Z",
style: "relative",
now: baseDate - 2 * 60 * 60 * 1000,
},
ctx,
);
expect(result).toBe("2h from now");
});
it("returns 'just now' when date equals now", () => {
const ctx = makeCtx();
const ts = new Date("2024-06-15T12:00:00Z").getTime();
const result = resolvePropValue(
{
$format: "date",
value: "2024-06-15T12:00:00Z",
style: "relative",
now: ts,
},
ctx,
);
expect(result).toBe("just now");
});
});
// ============================================================================
@@ -168,6 +198,18 @@ describe("$math", () => {
const ctx = makeCtx();
expect(resolvePropValue({ $math: "add", b: 3 }, ctx)).toBe(3);
});
it("warns when a non-numeric value is coerced to 0", () => {
const ctx = makeCtx();
const spy = vi.spyOn(console, "warn").mockImplementation(() => {});
const result = resolvePropValue({ $math: "add", a: "foo", b: 3 }, ctx);
expect(result).toBe(3);
expect(spy).toHaveBeenCalledWith(
"$math: non-numeric value coerced to 0:",
"foo",
);
spy.mockRestore();
});
});
// ============================================================================
+8 -5
View File
@@ -25,14 +25,17 @@ export const formatDirective = defineDirective({
if (raw.style === "relative") {
const now = raw.now ?? Date.now();
const diff = now - date.getTime();
const seconds = Math.floor(diff / 1000);
if (diff === 0) return "just now";
const absDiff = Math.abs(diff);
const suffix = diff > 0 ? "ago" : "from now";
const seconds = Math.floor(absDiff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) return `${days}d ago`;
if (hours > 0) return `${hours}h ago`;
if (minutes > 0) return `${minutes}m ago`;
return `${seconds}s ago`;
if (days > 0) return `${days}d ${suffix}`;
if (hours > 0) return `${hours}h ${suffix}`;
if (minutes > 0) return `${minutes}m ${suffix}`;
return `${seconds}s ${suffix}`;
}
return new Intl.DateTimeFormat(
locale,
+7 -1
View File
@@ -4,7 +4,13 @@ import { defineDirective, resolvePropValue } from "@json-render/core";
function toNum(v: unknown): number {
if (v == null) return 0;
const n = Number(v);
return Number.isNaN(n) ? 0 : n;
if (Number.isNaN(n)) {
if (process.env.NODE_ENV !== "production") {
console.warn(`$math: non-numeric value coerced to 0:`, v);
}
return 0;
}
return n;
}
export const mathDirective = defineDirective({