8c3e97ab47
vue-tui left `display` out of RESETTABLE_PROPS, so a removed/undefined `display` persisted its prior value (a removed display="none" stayed hidden). Adds `display` to RESETTABLE_PROPS -- the setter already maps undefined -> DISPLAY_FLEX -- so a withdrawn `display` returns to the Box default (visible), per render = f(current props), like flexDirection/flexWrap (G19). Deliberate, documented divergence from Ink (which hides on a present-undefined `display` via DISPLAY_NONE, and persists on omitted) -- recorded in ink-divergences.md. The reset is consistent across the visual and screen-reader paths (both read yoga's display state). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
111 lines
4.7 KiB
TypeScript
111 lines
4.7 KiB
TypeScript
import { expect, test } from "vite-plus/test";
|
|
// Internal modules not in package exports — import via relative source path,
|
|
// matching the convention in unit/animation-scheduler.sequential.test.ts.
|
|
// NOTE: yoga-layout is a dependency of @vue-tui/runtime, not of runtime-tests,
|
|
// so it cannot be imported here directly. We reference the stable yoga enum
|
|
// values numerically (EDGE_LEFT=0, EDGE_TOP=1, DIRECTION_LTR=1) and inspect the
|
|
// computed layout via the node's own getComputedMargin/Padding.
|
|
import { applyYogaProp, attachYoga, detachYoga } from "../../runtime/src/host/yoga.ts";
|
|
import { createBox } from "../../runtime/src/host/nodes.ts";
|
|
|
|
// yoga-layout YGEnums (generated/YGEnums.ts) — stable values.
|
|
const EDGE_LEFT = 0;
|
|
const EDGE_TOP = 1;
|
|
const DIRECTION_LTR = 1;
|
|
// Display enum (YGEnums Display): Flex=0 (default/visible), None=1 (hidden).
|
|
const DISPLAY_FLEX = 0;
|
|
const DISPLAY_NONE = 1;
|
|
|
|
// Blocker 2: Vue's HOST renderer passes next=null (not undefined) when a key
|
|
// disappears from a spread props object (e.g. Static spreads `style` into host
|
|
// props). applyYogaProp's reset path must treat null the same as undefined so a
|
|
// removed yoga key resets to its documented default instead of writing NaN/0.
|
|
|
|
function freshBox() {
|
|
const box = createBox();
|
|
attachYoga(box);
|
|
return box;
|
|
}
|
|
|
|
test("null removal of marginTop resets to default (Blocker 2)", () => {
|
|
const box = freshBox();
|
|
applyYogaProp(box, "marginTop", 4, undefined);
|
|
box.yoga.calculateLayout(undefined, undefined, DIRECTION_LTR as never);
|
|
expect(box.yoga.getComputedMargin(EDGE_TOP as never)).toBe(4);
|
|
|
|
// Removal arrives as next=null (key removed from a spread props object).
|
|
applyYogaProp(box, "marginTop", null, 4);
|
|
box.yoga.calculateLayout(undefined, undefined, DIRECTION_LTR as never);
|
|
expect(box.yoga.getComputedMargin(EDGE_TOP as never)).toBe(0);
|
|
|
|
detachYoga(box);
|
|
});
|
|
|
|
test("null removal of paddingLeft resets to default (Blocker 2)", () => {
|
|
const box = freshBox();
|
|
applyYogaProp(box, "paddingLeft", 5, undefined);
|
|
box.yoga.calculateLayout(undefined, undefined, DIRECTION_LTR as never);
|
|
expect(box.yoga.getComputedPadding(EDGE_LEFT as never)).toBe(5);
|
|
|
|
applyYogaProp(box, "paddingLeft", null, 5);
|
|
box.yoga.calculateLayout(undefined, undefined, DIRECTION_LTR as never);
|
|
expect(box.yoga.getComputedPadding(EDGE_LEFT as never)).toBe(0);
|
|
|
|
detachYoga(box);
|
|
});
|
|
|
|
test("raw null does not corrupt a yoga dimension to NaN (Blocker 2)", () => {
|
|
const box = freshBox();
|
|
applyYogaProp(box, "marginTop", 7, undefined);
|
|
// Removal arrives as null; must reset to 0, never NaN.
|
|
applyYogaProp(box, "marginTop", null, 7);
|
|
box.yoga.calculateLayout(undefined, undefined, DIRECTION_LTR as never);
|
|
const m = box.yoga.getComputedMargin(EDGE_TOP as never);
|
|
expect(Number.isNaN(m)).toBe(false);
|
|
expect(m).toBe(0);
|
|
detachYoga(box);
|
|
});
|
|
|
|
// display: removing/undefining `display` resets to the DEFAULT (DISPLAY_FLEX =
|
|
// visible), a DELIBERATE divergence from Ink (which hides on present-undefined).
|
|
// See .agents/docs/ink-divergences.md ("Removing `display` resets to the
|
|
// default"). These pin the yoga-level reset directly via getDisplay().
|
|
|
|
test("removing display=none resets to DISPLAY_FLEX, not stale DISPLAY_NONE (display divergence)", () => {
|
|
const box = freshBox();
|
|
applyYogaProp(box, "display", "none", undefined);
|
|
expect(box.yoga.getDisplay()).toBe(DISPLAY_NONE);
|
|
|
|
// Removal: prev="none", next=undefined → reset to the default (visible).
|
|
applyYogaProp(box, "display", undefined, "none");
|
|
expect(box.yoga.getDisplay()).toBe(DISPLAY_FLEX);
|
|
|
|
detachYoga(box);
|
|
});
|
|
|
|
test("null removal of display=none also resets to DISPLAY_FLEX (spread-props path)", () => {
|
|
const box = freshBox();
|
|
applyYogaProp(box, "display", "none", undefined);
|
|
expect(box.yoga.getDisplay()).toBe(DISPLAY_NONE);
|
|
|
|
// Vue's host renderer passes next=null when a key vanishes from a spread props
|
|
// object; the `value == null` reset path must treat it like undefined.
|
|
applyYogaProp(box, "display", null, "none");
|
|
expect(box.yoga.getDisplay()).toBe(DISPLAY_FLEX);
|
|
|
|
detachYoga(box);
|
|
});
|
|
|
|
test("absent-on-mount display (prev=null/undefined) does not force a reset write (display divergence)", () => {
|
|
// Guard check: on first mount Vue emits patchProp(el, 'display', null/undefined,
|
|
// undefined) for an unset prop. With no prior real value the reset must NOT
|
|
// fire — the node keeps yoga's default (DISPLAY_FLEX) regardless, so this only
|
|
// confirms the absent path is inert and never lands on DISPLAY_NONE.
|
|
const box = freshBox();
|
|
applyYogaProp(box, "display", undefined, undefined);
|
|
expect(box.yoga.getDisplay()).toBe(DISPLAY_FLEX);
|
|
applyYogaProp(box, "display", null, undefined);
|
|
expect(box.yoga.getDisplay()).toBe(DISPLAY_FLEX);
|
|
detachYoga(box);
|
|
});
|