UNPKG

vueless

Version:

Vue Styleless UI Component Library, powered by Tailwind CSS.

495 lines (397 loc) • 13 kB
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { computed, nextTick } from "vue"; import { mount } from "@vue/test-utils"; // TODO: Autogenerated, need to be reviewed // Mock the ui utils vi.mock("../../utils/ui.ts", () => ({ cx: vi.fn((classes) => (Array.isArray(classes) ? classes.filter(Boolean).join(" ") : classes)), cva: vi.fn((config) => { // Return a spy function that can be called and tracked const cvaSpy = vi.fn((props) => { if (!config.variants) return config.base || ""; let classes = config.base || ""; // Apply variants Object.entries(config.variants).forEach(([key, variants]) => { const value = props[key]; if (value && variants[value]) { classes += ` ${variants[value]}`; } }); return classes.trim(); }); return cvaSpy; }), setColor: vi.fn((classes, color) => classes?.replace(/{color}/g, color)), vuelessConfig: { components: {}, unstyled: false }, getMergedConfig: vi.fn((args) => { // Create a spy function that returns the expected merged config const { defaultConfig, globalConfig, propsConfig } = args; return { ...defaultConfig, ...globalConfig, ...propsConfig, }; }), })); // Mock Vue functions vi.mock("vue", async () => { const actual = await vi.importActual("vue"); return { ...actual, getCurrentInstance: vi.fn(), useAttrs: vi.fn(), }; }); import useUI from "../useUI.ts"; import * as uiUtils from "../../utils/ui.ts"; import { getCurrentInstance, useAttrs } from "vue"; // Test component for integration testing const TestComponent = { template: '<div :data-test="getDataTest()" v-bind="bodyAttrs">Test</div>', setup() { const defaultConfig = { body: { base: "base-class", variants: { variant: { primary: "primary-class", secondary: "secondary-class", }, size: { sm: "small-class", md: "medium-class", }, }, }, defaults: { variant: "primary", size: "md", }, }; return useUI(defaultConfig); }, }; describe("useUI", () => { beforeEach(() => { vi.clearAllMocks(); // Reset vuelessConfig uiUtils.vuelessConfig.components = {}; uiUtils.vuelessConfig.unstyled = false; // Setup default mocks vi.mocked(getCurrentInstance).mockReturnValue({ type: { __name: "TestComponent" }, props: { dataTest: "test", color: "primary", config: {} }, parent: null, }); vi.mocked(useAttrs).mockReturnValue({ class: "", style: "", }); }); afterEach(() => { vi.restoreAllMocks(); }); describe("Basic Functionality", () => { it("should return config, getDataTest, and attribute objects", () => { const defaultConfig = { body: { base: "test-class" }, defaults: { variant: "primary" }, }; const result = useUI(defaultConfig); expect(result).toHaveProperty("config"); expect(result).toHaveProperty("getDataTest"); expect(result).toHaveProperty("bodyAttrs"); expect(typeof result.getDataTest).toBe("function"); }); it("should merge default config with global and props config", async () => { const defaultConfig = { body: { base: "default-class" }, defaults: { variant: "primary" }, }; const globalConfig = { body: { base: "global-class" }, }; const propsConfig = { body: { base: "props-class" }, }; // Set global config uiUtils.vuelessConfig.components = { TestComponent: globalConfig, }; // Mock getCurrentInstance to return props config vi.mocked(getCurrentInstance).mockReturnValue({ type: { __name: "TestComponent" }, props: { config: propsConfig, dataTest: "test" }, parent: null, }); // Call useUI to trigger the config merging useUI(defaultConfig); expect(uiUtils.getMergedConfig).toHaveBeenCalledWith({ defaultConfig, globalConfig, propsConfig, unstyled: false, }); }); }); describe("CVA Integration", () => { it("should generate classes using CVA when config has variants", () => { const defaultConfig = { body: { base: "base-class", variants: { variant: { primary: "primary-class", secondary: "secondary-class", }, }, }, }; // Call useUI to trigger CVA usage useUI(defaultConfig); expect(uiUtils.cva).toHaveBeenCalledWith(defaultConfig.body); }); it("should handle string-based config values", () => { const defaultConfig = { body: "simple-string-class", }; const result = useUI(defaultConfig); const bodyAttrs = result.bodyAttrs; expect(bodyAttrs.value.class).toContain("simple-string-class"); }); }); describe("Color Handling", () => { it("should replace {color} placeholders in classes", () => { // Mock getCurrentInstance to return color prop vi.mocked(getCurrentInstance).mockReturnValue({ type: { __name: "TestComponent" }, props: { color: "blue", dataTest: "test" }, parent: null, }); // Test the setColor function directly expect(uiUtils.setColor).toBeDefined(); // The setColor function should replace {color} placeholders const testClasses = "text-{color} bg-{color}"; const coloredClasses = uiUtils.setColor(testClasses, "blue"); expect(coloredClasses).toBe("text-blue bg-blue"); // Test that useUI can be called with color-containing config const defaultConfig = { wrapper: { base: "text-{color} bg-{color}", }, }; const result = useUI(defaultConfig); expect(result).toHaveProperty("config"); expect(result).toHaveProperty("getDataTest"); }); }); describe("getDataTest Function", () => { it("should return data-test value when dataTest prop is provided", () => { const result = useUI({}); const dataTest = result.getDataTest(); expect(dataTest).toBe("test"); }); it("should return data-test with suffix when suffix is provided", () => { const result = useUI({}); const dataTest = result.getDataTest("button"); expect(dataTest).toBe("test-button"); }); it("should return null when dataTest prop is not provided", () => { // Mock getCurrentInstance to return no dataTest vi.mocked(getCurrentInstance).mockReturnValue({ type: { __name: "TestComponent" }, props: {}, parent: null, }); const result = useUI({}); const dataTest = result.getDataTest(); expect(dataTest).toBeNull(); }); }); describe("Nested Component Handling", () => { it("should handle nested component references like {UIcon}", () => { const defaultConfig = { icon: "{UIcon}", button: "btn {UIcon} end", }; const result = useUI(defaultConfig); // The nested component pattern should be processed expect(result).toHaveProperty("iconAttrs"); expect(result).toHaveProperty("buttonAttrs"); }); }); describe("Reactivity", () => { it("should update config when props.config changes", async () => { const component = mount(TestComponent); // Initial state expect(component.vm.config).toBeDefined(); // Change props await component.setProps({ config: { body: { base: "new-class" }, }, }); await nextTick(); // Config should be updated expect(uiUtils.getMergedConfig).toHaveBeenCalled(); }); }); describe("Extends Pattern Handling", () => { it("should handle extends pattern {>key} syntax", () => { const defaultConfig = { button: "base-class {>icon}", icon: "icon-class", }; const result = useUI(defaultConfig); expect(result).toHaveProperty("buttonAttrs"); expect(result).toHaveProperty("iconAttrs"); }); }); describe("Mutated Props", () => { it("should use mutated props for class generation", () => { const defaultConfig = { body: { base: "base-class", variants: { hasIcon: { true: "with-icon", false: "without-icon", }, }, }, }; const mutatedProps = computed(() => ({ hasIcon: true, })); const result = useUI(defaultConfig, mutatedProps); expect(result).toHaveProperty("bodyAttrs"); }); }); describe("Component Name Detection", () => { it("should detect component name from type.__name", () => { vi.mocked(getCurrentInstance).mockReturnValue({ type: { __name: "UButton" }, props: { dataTest: "test" }, parent: null, }); const result = useUI({}); expect(result).toBeDefined(); }); it("should detect component name from parent when internal component", () => { vi.mocked(getCurrentInstance).mockReturnValue({ type: { internal: true }, props: { dataTest: "test" }, parent: { type: { __name: "UButton" }, }, }); const result = useUI({}); expect(result).toBeDefined(); }); }); describe("Attribute Generation", () => { it("should generate proper attributes for each config key", () => { const defaultConfig = { wrapper: { base: "wrapper-class" }, content: { base: "content-class" }, footer: "footer-class", }; const result = useUI(defaultConfig); expect(result).toHaveProperty("wrapperAttrs"); expect(result).toHaveProperty("contentAttrs"); expect(result).toHaveProperty("footerAttrs"); }); it("should include config in attributes for nested components", () => { const defaultConfig = { icon: { base: "{UIcon}", defaults: { size: "sm", }, }, }; const result = useUI(defaultConfig); const iconAttrs = result.iconAttrs; expect(iconAttrs.value).toHaveProperty("config"); }); }); describe("Edge Cases", () => { it("should handle empty config", () => { const result = useUI({}); expect(result.config).toBeDefined(); expect(result.getDataTest).toBeDefined(); }); it("should handle null/undefined config values", () => { const defaultConfig = { body: "", icon: undefined, }; const result = useUI(defaultConfig); expect(result).toHaveProperty("bodyAttrs"); expect(result).toHaveProperty("iconAttrs"); // Should not throw errors when accessing attributes expect(() => result.bodyAttrs.value).not.toThrow(); expect(() => result.iconAttrs.value).not.toThrow(); }); it("should handle unstyled mode", () => { uiUtils.vuelessConfig.unstyled = true; const defaultConfig = { body: { base: "styled-class" }, }; useUI(defaultConfig); expect(uiUtils.getMergedConfig).toHaveBeenCalledWith( expect.objectContaining({ unstyled: true, }), ); }); it("should handle config with only defaults", () => { const defaultConfig = { defaults: { variant: "primary", size: "md", }, }; const result = useUI(defaultConfig); expect(result.config).toBeDefined(); }); it("should handle deep config changes", async () => { const component = mount(TestComponent); // Change nested config await component.setProps({ config: { body: { base: "new-base", variants: { variant: { custom: "custom-class", }, }, }, }, }); await nextTick(); expect(uiUtils.getMergedConfig).toHaveBeenCalled(); }); }); describe("Integration Tests", () => { it("should work with real component mounting", () => { const component = mount(TestComponent); expect(component.exists()).toBe(true); expect(component.attributes("data-test")).toBe("test"); }); it("should handle prop changes in mounted component", async () => { const component = mount(TestComponent); // The component should render with the default dataTest from the mock expect(component.exists()).toBe(true); // Test that the component can handle config changes await component.setProps({ config: { body: { base: "new-config-class" }, }, }); // The component should still exist and function after prop changes expect(component.exists()).toBe(true); }); }); });