Files
skywalking-booster-ui/src/components/__tests__/Radio.spec.ts

521 lines
14 KiB
TypeScript

/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import { mount, type VueWrapper } from "@vue/test-utils";
import { nextTick } from "vue";
import Radio from "../Radio.vue";
describe("Radio Component", () => {
let wrapper: Recordable;
const mockOptions = [
{ label: "Option 1", value: "option1" },
{ label: "Option 2", value: "option2" },
{ label: "Option 3", value: "option3" },
];
const mockOptionsWithNumbers = [
{ label: "Option 1", value: 1 },
{ label: "Option 2", value: 2 },
{ label: "Option 3", value: 3 },
];
beforeEach(() => {
vi.clearAllMocks();
});
describe("Props", () => {
it("should render with default props", () => {
wrapper = mount(Radio);
expect(wrapper.exists()).toBe(true);
expect(wrapper.vm.selected).toBe("");
expect(wrapper.vm.options).toEqual([]);
expect(wrapper.vm.size).toBe("default");
});
it("should render with custom options", () => {
wrapper = mount(Radio, {
props: {
options: mockOptions,
},
});
expect(wrapper.vm.options).toEqual(mockOptions);
expect(wrapper.findAll(".el-radio")).toHaveLength(3);
});
it("should render with custom value", () => {
wrapper = mount(Radio, {
props: {
options: mockOptions,
value: "option2",
},
});
expect(wrapper.vm.selected).toBe("option2");
});
it("should render with custom size", () => {
wrapper = mount(Radio, {
props: {
size: "small",
},
});
expect(wrapper.vm.size).toBe("small");
});
it("should handle options with number values", () => {
wrapper = mount(Radio, {
props: {
options: mockOptionsWithNumbers,
value: "2",
},
});
expect(wrapper.vm.options).toEqual(mockOptionsWithNumbers);
expect(wrapper.findAll(".el-radio")).toHaveLength(3);
});
it("should handle empty options array", () => {
wrapper = mount(Radio, {
props: {
options: [],
},
});
expect(wrapper.vm.options).toEqual([]);
expect(wrapper.findAll(".el-radio")).toHaveLength(0);
});
it("should handle undefined options", () => {
wrapper = mount(Radio, {
props: {
options: undefined,
},
});
expect(wrapper.vm.options).toEqual([]);
});
});
describe("Rendering", () => {
it("should render el-radio-group", () => {
wrapper = mount(Radio);
expect(wrapper.find(".el-radio-group").exists()).toBe(true);
});
it("should render correct number of radio options", () => {
wrapper = mount(Radio, {
props: {
options: mockOptions,
},
});
const radioElements = wrapper.findAll(".el-radio");
expect(radioElements).toHaveLength(3);
});
it("should render radio labels correctly", () => {
wrapper = mount(Radio, {
props: {
options: mockOptions,
},
});
const radioElements = wrapper.findAll(".el-radio");
expect(radioElements[0].text()).toContain("Option 1");
expect(radioElements[1].text()).toContain("Option 2");
expect(radioElements[2].text()).toContain("Option 3");
});
it("should set correct key attributes", () => {
wrapper = mount(Radio, {
props: {
options: mockOptions,
},
});
// Vue doesn't expose key attributes directly, but we can verify the structure
const radioElements = wrapper.findAll(".el-radio");
expect(radioElements).toHaveLength(3);
// Verify that each radio has a unique structure based on the options
expect(radioElements[0].text()).toContain("Option 1");
expect(radioElements[1].text()).toContain("Option 2");
expect(radioElements[2].text()).toContain("Option 3");
});
it("should set correct label attributes", () => {
wrapper = mount(Radio, {
props: {
options: mockOptions,
},
});
// Vue doesn't expose label attributes directly, but we can verify the structure
const radioElements = wrapper.findAll(".el-radio");
expect(radioElements).toHaveLength(3);
// Verify that each radio has the correct text content
expect(radioElements[0].text()).toContain("Option 1");
expect(radioElements[1].text()).toContain("Option 2");
expect(radioElements[2].text()).toContain("Option 3");
});
it("should handle mixed string and number labels", () => {
const mixedOptions = [
{ label: "String Label", value: "string" },
{ label: 123, value: "number" },
];
wrapper = mount(Radio, {
props: {
options: mixedOptions,
},
});
const radioElements = wrapper.findAll(".el-radio");
expect(radioElements[0].text()).toContain("String Label");
expect(radioElements[1].text()).toContain("123");
});
});
describe("Events", () => {
it("should emit change event when radio is selected", async () => {
wrapper = mount(Radio, {
props: {
options: mockOptions,
},
});
// Trigger the change event by calling the checked function directly
await wrapper.vm.checked("option2");
await nextTick();
expect(wrapper.emitted("change")).toBeTruthy();
expect(wrapper.emitted("change")[0]).toEqual(["option2"]);
});
it("should emit change event with correct value", async () => {
wrapper = mount(Radio, {
props: {
options: mockOptions,
},
});
await wrapper.vm.checked("option3");
await nextTick();
expect(wrapper.emitted("change")[0]).toEqual(["option3"]);
});
it("should emit change event multiple times", async () => {
wrapper = mount(Radio, {
props: {
options: mockOptions,
},
});
await wrapper.vm.checked("option1");
await nextTick();
await wrapper.vm.checked("option2");
await nextTick();
expect(wrapper.emitted("change")).toHaveLength(2);
expect(wrapper.emitted("change")[0]).toEqual(["option1"]);
expect(wrapper.emitted("change")[1]).toEqual(["option2"]);
});
it("should handle change event with number value", async () => {
wrapper = mount(Radio, {
props: {
options: mockOptionsWithNumbers,
},
});
await wrapper.vm.checked(2);
await nextTick();
expect(wrapper.emitted("change")[0]).toEqual([2]);
});
});
describe("Data Binding", () => {
it("should update selected value when props change", async () => {
wrapper = mount(Radio, {
props: {
options: mockOptions,
value: "option1",
},
});
expect(wrapper.vm.selected).toBe("option1");
await wrapper.setProps({ value: "option2" });
await nextTick();
expect(wrapper.vm.selected).toBe("option2");
});
it("should update options when props change", async () => {
wrapper = mount(Radio, {
props: {
options: mockOptions,
},
});
expect(wrapper.vm.options).toEqual(mockOptions);
const newOptions = [
{ label: "New Option 1", value: "new1" },
{ label: "New Option 2", value: "new2" },
];
await wrapper.setProps({ options: newOptions });
await nextTick();
expect(wrapper.vm.options).toEqual(newOptions);
expect(wrapper.findAll(".el-radio")).toHaveLength(2);
});
it("should maintain selected value when options change", async () => {
wrapper = mount(Radio, {
props: {
options: mockOptions,
value: "option2",
},
});
expect(wrapper.vm.selected).toBe("option2");
const newOptions = [
{ label: "New Option 1", value: "new1" },
{ label: "New Option 2", value: "new2" },
];
await wrapper.setProps({ options: newOptions });
await nextTick();
expect(wrapper.vm.selected).toBe("option2"); // Should maintain the selected value
});
});
describe("Edge Cases", () => {
it("should handle null options", () => {
wrapper = mount(Radio, {
props: {
options: null as any,
},
});
// When null is passed, Vue will pass it through as-is
// The component should handle this gracefully in the template
expect(wrapper.vm.options).toBeNull();
// But the component should still render without errors
expect(wrapper.find(".el-radio-group").exists()).toBe(true);
});
it("should handle undefined value", () => {
wrapper = mount(Radio, {
props: {
value: undefined,
},
});
expect(wrapper.vm.selected).toBe("");
});
it("should handle empty string value", () => {
wrapper = mount(Radio, {
props: {
value: "",
},
});
expect(wrapper.vm.selected).toBe("");
});
it("should handle options with empty labels", () => {
const optionsWithEmptyLabels = [
{ label: "", value: "empty" },
{ label: "Valid Label", value: "valid" },
];
wrapper = mount(Radio, {
props: {
options: optionsWithEmptyLabels,
},
});
expect(wrapper.vm.options).toEqual(optionsWithEmptyLabels);
expect(wrapper.findAll(".el-radio")).toHaveLength(2);
});
it("should handle options with empty values", () => {
const optionsWithEmptyValues = [
{ label: "Label 1", value: "" },
{ label: "Label 2", value: "valid" },
];
wrapper = mount(Radio, {
props: {
options: optionsWithEmptyValues,
},
});
expect(wrapper.vm.options).toEqual(optionsWithEmptyValues);
expect(wrapper.findAll(".el-radio")).toHaveLength(2);
});
it("should handle very long labels", () => {
const longLabel = "A".repeat(1000);
const optionsWithLongLabel = [{ label: longLabel, value: "long" }];
wrapper = mount(Radio, {
props: {
options: optionsWithLongLabel,
},
});
expect(wrapper.vm.options).toEqual(optionsWithLongLabel);
expect(wrapper.findAll(".el-radio")).toHaveLength(1);
});
it("should handle special characters in labels", () => {
const specialOptions = [
{ label: "Option with & symbols", value: "special1" },
{ label: "Option with <script> tags", value: "special2" },
{ label: "Option with 'quotes'", value: "special3" },
];
wrapper = mount(Radio, {
props: {
options: specialOptions,
},
});
expect(wrapper.vm.options).toEqual(specialOptions);
expect(wrapper.findAll(".el-radio")).toHaveLength(3);
});
});
describe("Component Integration", () => {
it("should work with Element Plus radio components", () => {
wrapper = mount(Radio, {
props: {
options: mockOptions,
},
});
const radioGroup = wrapper.find(".el-radio-group");
const radioElements = wrapper.findAll(".el-radio");
expect(radioGroup.exists()).toBe(true);
expect(radioElements.length).toBeGreaterThan(0);
// Verify the component structure is correct
expect(wrapper.vm.selected).toBe("");
});
it("should have correct v-model binding", () => {
wrapper = mount(Radio, {
props: {
options: mockOptions,
value: "option1",
},
});
// Verify the internal selected value matches the prop
expect(wrapper.vm.selected).toBe("option1");
});
it("should have correct change event binding", () => {
wrapper = mount(Radio, {
props: {
options: mockOptions,
},
});
// Verify the component has the checked function
expect(typeof wrapper.vm.checked).toBe("function");
});
});
describe("Accessibility", () => {
it("should have proper ARIA attributes", () => {
wrapper = mount(Radio, {
props: {
options: mockOptions,
},
});
const radioGroup = wrapper.find(".el-radio-group");
expect(radioGroup.exists()).toBe(true);
});
it("should render radio options with proper structure", () => {
wrapper = mount(Radio, {
props: {
options: mockOptions,
},
});
const radioElements = wrapper.findAll(".el-radio");
radioElements.forEach((radio: VueWrapper) => {
expect(radio.exists()).toBe(true);
expect(radio.text()).toBeTruthy();
});
});
});
describe("Performance", () => {
it("should handle large number of options", () => {
const largeOptions = Array.from({ length: 100 }, (_, i) => ({
label: `Option ${i + 1}`,
value: `option${i + 1}`,
}));
wrapper = mount(Radio, {
props: {
options: largeOptions,
},
});
expect(wrapper.vm.options).toEqual(largeOptions);
expect(wrapper.findAll(".el-radio")).toHaveLength(100);
});
it("should handle rapid prop changes", async () => {
wrapper = mount(Radio, {
props: {
options: mockOptions,
value: "option1",
},
});
// Rapidly change props
await wrapper.setProps({ value: "option2" });
await wrapper.setProps({ value: "option3" });
await wrapper.setProps({ value: "option1" });
await nextTick();
expect(wrapper.vm.selected).toBe("option1");
});
});
});