tests: TimesheetNameInput remaining tests

This commit is contained in:
Jacobtread 2026-04-11 13:40:39 +12:00
parent e8afd91512
commit c2dce2ed18
2 changed files with 88 additions and 18 deletions

View file

@ -239,6 +239,10 @@ describe("TimesheetNameInput", () => {
expect(component.getValue()).toBe("Test");
});
it("should get an empty string attempting to retrieve the input value before load", () => {
expect(component.getValue()).toBe("");
});
it("should be able to reset the input value", () => {
autocomplete.names.setState(["Test", "Test 1"]);
component.load();
@ -256,6 +260,10 @@ describe("TimesheetNameInput", () => {
expect(inputEl.value).toBe("");
});
it("resetting the input value before loading should do nothing", () => {
component.resetValue();
});
it("down arrow should be able to move the focused suggestion down", () => {
autocomplete.names.setState(["Test", "Test 1"]);
@ -332,6 +340,24 @@ describe("TimesheetNameInput", () => {
expect(onSelectSuggestion).toHaveBeenLastCalledWith("Test");
});
it("enter should do nothing without any suggestions", () => {
autocomplete.names.setState(["Test", "Other"]);
const onKeyDown = vi.spyOn(component, "onKeyDown");
const onSelectSuggestion = vi.spyOn(component, "onSelectSuggestion");
component.load();
const inputEl = containerEl.querySelector(".timekeep-name")! as HTMLInputElement;
expect(inputEl).not.toBeNull();
inputEl.dispatchEvent(
new KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: "Enter" })
);
expect(onKeyDown).toHaveBeenCalledOnce();
expect(onSelectSuggestion).not.toHaveBeenCalled();
});
it("escape should close the suggestions", () => {
autocomplete.names.setState(["Test", "Other"]);
@ -396,6 +422,34 @@ describe("TimesheetNameInput", () => {
expect(setSuggestionsOpen).toHaveBeenLastCalledWith(false);
});
it("clicking inside the input container should not close suggestions", () => {
document.body.appendChild(containerEl);
const component = new TimesheetNameInput(containerEl, autocomplete);
autocomplete.names.setState(["Test", "Other"]);
const onClickOutside = vi.spyOn(component, "onClickOutside");
const setSuggestionsOpen = vi.spyOn(component, "setSuggestionsOpen");
component.load();
const inputEl = containerEl.querySelector(".timekeep-name")! as HTMLInputElement;
expect(inputEl).not.toBeNull();
inputEl.value = "Test";
inputEl.focus();
component.wrapperEl!.dispatchEvent(
new MouseEvent("mousedown", { bubbles: true, cancelable: true })
);
expect(onClickOutside).toHaveBeenCalledOnce();
expect(setSuggestionsOpen).not.toHaveBeenLastCalledWith(false);
document.body.removeChild(containerEl);
});
it("touching outside the input container should close suggestions", () => {
const component = new TimesheetNameInput(containerEl, autocomplete);
@ -434,6 +488,7 @@ describe("TimesheetNameInput", () => {
]);
const onSelectSuggestion = vi.spyOn(component, "onSelectSuggestion");
const onClickSuggestions = vi.spyOn(component, "onClickSuggestions");
component.load();
const inputEl = containerEl.querySelector(".timekeep-name")! as HTMLInputElement;
@ -451,6 +506,7 @@ describe("TimesheetNameInput", () => {
suggestion.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true }));
expect(onSelectSuggestion).toHaveBeenCalledWith("Test");
expect(onClickSuggestions).toHaveBeenCalled();
});
it("clicking events should be ignored when outside a suggestion", () => {

View file

@ -137,8 +137,13 @@ export class TimesheetNameInput extends DomComponent {
*/
onClickSuggestions(event: MouseEvent) {
const target = event.target;
/* v8 ignore start -- @preserve */
if (!(target instanceof HTMLElement)) return;
/* v8 ignore stop -- @preserve */
if (!target.id.startsWith("timekeepSuggestion-")) return;
const suggestion = target;
const value = suggestion.getAttribute("value");
assert(value !== null, "Suggestion value should not be null");
@ -167,13 +172,16 @@ export class TimesheetNameInput extends DomComponent {
*
* @param event The click / touch event
*/
onClickOutside(event: MouseEvent | TouchEvent) {
if (
!(event.target instanceof Node) ||
(this.wrapperEl && !this.wrapperEl.contains(event.target))
) {
this.setSuggestionsOpen(false);
}
assert(
this.wrapperEl,
"Wrapper element must be defined for the onClickOutside event to fire"
);
if (this.wrapperEl.contains(event.target as Node | null)) return;
this.setSuggestionsOpen(false);
}
/**
@ -194,7 +202,8 @@ export class TimesheetNameInput extends DomComponent {
const suggestionsEl = this.#suggestionsEl;
assert(suggestionsEl, "Suggestions element should be defined");
const suggestionsOpen = !(suggestionsEl.hidden ?? false);
const suggestionsHidden = suggestionsEl.hidden;
const suggestionsOpen = !suggestionsHidden;
if (!suggestionsOpen && event.key === "ArrowDown") {
this.setSuggestionsOpen(true);
@ -224,14 +233,18 @@ export class TimesheetNameInput extends DomComponent {
}
case "Enter": {
event.preventDefault();
// Clamp focus within suggestion bounds before making decisions
this.clampSuggestionFocus();
const focusIndex = this.#suggestionFocusIndex;
if (focusIndex >= 0) {
event.preventDefault();
const suggestion = this.#suggestions[focusIndex];
if (suggestion) {
this.onSelectSuggestion(suggestion.item);
}
}
if (focusIndex < 0) return;
const suggestion = this.#suggestions[focusIndex];
assert(suggestion, "Suggestion should always be within defined bounds");
this.onSelectSuggestion(suggestion.item);
break;
}
@ -337,10 +350,11 @@ export class TimesheetNameInput extends DomComponent {
* @param value The suggestion value
*/
onSelectSuggestion(value: string) {
if (this.#inputEl) {
this.#inputEl.value = value;
}
assert(
this.#inputEl,
"Input element should be defined for onSelectSuggestion to be called"
);
this.#inputEl.value = value;
this.setSuggestionsOpen(false);
this.setSuggestionFocus(-1);
}