Files
vue-tui/packages/runtime-tests/integration/focus/programmatic-focus.test.tsx
T
Yunfei He 5e54e5742d refactor: replace ref() with shallowRef() in all test files
AGENTS.md requires shallowRef by default. All test state uses
reassignment (not mutation), so shallowRef is correct.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 23:28:06 +08:00

226 lines
6.2 KiB
TypeScript

import { defineComponent, nextTick, shallowRef } from "vue";
import { expect, test } from "vite-plus/test";
import { render } from "@vue-tui/testing";
import { Box, Text, useFocus, useFocusManager } from "@vue-tui/runtime";
test("focus(id) programmatically focuses another component", async () => {
let focusFn!: (id: string) => void;
const Item = defineComponent({
props: { id: { type: String, required: true } },
setup(props) {
const { isFocused, focus } = useFocus({ id: props.id });
if (props.id === "a") focusFn = focus;
return () => (
<Text>
{isFocused.value ? "▶ " : " "}
{props.id}
</Text>
);
},
});
const { lastFrame, stdin } = await render(() => (
<Box flexDirection="column">
<Item id="a" />
<Item id="b" />
<Item id="c" />
</Box>
));
// Need to send a Tab to activate focus system (raw mode)
await stdin.write("\t");
focusFn("c");
// Need to wait for Vue reactivity
const { nextTick } = await import("vue");
await nextTick();
expect(lastFrame()).toContain("▶ c");
expect(lastFrame()).not.toContain("▶ a");
});
test("isActive=false prevents component from receiving focus", async () => {
const active = shallowRef(false);
const Item = defineComponent({
props: { id: { type: String, required: true } },
setup(props) {
const opts = props.id === "skip" ? { id: props.id, isActive: active } : { id: props.id };
const { isFocused } = useFocus(opts);
return () => (
<Text>
{isFocused.value ? "▶ " : " "}
{props.id}
</Text>
);
},
});
const { lastFrame, stdin } = await render(() => (
<Box flexDirection="column">
<Item id="first" />
<Item id="skip" />
<Item id="last" />
</Box>
));
// Tab to first
await stdin.write("\t");
expect(lastFrame()).toContain("▶ first");
// Tab should skip "skip" and go to "last"
await stdin.write("\t");
expect(lastFrame()).toContain("▶ last");
expect(lastFrame()).not.toContain("▶ skip");
});
test("autoFocus + isActive=false does not focus at mount", async () => {
const active = shallowRef(false);
const App = defineComponent(() => {
const { isFocused } = useFocus({ id: "item", autoFocus: true, isActive: active });
return () => <Text>{isFocused.value ? "focused" : "unfocused"}</Text>;
});
const { lastFrame } = await render(App);
expect(lastFrame()).toContain("unfocused");
active.value = true;
await nextTick();
// Becoming active doesn't auto-focus retroactively
expect(lastFrame()).toContain("unfocused");
});
test("flipping isActive to false on focused item blurs it", async () => {
const active = shallowRef(true);
const App = defineComponent(() => {
const { isFocused } = useFocus({ id: "item", autoFocus: true, isActive: active });
return () => <Text>{isFocused.value ? "focused" : "unfocused"}</Text>;
});
const { lastFrame } = await render(App);
expect(lastFrame()).toContain("focused");
active.value = false;
await nextTick();
expect(lastFrame()).toContain("unfocused");
});
// ---------------------------------------------------------------------------
// Ink-ported: programmatic focus, unregister, focusNext/focusPrevious
// ---------------------------------------------------------------------------
const FocusItem = defineComponent({
props: {
label: { type: String, required: true as const },
autoFocus: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
},
setup(props) {
const { isFocused } = useFocus({
autoFocus: props.autoFocus,
isActive: () => !props.disabled,
});
return () => (
<Text>
{props.label}
{isFocused.value ? " ✔" : ""}
</Text>
);
},
});
test("reset focus when focused component unregisters", async () => {
const showFirst = shallowRef(true);
const App = defineComponent(() => {
return () => (
<Box flexDirection="column">
{showFirst.value ? <FocusItem label="First" autoFocus /> : null}
<FocusItem label="Second" autoFocus />
<FocusItem label="Third" autoFocus />
</Box>
);
});
const { lastFrame } = await render(App);
expect(lastFrame()).toMatch(/First ✔/);
showFirst.value = false;
await nextTick();
expect(lastFrame()).not.toMatch(/✔/);
});
test("focus first component after focused component unregisters", async () => {
const showFirst = shallowRef(true);
const App = defineComponent(() => {
return () => (
<Box flexDirection="column">
{showFirst.value ? <FocusItem label="First" autoFocus /> : null}
<FocusItem label="Second" autoFocus />
<FocusItem label="Third" autoFocus />
</Box>
);
});
const { lastFrame, stdin } = await render(App);
expect(lastFrame()).toMatch(/First ✔/);
showFirst.value = false;
await nextTick();
expect(lastFrame()).not.toMatch(/✔/);
await stdin.write("\t");
expect(lastFrame()).toMatch(/Second ✔/);
});
test("manually focus next component via focusNext()", async () => {
let doFocusNext!: () => void;
const App = defineComponent(() => {
const manager = useFocusManager();
doFocusNext = manager.focusNext;
return () => (
<Box flexDirection="column">
<FocusItem label="First" autoFocus />
<FocusItem label="Second" autoFocus />
<FocusItem label="Third" autoFocus />
</Box>
);
});
const { lastFrame } = await render(App);
expect(lastFrame()).toMatch(/First ✔/);
doFocusNext();
await nextTick();
expect(lastFrame()).toMatch(/Second ✔/);
});
test("manually focus previous component via focusPrevious()", async () => {
let doFocusPrevious!: () => void;
const App = defineComponent(() => {
const manager = useFocusManager();
doFocusPrevious = manager.focusPrevious;
return () => (
<Box flexDirection="column">
<FocusItem label="First" autoFocus />
<FocusItem label="Second" autoFocus />
<FocusItem label="Third" autoFocus />
</Box>
);
});
const { lastFrame } = await render(App);
expect(lastFrame()).toMatch(/First ✔/);
doFocusPrevious();
await nextTick();
expect(lastFrame()).toMatch(/Third ✔/);
});