Files
HealthLog/src/__tests__/weight-tile-direction-guard.test.ts
T
Marc-André Bombeck 1273a76a63 feat(weight): judge the weight trend against the stored target
The weight tile's arrow, 7-day delta and comparison caption were wired
as "up is bad", so a person below their own target who was gaining saw
every step toward it coloured as a setback. Movement is now judged
against the target they entered: below the band a gain is progress,
above it a loss is, and inside it holding steady is, with a move either
way shown neutral rather than orange. Without a stored target the tile
reads exactly as before; the height-derived band is not treated as a
target because nobody chose it.

The direction is resolved once on the server and published on the
dashboard snapshot as tiles.weightTrend, so the web tile and the native
client colour the same reading the same way. The personal-record
direction reads the same placement helper. The feature payload behind
the briefing, the overview analysis and the Coach now carries the
target band and which way is progress, so a narrative no longer calls a
gain a regression when gaining is the goal. The insights vitals grid
drops weight from its fixed "up is bad" set.

Refs #1006
2026-09-24 09:22:50 +02:00

69 lines
2.4 KiB
TypeScript

/**
* The dashboard weight tile colours its trend by the resolved target
* direction, never by a literal (#1006).
*
* Every other tile on the strip passes a literal `directionSentiment`, because
* for resting pulse or steps the good direction is a property of the metric.
* Weight is the exception: which way is progress depends on the person's own
* target, and it is resolved on the server (`tiles.weightTrend` on the
* snapshot, `src/lib/targets/weight-trend.ts`). A literal `"up-bad"` here is
* exactly the defect the issue reported — a person below their target saw
* every gain coloured as a setback — and it would type-check and render
* without complaint, so this pins the attribute rather than the pixels.
*
* Structural rather than rendered because the dashboard page early-returns a
* loading gate under SSR, and the property is about the source: the weight
* `<TrendCard>` names `weightTrend.direction`, or it does not.
*/
import { readFileSync } from "node:fs";
import { join } from "node:path";
import ts from "typescript";
import { describe, expect, it } from "vitest";
const PAGE = join(process.cwd(), "src/app/page-client.tsx");
function weightTileDirections(): string[] {
const text = readFileSync(PAGE, "utf8");
const file = ts.createSourceFile(
PAGE,
text,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TSX,
);
const found: string[] = [];
const visit = (node: ts.Node) => {
if (
ts.isJsxSelfClosingElement(node) &&
node.tagName.getText(file) === "TrendCard"
) {
const attrs = node.attributes.properties.filter(ts.isJsxAttribute);
const key = attrs.find((a) => a.name.getText(file) === "key");
const isWeight =
key?.initializer &&
ts.isStringLiteral(key.initializer) &&
key.initializer.text === "weight";
if (isWeight) {
const direction = attrs.find(
(a) => a.name.getText(file) === "directionSentiment",
);
found.push(direction?.initializer?.getText(file) ?? "<absent>");
}
}
ts.forEachChild(node, visit);
};
visit(file);
return found;
}
describe("dashboard weight tile direction", () => {
it("finds exactly one weight tile, so an empty match cannot pass", () => {
expect(weightTileDirections()).toHaveLength(1);
});
it("reads the resolved target direction rather than a literal", () => {
expect(weightTileDirections()).toEqual(["{weightTrend.direction}"]);
});
});