/**
* @jest-environment jsdom
*/
import { Application } from "stimulus";
import tom_select_controller from "../../../app/webpacker/controllers/tom_select_controller.js";
describe("TomSelectController", () => {
let application;
beforeAll(() => {
application = Application.start();
application.register("tom-select", tom_select_controller);
});
beforeEach(() => {
global.requestAnimationFrame = jest.fn((cb) => {
cb();
return 1;
});
// Mock fetch for remote data tests
global.fetch = jest.fn();
});
afterEach(() => {
document.body.innerHTML = "";
jest.clearAllMocks();
});
describe("basic initialization", () => {
it("initializes TomSelect with default options and settings", async () => {
document.body.innerHTML = `
`;
await new Promise((resolve) => setTimeout(resolve, 0));
const select = document.getElementById("test-select");
const controller = application.getControllerForElementAndIdentifier(select, "tom-select");
expect(controller).not.toBeNull();
expect(controller.control).toBeDefined();
expect(controller.control.settings.maxItems).toBe(1);
expect(controller.control.settings.maxOptions).toBeNull();
expect(controller.control.settings.allowEmptyOption).toBe(true);
expect(controller.control.settings.placeholder).toBe("Choose an option");
expect(controller.control.settings.plugins).toContain("dropdown_input");
expect(controller.control.settings.onItemAdd).toBeDefined();
});
it("uses empty option text as placeholder when no placeholder value provided", async () => {
document.body.innerHTML = `
`;
await new Promise((resolve) => setTimeout(resolve, 0));
const select = document.querySelector("select");
const controller = application.getControllerForElementAndIdentifier(select, "tom-select");
expect(controller).not.toBeNull();
expect(controller.control.settings.placeholder).toBe("-- Default Placeholder --");
});
it("accepts custom options via data attribute", async () => {
document.body.innerHTML = `
`;
await new Promise((resolve) => setTimeout(resolve, 0));
const select = document.querySelector("select");
const controller = application.getControllerForElementAndIdentifier(select, "tom-select");
expect(controller).not.toBeNull();
expect(controller.control.settings.maxItems).toBe(3);
expect(controller.control.settings.create).toBe(true);
});
it("cleans up on disconnect", async () => {
document.body.innerHTML = `
`;
await new Promise((resolve) => setTimeout(resolve, 0));
const select = document.querySelector("select");
const controller = application.getControllerForElementAndIdentifier(select, "tom-select");
expect(controller).not.toBeNull();
const destroySpy = jest.spyOn(controller.control, "destroy");
controller.disconnect();
expect(destroySpy).toHaveBeenCalled();
});
});
describe("remote data loading (#addRemoteOptions)", () => {
it("configures remote loading with proper field and pagination setup", async () => {
document.body.innerHTML = `
`;
await new Promise((resolve) => setTimeout(resolve, 0));
const select = document.querySelector("select");
const controller = application.getControllerForElementAndIdentifier(select, "tom-select");
expect(controller).not.toBeNull();
expect(controller.page).toBe(1);
expect(controller.hasMore).toBe(true);
expect(controller.loading).toBe(false);
expect(controller.scrollAttached).toBe(false);
expect(controller.control.settings.valueField).toBe("value");
expect(controller.control.settings.labelField).toBe("label");
expect(controller.control.settings.searchField).toBe("label");
});
it("resets pagination on new query but preserves on same query", async () => {
document.body.innerHTML = `
`;
await new Promise((resolve) => setTimeout(resolve, 0));
const select = document.querySelector("select");
const controller = application.getControllerForElementAndIdentifier(select, "tom-select");
expect(controller).not.toBeNull();
controller.page = 5;
controller.hasMore = false;
controller.lastQuery = "old";
// Same query preserves state
controller.control.settings.onType("old");
expect(controller.page).toBe(5);
// New query resets state
controller.control.settings.onType("new");
expect(controller.page).toBe(1);
expect(controller.hasMore).toBe(true);
expect(controller.lastQuery).toBe("new");
});
it("loads initial data on focus when not loading", async () => {
global.fetch.mockResolvedValue({
json: jest.fn().mockResolvedValue({
results: [],
pagination: { more: false },
}),
});
document.body.innerHTML = `
`;
await new Promise((resolve) => setTimeout(resolve, 0));
const select = document.querySelector("select");
const controller = application.getControllerForElementAndIdentifier(select, "tom-select");
expect(controller).not.toBeNull();
const loadSpy = jest.spyOn(controller.control, "load");
controller.control.settings.onFocus();
expect(loadSpy).toHaveBeenCalledWith("", expect.any(Function));
expect(controller.lastQuery).toBe("");
// Does not load when already loading
controller.loading = true;
loadSpy.mockClear();
controller.control.settings.onFocus();
expect(loadSpy).not.toHaveBeenCalled();
});
it("attaches scroll listener once on dropdown open", async () => {
document.body.innerHTML = `
`;
await new Promise((resolve) => setTimeout(resolve, 0));
const select = document.querySelector("select");
const controller = application.getControllerForElementAndIdentifier(select, "tom-select");
expect(controller).not.toBeNull();
expect(controller.scrollAttached).toBe(false);
const addEventListenerSpy = jest.spyOn(
controller.control.dropdown_content,
"addEventListener",
);
controller.control.settings.onDropdownOpen();
expect(controller.scrollAttached).toBe(true);
expect(addEventListenerSpy).toHaveBeenCalledWith("scroll", expect.any(Function));
// Does not attach multiple times
controller.control.settings.onDropdownOpen();
expect(addEventListenerSpy).toHaveBeenCalledTimes(1);
});
});
describe("infinite scroll (#fetchNextPage)", () => {
it("initializes pagination infrastructure", async () => {
document.body.innerHTML = `
`;
await new Promise((resolve) => setTimeout(resolve, 0));
const select = document.querySelector("select");
const controller = application.getControllerForElementAndIdentifier(select, "tom-select");
expect(controller).not.toBeNull();
expect(controller._fetchOptions).toBeDefined();
expect(typeof controller._fetchOptions).toBe("function");
expect(controller.lastQuery).toBe("");
expect(controller.control.settings.onDropdownOpen).toBeDefined();
});
it("manages pagination state correctly", async () => {
document.body.innerHTML = `
`;
await new Promise((resolve) => setTimeout(resolve, 0));
const select = document.querySelector("select");
const controller = application.getControllerForElementAndIdentifier(select, "tom-select");
expect(controller).not.toBeNull();
// Initial state
expect(controller.page).toBe(1);
expect(controller.hasMore).toBe(true);
expect(controller.loading).toBe(false);
// Can increment page
controller.page += 1;
expect(controller.page).toBe(2);
// Can update hasMore flag
controller.hasMore = false;
expect(controller.hasMore).toBe(false);
// Can track loading
controller.loading = true;
expect(controller.loading).toBe(true);
});
it("provides dropdown element access for scroll detection", async () => {
document.body.innerHTML = `
`;
await new Promise((resolve) => setTimeout(resolve, 0));
const select = document.querySelector("select");
const controller = application.getControllerForElementAndIdentifier(select, "tom-select");
expect(controller).not.toBeNull();
expect(controller.control.dropdown_content).toBeDefined();
const dropdown = controller.control.dropdown_content;
expect(typeof dropdown.scrollTop).toBe("number");
expect(typeof dropdown.clientHeight).toBe("number");
expect(typeof dropdown.scrollHeight).toBe("number");
});
});
});