9ed77fc38c
Ink coerces ANY string dimension/position value to a percent before handing it
to yoga (applyDimensionStyles uses parseInt; applyPositionStyles uses parseFloat).
vue forwarded the raw string to native yoga, which only treats %-suffixed strings
as percent — so width="50" rendered as 50 absolute cells instead of 50%, top="2"
as 2 cells instead of 2%, and width="" crashed the render ("Invalid value").
width/height now also fall back to setWidthAuto()/setHeightAuto() on a non-number,
non-string junk value (matching Ink's else branch), so width={false} no longer
throws where Ink renders fine. min/max/position keep forwarding junk to the raw
cell setter, matching Ink (which has no auto fallback there and throws identically).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
220 lines
6.4 KiB
TypeScript
220 lines
6.4 KiB
TypeScript
import { defineComponent, shallowRef, nextTick } from "vue";
|
|
import { expect, test } from "vite-plus/test";
|
|
import { render } from "@vue-tui/testing";
|
|
import { Box, Text } from "@vue-tui/runtime";
|
|
|
|
test("absolute position with top and left offsets", async () => {
|
|
const { lastFrame } = await render(
|
|
defineComponent(() => () => (
|
|
<Box flexDirection="row" width={5} height={3}>
|
|
<Box position="absolute" top={1} left={2}>
|
|
<Text>X</Text>
|
|
</Box>
|
|
</Box>
|
|
)),
|
|
{ columns: 100 },
|
|
);
|
|
expect(lastFrame({ trimLines: true })).toBe("\n X\n");
|
|
});
|
|
|
|
test("absolute position with bottom and right offsets", async () => {
|
|
const { lastFrame } = await render(
|
|
defineComponent(() => () => (
|
|
<Box flexDirection="row" width={6} height={4}>
|
|
<Box position="absolute" bottom={1} right={1}>
|
|
<Text>X</Text>
|
|
</Box>
|
|
</Box>
|
|
)),
|
|
{ columns: 100 },
|
|
);
|
|
expect(lastFrame({ trimLines: true })).toBe("\n\n X\n");
|
|
});
|
|
|
|
test("absolute position with percentage offsets", async () => {
|
|
const { lastFrame } = await render(
|
|
defineComponent(() => () => (
|
|
<Box flexDirection="row" width={6} height={4}>
|
|
<Box position="absolute" top="50%" left="50%">
|
|
<Text>X</Text>
|
|
</Box>
|
|
</Box>
|
|
)),
|
|
{ columns: 100 },
|
|
);
|
|
expect(lastFrame({ trimLines: true })).toBe("\n\n X\n");
|
|
});
|
|
|
|
test("absolute position with percentage bottom and right offsets", async () => {
|
|
const { lastFrame } = await render(
|
|
defineComponent(() => () => (
|
|
<Box flexDirection="row" width={6} height={4}>
|
|
<Box position="absolute" bottom="50%" right="50%">
|
|
<Text>X</Text>
|
|
</Box>
|
|
</Box>
|
|
)),
|
|
{ columns: 100 },
|
|
);
|
|
expect(lastFrame({ trimLines: true })).toBe("\n X\n\n");
|
|
});
|
|
|
|
// Ink coerces a STRING position offset to a PERCENT (styles.ts
|
|
// applyPositionStyles: `typeof value === 'string'` →
|
|
// setPositionPercent(edge, parseFloat(value))). So a bare-numeric string like
|
|
// top="50" is 50% of the container height, NOT 50 absolute cells. This mirrors
|
|
// the "absolute position with percentage offsets" test (top="50%" left="50%")
|
|
// but with bare-numeric strings. Without the fix vue forwards "50" raw to
|
|
// setPosition → 50 absolute cells → pushed off-screen.
|
|
test("absolute position with bare numeric string offsets is a percent (Ink parity)", async () => {
|
|
const { lastFrame } = await render(
|
|
defineComponent(() => () => (
|
|
<Box flexDirection="row" width={6} height={4}>
|
|
<Box position="absolute" top="50" left="50">
|
|
<Text>X</Text>
|
|
</Box>
|
|
</Box>
|
|
)),
|
|
{ columns: 100 },
|
|
);
|
|
expect(lastFrame({ trimLines: true })).toBe("\n\n X\n");
|
|
});
|
|
|
|
test("relative position offsets visual position while keeping flow", async () => {
|
|
const { lastFrame } = await render(
|
|
defineComponent(() => () => (
|
|
<Box flexDirection="row" width={5}>
|
|
<Box position="relative" left={2}>
|
|
<Text>A</Text>
|
|
</Box>
|
|
<Text>B</Text>
|
|
</Box>
|
|
)),
|
|
{ columns: 100 },
|
|
);
|
|
expect(lastFrame({ trimLines: true })).toBe(" BA");
|
|
});
|
|
|
|
test("static position ignores offsets", async () => {
|
|
const { lastFrame } = await render(
|
|
defineComponent(() => () => (
|
|
<Box flexDirection="row" width={5}>
|
|
<Box position="static" left={2}>
|
|
<Text>A</Text>
|
|
</Box>
|
|
<Text>B</Text>
|
|
</Box>
|
|
)),
|
|
{ columns: 100 },
|
|
);
|
|
expect(lastFrame({ trimLines: true })).toBe("AB");
|
|
});
|
|
|
|
test("static position ignores percentage offsets", async () => {
|
|
const { lastFrame } = await render(
|
|
defineComponent(() => () => (
|
|
<Box flexDirection="row" width={5}>
|
|
<Box position="static" left="50%">
|
|
<Text>A</Text>
|
|
</Box>
|
|
<Text>B</Text>
|
|
</Box>
|
|
)),
|
|
{ columns: 100 },
|
|
);
|
|
expect(lastFrame({ trimLines: true })).toBe("AB");
|
|
});
|
|
|
|
test("clears top offset on rerender", async () => {
|
|
const top = shallowRef<number | undefined>(1);
|
|
|
|
const { lastFrame } = await render(
|
|
defineComponent(() => () => (
|
|
<Box flexDirection="row" width={5} height={3}>
|
|
<Box position="absolute" top={top.value} left={2}>
|
|
<Text>X</Text>
|
|
</Box>
|
|
</Box>
|
|
)),
|
|
{ columns: 100 },
|
|
);
|
|
|
|
expect(lastFrame({ trimLines: true })).toBe("\n X\n");
|
|
|
|
top.value = undefined;
|
|
await nextTick();
|
|
expect(lastFrame({ trimLines: true })).toBe(" X\n\n");
|
|
});
|
|
|
|
test("clears percentage top and left offsets on rerender", async () => {
|
|
const top = shallowRef<string | undefined>("50%");
|
|
const left = shallowRef<string | undefined>("50%");
|
|
|
|
const { lastFrame } = await render(
|
|
defineComponent(() => () => (
|
|
<Box flexDirection="row" width={6} height={4}>
|
|
<Box position="absolute" top={top.value} left={left.value}>
|
|
<Text>X</Text>
|
|
</Box>
|
|
</Box>
|
|
)),
|
|
{ columns: 100 },
|
|
);
|
|
|
|
expect(lastFrame({ trimLines: true })).toBe("\n\n X\n");
|
|
|
|
top.value = undefined;
|
|
left.value = undefined;
|
|
await nextTick();
|
|
expect(lastFrame({ trimLines: true })).toBe("X\n\n\n");
|
|
});
|
|
|
|
test("clears percentage top and left offsets when props are omitted on rerender", async () => {
|
|
const showOffsets = shallowRef(true);
|
|
|
|
const { lastFrame } = await render(
|
|
defineComponent(() => () => (
|
|
<Box flexDirection="row" width={6} height={4}>
|
|
<Box
|
|
position="absolute"
|
|
{...(showOffsets.value ? { top: "50%" as const, left: "50%" as const } : {})}
|
|
>
|
|
<Text>X</Text>
|
|
</Box>
|
|
</Box>
|
|
)),
|
|
{ columns: 100 },
|
|
);
|
|
|
|
expect(lastFrame({ trimLines: true })).toBe("\n\n X\n");
|
|
|
|
showOffsets.value = false;
|
|
await nextTick();
|
|
expect(lastFrame({ trimLines: true })).toBe("X\n\n\n");
|
|
});
|
|
|
|
test("clears bottom and right offsets on rerender", async () => {
|
|
const bottom = shallowRef<number | undefined>(1);
|
|
const right = shallowRef<number | undefined>(1);
|
|
|
|
const { lastFrame } = await render(
|
|
defineComponent(() => () => (
|
|
<Box flexDirection="row" width={6} height={4}>
|
|
<Box position="absolute" bottom={bottom.value} right={right.value}>
|
|
<Text>X</Text>
|
|
</Box>
|
|
</Box>
|
|
)),
|
|
{ columns: 100 },
|
|
);
|
|
|
|
expect(lastFrame({ trimLines: true })).toBe("\n\n X\n");
|
|
|
|
bottom.value = undefined;
|
|
right.value = undefined;
|
|
await nextTick();
|
|
expect(lastFrame({ trimLines: true })).toBe("X\n\n\n");
|
|
});
|
|
|
|
// Skipped: absolute position with top and left offsets - concurrent
|