vueless
Version:
Vue Styleless UI Component Library, powered by Tailwind CSS.
495 lines (397 loc) • 13 kB
text/typescript
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);
});
});
});