Newer
Older
gnexus-ui-kit / tests / unit / utils.spec.js
@Eugene Sukhodolskiy Eugene Sukhodolskiy 1 day ago 3 KB Add Vue adapter behavior contract tests
import { describe, expect, it, vi } from "vitest";
import { cx, eventValue, iconNode, normalizeVariant, trapFocus } from "../../src/vue/utils.js";

describe("cx", () => {
	it("joins strings and skips falsy values", () => {
		expect(cx("a", false, null, undefined, "", "b")).toBe("a b");
	});

	it("flattens arrays", () => {
		expect(cx("a", ["b", "c"])).toBe("a b c");
	});

	it("picks enabled keys from objects", () => {
		expect(cx({ a: true, b: false, c: 1 })).toBe("a c");
	});
});

describe("normalizeVariant", () => {
	it("keeps known variants", () => {
		expect(normalizeVariant("success")).toBe("success");
		expect(normalizeVariant("error")).toBe("error");
	});

	it("falls back on unknown variants", () => {
		expect(normalizeVariant("nope")).toBe("primary");
		expect(normalizeVariant("nope", "info")).toBe("info");
	});
});

describe("iconNode", () => {
	it("renders an <i> with the ph base class", () => {
		const node = iconNode("ph-house");
		expect(node.type).toBe("i");
		expect(node.props.class).toBe("ph ph-house");
		expect(node.props["aria-hidden"]).toBe("true");
	});

	it("adds the ph- prefix when missing and warns in development", () => {
		const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
		const node = iconNode("house");
		expect(node.props.class).toBe("ph ph-house");
		expect(warn).toHaveBeenCalledWith(expect.stringContaining('missing the required "ph-" prefix'));
		warn.mockRestore();
	});

	it("returns null for empty icons", () => {
		expect(iconNode("")).toBeNull();
		expect(iconNode(undefined)).toBeNull();
	});

	it("appends extra classes", () => {
		const node = iconNode("ph-bold ph-spinner", "spin");
		expect(node.props.class).toBe("ph ph-bold ph-spinner spin");
	});
});

describe("eventValue", () => {
	it("reads checked for checkboxes", () => {
		expect(eventValue({ target: { type: "checkbox", checked: true } })).toBe(true);
	});

	it("reads value for other inputs", () => {
		expect(eventValue({ target: { type: "text", value: "abc" } })).toBe("abc");
	});
});

describe("trapFocus", () => {
	const setupRoot = () => {
		const root = document.createElement("div");
		root.innerHTML = '<button id="a">a</button><button id="b">b</button><button id="c">c</button>';
		document.body.appendChild(root);
		root.querySelectorAll("button").forEach(button => {
			// jsdom has no layout, so offsetParent is always null; pretend visible
			Object.defineProperty(button, "offsetParent", { get: () => root });
		});
		return root;
	};

	const tabEvent = (shiftKey = false) => {
		const event = new KeyboardEvent("keydown", { key: "Tab", shiftKey, cancelable: true, bubbles: true });
		document.body.dispatchEvent(event);
		return event;
	};

	it("wraps forward from the last focusable to the first", () => {
		const root = setupRoot();
		const buttons = root.querySelectorAll("button");
		buttons[2].focus();

		const event = tabEvent();
		trapFocus(event, root);

		expect(event.defaultPrevented).toBe(true);
		expect(document.activeElement).toBe(buttons[0]);
		root.remove();
	});

	it("wraps backward from the first focusable to the last", () => {
		const root = setupRoot();
		const buttons = root.querySelectorAll("button");
		buttons[0].focus();

		const event = tabEvent(true);
		trapFocus(event, root);

		expect(event.defaultPrevented).toBe(true);
		expect(document.activeElement).toBe(buttons[2]);
		root.remove();
	});

	it("ignores non-Tab keys", () => {
		const root = setupRoot();
		const event = new KeyboardEvent("keydown", { key: "Escape", cancelable: true });
		trapFocus(event, root);
		expect(event.defaultPrevented).toBe(false);
		root.remove();
	});

	it("refocuses the root when nothing is focusable", () => {
		const root = document.createElement("div");
		root.tabIndex = -1;
		document.body.appendChild(root);
		root.focus();

		const event = tabEvent();
		trapFocus(event, root);

		expect(event.defaultPrevented).toBe(true);
		expect(document.activeElement).toBe(root);
		root.remove();
	});
});