test(editor): cover Tab/Enter completion accept semantics
Some checks failed
Deploy to GitHub Pages / build (push) Failing after 44s
Deploy to GitHub Pages / deploy (push) Has been skipped

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:
Julien Calixte
2026-07-02 09:39:37 +02:00
parent f20935a85b
commit 16b07844a8

View 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 = ")
})
})