c8c2c4a32d
Regression tests for behaviors the audit found correct-but-unpinned, so the suite is a strict superset of Ink: - A07: two <Static> regions both render (the additive divergence) - B04: Static render-prop index = absolute index across appends; container vertical padding adds blank rows to the static frame - B11: lazy raw-mode acquire/release under rawMode:'auto' (the path the 'always' default masks) - B19: child useCursor unmount emits the cursor-hide escape (stream-level) - B20: animation interval 0/negative clamps to 1ms (normalizeInterval unit) and advances without busy-hang - B21/B28: INK_SCREEN_READER env auto-detection + useIsScreenReaderEnabled true-path (env tests isolated in a *.sequential file per the global-state rule) - B29: renderToString serves useCursor/usePaste/useTerminalSize/useAnimation/ useBoxMetrics as inert no-ops (don't throw) - B30: dedicated columnGap/rowGap props + their removal-reset Test-only; no production changes. Codex-reviewed for non-vacuousness, Ink correctness, and process-global isolation. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
137 lines
4.6 KiB
TypeScript
137 lines
4.6 KiB
TypeScript
// Sequential: these tests mutate the process-GLOBAL env var
|
|
// `INK_SCREEN_READER`. The mount path auto-detects it
|
|
// (render.ts:526-527: isScreenReaderEnabled =
|
|
// options.isScreenReaderEnabled ?? process.env["INK_SCREEN_READER"] === "true").
|
|
// Under the repo's file-level parallelism a concurrent sibling in another file
|
|
// could observe the mutated value mid-flight (test.sequential only serializes
|
|
// WITHIN a file), so per the process-global convention they live here. Each test
|
|
// captures the prior value in beforeEach and restores-or-deletes it in afterEach
|
|
// so an ambient INK_SCREEN_READER is never blown away.
|
|
|
|
import { defineComponent, nextTick } from "vue";
|
|
import { afterEach, beforeEach, expect, test } from "vite-plus/test";
|
|
import { Box, Text, createApp, useIsScreenReaderEnabled } from "@vue-tui/runtime";
|
|
import {
|
|
makeFakeStdin,
|
|
makeFakeWritable,
|
|
captureWrites,
|
|
getContentWrites,
|
|
} from "../lifecycle/test-streams.ts";
|
|
|
|
// Whether INK_SCREEN_READER was set in the ambient environment, and to what.
|
|
let hadEnv = false;
|
|
let savedEnv: string | undefined;
|
|
|
|
beforeEach(() => {
|
|
hadEnv = Object.prototype.hasOwnProperty.call(process.env, "INK_SCREEN_READER");
|
|
savedEnv = process.env["INK_SCREEN_READER"];
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Restore the ORIGINAL state: only re-set the value if it was actually
|
|
// present originally; otherwise delete it. Never resurrect/clobber an
|
|
// unset-vs-set distinction so an ambient value survives untouched.
|
|
if (hadEnv) {
|
|
process.env["INK_SCREEN_READER"] = savedEnv;
|
|
} else {
|
|
delete process.env["INK_SCREEN_READER"];
|
|
}
|
|
});
|
|
|
|
test.sequential("INK_SCREEN_READER=true auto-detects SR mode at mount (no explicit option) — linearized output, no border glyphs", async () => {
|
|
process.env["INK_SCREEN_READER"] = "true";
|
|
|
|
const App = defineComponent(() => {
|
|
return () => (
|
|
<Box borderStyle="round">
|
|
<Text>Hello world</Text>
|
|
</Box>
|
|
);
|
|
});
|
|
|
|
const app = createApp(App);
|
|
const stdout = makeFakeWritable({ columns: 80 });
|
|
const stderr = makeFakeWritable({ columns: 80 });
|
|
const { stream: stdin } = makeFakeStdin();
|
|
const writes = captureWrites(stdout);
|
|
|
|
// NO isScreenReaderEnabled option — the env var alone must enable SR mode.
|
|
app.mount({ stdout, stdin, stderr, exitOnCtrlC: false });
|
|
|
|
await nextTick();
|
|
await nextTick();
|
|
|
|
const content = getContentWrites(writes).join("");
|
|
// SR-linearized text is present...
|
|
expect(content).toContain("Hello world");
|
|
// ...and the 2D box-drawing glyphs are NOT (SR mode linearizes the tree;
|
|
// the non-SR path would emit a bordered grid with these glyphs).
|
|
for (const glyph of ["╭", "╮", "╰", "╯", "─", "│"]) {
|
|
expect(content).not.toContain(glyph);
|
|
}
|
|
|
|
app.unmount();
|
|
});
|
|
|
|
test.sequential("useIsScreenReaderEnabled() returns true under INK_SCREEN_READER=true auto-detection", async () => {
|
|
process.env["INK_SCREEN_READER"] = "true";
|
|
|
|
let observed: boolean | undefined;
|
|
const App = defineComponent(() => {
|
|
observed = useIsScreenReaderEnabled();
|
|
return () => <Text>flag</Text>;
|
|
});
|
|
|
|
const app = createApp(App);
|
|
const stdout = makeFakeWritable({ columns: 80 });
|
|
const stderr = makeFakeWritable({ columns: 80 });
|
|
const { stream: stdin } = makeFakeStdin();
|
|
|
|
app.mount({ stdout, stdin, stderr, exitOnCtrlC: false });
|
|
|
|
await nextTick();
|
|
await nextTick();
|
|
|
|
// The composable observed the env-var-derived SR flag as enabled.
|
|
expect(observed).toBe(true);
|
|
|
|
app.unmount();
|
|
});
|
|
|
|
test.sequential("explicit isScreenReaderEnabled:false overrides INK_SCREEN_READER=true (?? only falls back when option is undefined)", async () => {
|
|
// Guards the `??` semantics: the env var is the FALLBACK, not an override.
|
|
// A live app explicitly opting OUT must render the visual (bordered) frame
|
|
// even when INK_SCREEN_READER=true.
|
|
process.env["INK_SCREEN_READER"] = "true";
|
|
|
|
let observed: boolean | undefined;
|
|
const App = defineComponent(() => {
|
|
observed = useIsScreenReaderEnabled();
|
|
return () => (
|
|
<Box borderStyle="round">
|
|
<Text>Visible</Text>
|
|
</Box>
|
|
);
|
|
});
|
|
|
|
const app = createApp(App);
|
|
const stdout = makeFakeWritable({ columns: 80 });
|
|
const stderr = makeFakeWritable({ columns: 80 });
|
|
const { stream: stdin } = makeFakeStdin();
|
|
const writes = captureWrites(stdout);
|
|
|
|
// Explicit false must win over the env var.
|
|
app.mount({ stdout, stdin, stderr, exitOnCtrlC: false, isScreenReaderEnabled: false });
|
|
|
|
await nextTick();
|
|
await nextTick();
|
|
|
|
expect(observed).toBe(false);
|
|
const content = getContentWrites(writes).join("");
|
|
expect(content).toContain("Visible");
|
|
// The visual (non-SR) path DOES emit border glyphs.
|
|
expect(content).toContain("─");
|
|
|
|
app.unmount();
|
|
});
|