mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-02-22 00:57:26 +00:00
297 lines
7.8 KiB
JavaScript
297 lines
7.8 KiB
JavaScript
/**
|
|
* @jest-environment jsdom
|
|
*/
|
|
|
|
import { Application } from "stimulus";
|
|
import { fireEvent, waitFor } from "@testing-library/dom";
|
|
import tom_select_controller from "controllers/tom_select_controller";
|
|
import showHttpError from "js/services/show_http_error";
|
|
|
|
jest.mock("js/services/show_http_error", () => ({
|
|
__esModule: true,
|
|
default: jest.fn(),
|
|
}));
|
|
|
|
/* ------------------------------------------------------------------
|
|
* Helpers
|
|
* ------------------------------------------------------------------ */
|
|
|
|
const buildResults = (count, start = 1) =>
|
|
Array.from({ length: count }, (_, i) => ({
|
|
value: String(start + i),
|
|
label: `Option ${start + i}`,
|
|
}));
|
|
|
|
const setupDOM = (html) => {
|
|
document.body.innerHTML = html;
|
|
};
|
|
|
|
const getSelect = () => document.getElementById("select");
|
|
const getTomSelect = () => getSelect().tomselect;
|
|
|
|
const openDropdown = () => fireEvent.click(document.getElementById("select-ts-control"));
|
|
|
|
const mockRemoteFetch = (...responses) => {
|
|
responses.forEach((response) => {
|
|
fetch.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => response,
|
|
});
|
|
});
|
|
};
|
|
|
|
const mockDropdownScroll = (
|
|
dropdown,
|
|
{ scrollHeight = 1000, clientHeight = 300, scrollTop = 700 } = {},
|
|
) => {
|
|
Object.defineProperty(dropdown, "scrollHeight", {
|
|
configurable: true,
|
|
value: scrollHeight,
|
|
});
|
|
|
|
Object.defineProperty(dropdown, "clientHeight", {
|
|
configurable: true,
|
|
value: clientHeight,
|
|
});
|
|
|
|
Object.defineProperty(dropdown, "scrollTop", {
|
|
configurable: true,
|
|
writable: true,
|
|
value: scrollTop,
|
|
});
|
|
|
|
fireEvent.scroll(dropdown);
|
|
};
|
|
|
|
/* ------------------------------------------------------------------
|
|
* Expectation helpers
|
|
* ------------------------------------------------------------------ */
|
|
|
|
const expectOptionsCount = (count) => {
|
|
expect(document.querySelectorAll('.ts-dropdown-content [role="option"]').length).toBe(count);
|
|
};
|
|
|
|
const expectDropdownToContain = (text) => {
|
|
expect(document.querySelector(".ts-dropdown-content")?.textContent).toContain(text);
|
|
};
|
|
|
|
const expectDropdownWithNoResults = () => {
|
|
expect(document.querySelector(".ts-dropdown-content")?.textContent).toBe("No results found");
|
|
};
|
|
|
|
/* ------------------------------------------------------------------
|
|
* Specs
|
|
* ------------------------------------------------------------------ */
|
|
|
|
describe("TomSelectController", () => {
|
|
let application;
|
|
|
|
beforeAll(() => {
|
|
application = Application.start();
|
|
application.register("tom-select", tom_select_controller);
|
|
});
|
|
|
|
beforeEach(() => {
|
|
global.fetch = jest.fn();
|
|
global.I18n = { t: (key) => key };
|
|
});
|
|
|
|
afterEach(() => {
|
|
document.body.innerHTML = "";
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe("connect()", () => {
|
|
beforeEach(() => {
|
|
setupDOM(`
|
|
<select id="select" data-controller="tom-select">
|
|
<option value="">Default Option</option>
|
|
<option value="1">Option 1</option>
|
|
<option value="2">Option 2</option>
|
|
</select>
|
|
`);
|
|
});
|
|
|
|
it("initializes TomSelect with default options", () => {
|
|
const settings = getTomSelect().settings;
|
|
|
|
expect(settings.placeholder).toBe("Default Option");
|
|
expect(settings.maxItems).toBe(1);
|
|
expect(settings.plugins).toEqual(["dropdown_input"]);
|
|
expect(settings.allowEmptyOption).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("connect() with custom values", () => {
|
|
beforeEach(() => {
|
|
setupDOM(`
|
|
<select
|
|
id="select"
|
|
data-controller="tom-select"
|
|
data-tom-select-placeholder-value="Choose an option"
|
|
data-tom-select-options-value='{"maxItems": 3, "plugins": ["remove_button"]}'
|
|
>
|
|
<option value="1">Option 1</option>
|
|
<option value="2">Option 2</option>
|
|
</select>
|
|
`);
|
|
});
|
|
|
|
it("applies custom placeholder and options", () => {
|
|
const settings = getTomSelect().settings;
|
|
|
|
expect(settings.placeholder).toBe("Choose an option");
|
|
expect(settings.maxItems).toBe(3);
|
|
expect(settings.plugins).toEqual(["remove_button"]);
|
|
});
|
|
});
|
|
|
|
describe("connect() with remoteUrl", () => {
|
|
beforeEach(() => {
|
|
setupDOM(`
|
|
<select
|
|
id="select"
|
|
data-controller="tom-select"
|
|
data-tom-select-options-value='{"plugins":["virtual_scroll"]}'
|
|
data-tom-select-remote-url-value="https://ofn-tests.com/api/search"
|
|
></select>
|
|
`);
|
|
});
|
|
|
|
it("configures remote loading callbacks", () => {
|
|
const settings = getTomSelect().settings;
|
|
|
|
expect(settings.valueField).toBe("value");
|
|
expect(settings.labelField).toBe("label");
|
|
expect(settings.searchField).toBe("label");
|
|
expect(settings.load).toEqual(expect.any(Function));
|
|
expect(settings.firstUrl).toEqual(expect.any(Function));
|
|
expect(settings.onFocus).toEqual(expect.any(Function));
|
|
});
|
|
|
|
it("fetches page 1 on focus", async () => {
|
|
mockRemoteFetch({
|
|
results: buildResults(1),
|
|
pagination: { more: false },
|
|
});
|
|
|
|
openDropdown();
|
|
|
|
await waitFor(() => expect(fetch).toHaveBeenCalledTimes(1));
|
|
|
|
expect(fetch).toHaveBeenCalledWith(expect.stringContaining("q=&page=1"));
|
|
|
|
await waitFor(() => {
|
|
expectOptionsCount(1);
|
|
expectDropdownToContain("Option 1");
|
|
});
|
|
});
|
|
|
|
it("fetches remote options using search query", async () => {
|
|
const appleOption = { value: "apple", label: "Apple" };
|
|
mockRemoteFetch({
|
|
results: [...buildResults(1), appleOption],
|
|
pagination: { more: false },
|
|
});
|
|
|
|
openDropdown();
|
|
|
|
await waitFor(() => {
|
|
expectOptionsCount(2);
|
|
});
|
|
|
|
mockRemoteFetch({
|
|
results: [appleOption],
|
|
pagination: { more: false },
|
|
});
|
|
|
|
fireEvent.input(document.getElementById("select-ts-control"), {
|
|
target: { value: "apple" },
|
|
});
|
|
|
|
await waitFor(() =>
|
|
expect(fetch).toHaveBeenCalledWith(expect.stringContaining("q=apple&page=1")),
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expectOptionsCount(1);
|
|
expectDropdownToContain("Apple");
|
|
});
|
|
});
|
|
|
|
it("loads next page on scroll (infinite scroll)", async () => {
|
|
mockRemoteFetch(
|
|
{
|
|
results: buildResults(30),
|
|
pagination: { more: true },
|
|
},
|
|
{
|
|
results: buildResults(1, 31),
|
|
pagination: { more: false },
|
|
},
|
|
);
|
|
|
|
openDropdown();
|
|
|
|
await waitFor(() => {
|
|
expectOptionsCount(30);
|
|
});
|
|
|
|
const dropdown = document.querySelector(".ts-dropdown-content");
|
|
mockDropdownScroll(dropdown);
|
|
|
|
await waitFor(() => {
|
|
expectOptionsCount(31);
|
|
});
|
|
|
|
expect(fetch).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("handles fetch errors gracefully", async () => {
|
|
fetch.mockRejectedValueOnce(new Error("Fetch error"));
|
|
|
|
openDropdown();
|
|
|
|
await waitFor(() => {
|
|
expectDropdownWithNoResults();
|
|
});
|
|
|
|
expect(showHttpError).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("displays HTTP error on failure", async () => {
|
|
fetch.mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 500,
|
|
json: async () => ({}),
|
|
});
|
|
|
|
openDropdown();
|
|
|
|
await waitFor(() => {
|
|
expect(showHttpError).toHaveBeenCalledWith(500);
|
|
});
|
|
|
|
expectDropdownWithNoResults();
|
|
});
|
|
|
|
it("controls loading behavior based on user interaction", () => {
|
|
const settings = getTomSelect().settings;
|
|
|
|
// Initial state: openedByClick is false, query is empty
|
|
expect(settings.shouldLoad("")).toBe(false);
|
|
|
|
// Simulating opening the dropdown
|
|
settings.onDropdownOpen();
|
|
expect(settings.shouldLoad("")).toBe(true);
|
|
|
|
// Simulating typing
|
|
settings.onType();
|
|
expect(settings.shouldLoad("")).toBe(false);
|
|
|
|
// Query present
|
|
expect(settings.shouldLoad("a")).toBe(true);
|
|
});
|
|
});
|
|
});
|