test(editor): cover Tab/Enter completion accept semantics
Mounts PlanEditor under happy-dom and drives the popup: Tab accepts an untouched menu, Enter closes it without accepting, and Enter accepts once the selection has been navigated.
This commit is contained in:
73
src/components/PlanEditor.test.ts
Normal file
73
src/components/PlanEditor.test.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { describe, it, expect, beforeAll } from "vitest"
|
||||
import { mount } from "@vue/test-utils"
|
||||
import PlanEditor from "./PlanEditor.vue"
|
||||
|
||||
// happy-dom ships no canvas 2D context; measure() only needs measureText, so a
|
||||
// tiny stub keeps onMounted from throwing without pulling in a canvas polyfill.
|
||||
beforeAll(() => {
|
||||
HTMLCanvasElement.prototype.getContext = (() => ({
|
||||
measureText: () => ({ width: 80 }),
|
||||
font: "",
|
||||
})) as unknown as HTMLCanvasElement["getContext"]
|
||||
})
|
||||
|
||||
/** Mount with a working v-model so emitted edits flow back into the prop. */
|
||||
function mountEditor(initial = "") {
|
||||
const wrapper = mount(PlanEditor, {
|
||||
props: {
|
||||
modelValue: initial,
|
||||
error: null,
|
||||
"onUpdate:modelValue": (v: string) => wrapper.setProps({ modelValue: v }),
|
||||
},
|
||||
})
|
||||
return wrapper
|
||||
}
|
||||
|
||||
/** Type `text` into the textarea (caret at the end) and let the popup refresh. */
|
||||
async function type(wrapper: ReturnType<typeof mountEditor>, text: string) {
|
||||
const ta = wrapper.find("textarea")
|
||||
const el = ta.element as HTMLTextAreaElement
|
||||
el.value = text
|
||||
el.selectionStart = el.selectionEnd = text.length
|
||||
await ta.trigger("input")
|
||||
}
|
||||
|
||||
const lastEmit = (wrapper: ReturnType<typeof mountEditor>) => {
|
||||
const events = wrapper.emitted("update:modelValue")
|
||||
return events?.at(-1)?.[0] as string | undefined
|
||||
}
|
||||
|
||||
describe("PlanEditor completion keys", () => {
|
||||
it("opens the popup as the author types a key prefix", async () => {
|
||||
const wrapper = mountEditor()
|
||||
await type(wrapper, "t")
|
||||
expect(wrapper.find(".completion").exists()).toBe(true)
|
||||
expect(wrapper.findAll(".completion .label").map((el) => el.text())).toContain("title")
|
||||
})
|
||||
|
||||
it("Tab accepts the selected item without navigating first", async () => {
|
||||
const wrapper = mountEditor()
|
||||
await type(wrapper, "t")
|
||||
await wrapper.find("textarea").trigger("keydown", { key: "Tab" })
|
||||
expect(lastEmit(wrapper)).toBe("title = ")
|
||||
expect(wrapper.find(".completion").exists()).toBe(false)
|
||||
})
|
||||
|
||||
it("Enter does not accept an untouched popup — it closes instead", async () => {
|
||||
const wrapper = mountEditor()
|
||||
await type(wrapper, "t")
|
||||
await wrapper.find("textarea").trigger("keydown", { key: "Enter" })
|
||||
// No accept happened: the last emit is still the raw typed value.
|
||||
expect(lastEmit(wrapper)).toBe("t")
|
||||
expect(wrapper.find(".completion").exists()).toBe(false)
|
||||
})
|
||||
|
||||
it("Enter accepts once the author has navigated the popup", async () => {
|
||||
const wrapper = mountEditor()
|
||||
await type(wrapper, "t")
|
||||
await wrapper.find("textarea").trigger("keydown", { key: "ArrowDown" })
|
||||
await wrapper.find("textarea").trigger("keydown", { key: "Enter" })
|
||||
expect(lastEmit(wrapper)).toBe("title = ")
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user