diff --git a/packages/components/Button/Button.test.tsx b/packages/components/Button/Button.test.tsx index ff6f8e7..647e69c 100644 --- a/packages/components/Button/Button.test.tsx +++ b/packages/components/Button/Button.test.tsx @@ -1,6 +1,8 @@ -import { describe, it, expect } from 'vitest' +import { describe, it, expect, test } from 'vitest' import { mount } from '@vue/test-utils' import Button from './Button.vue' +import Icon from '../Icon/Icon.vue'; +import ButtonGroup from './ButtonGroup.vue'; import type { ButtonType, ButtonSize } from './types'; describe('Button.vue', ()=> { @@ -67,4 +69,124 @@ describe('Button.vue', ()=> { await wrapper.trigger("click"); expect(wrapper.emitted().click).toHaveLength(1); }); -}) \ No newline at end of file + + // Exception Handling: loading state + it("should display loading icon and not emit click event when button is loading", async () => { + const wrapper = mount(Button, { + props: { loading: true }, + global: { + stubs: ["MyIcon"], + } + }); + const iconElement = wrapper.findComponent(Icon); + + expect(wrapper.find('.loading-icon').exists()).toBe(true); + expect(iconElement.exists()).toBeTruthy(); + expect(iconElement.attributes('icon')).toBe('spinner'); + await wrapper.trigger('click'); + expect(wrapper.emitted('click')).toBeUndefined(); + }) + + test("loading button", () => { + const wrapper = mount(Button, { + props: { + loading: true, + }, + slots: { + default: "loading button", + }, + global: { + stubs: ["MyIcon"], + }, + }); + + // class + expect(wrapper.classes()).toContain("is-loading"); + + // attrs + expect(wrapper.attributes("disabled")).toBeDefined(); + expect(wrapper.find("button").element.disabled).toBeTruthy(); + + // events + wrapper.get("button").trigger("click"); + expect(wrapper.emitted()).not.toHaveProperty("click"); + + // icon + const iconElement = wrapper.findComponent(Icon); + expect(iconElement.exists()).toBeTruthy(); + expect(iconElement.attributes("icon")).toBe("spinner"); + }); + + test("icon button", () => { + const wrapper = mount(Button, { + props: { + icon: "arrow-up", + }, + slots: { + default: "icon button", + }, + global: { + stubs: ["MyIcon"], + }, + }); + + const iconElement = wrapper.findComponent(Icon); + expect(iconElement.exists()).toBeTruthy(); + expect(iconElement.attributes("icon")).toBe("arrow-up"); + }); +}) + +describe("ButtonGroup.vue", () => { + test("basic button group", async () => { + const wrapper = mount(() => ( + + + + + )); + + expect(wrapper.classes()).toContain("my-button-group"); + }); + + test("button group size", () => { + const sizes = ["large", "default", "small"]; + sizes.forEach((size) => { + const wrapper = mount(() => ( + + + + + )); + + const buttonWrapper = wrapper.findComponent(Button); + expect(buttonWrapper.classes()).toContain(`my-button--${size}`); + }); + }); + + test("button group type", () => { + const types = ["primary", "success", "warning", "danger", "info"]; + types.forEach((type) => { + const wrapper = mount(() => ( + + + + + )); + + const buttonWrapper = wrapper.findComponent(Button); + expect(buttonWrapper.classes()).toContain(`my-button--${type}`); + }); + }); + + test("button group disabled", () => { + const wrapper = mount(() => ( + + + + + )); + + const buttonWrapper = wrapper.findComponent(Button); + expect(buttonWrapper.classes()).toContain(`is-disabled`); + }); +}); \ No newline at end of file diff --git a/packages/components/Button/Button.vue b/packages/components/Button/Button.vue index 9d60894..5622c43 100644 --- a/packages/components/Button/Button.vue +++ b/packages/components/Button/Button.vue @@ -1,8 +1,9 @@ + + \ No newline at end of file diff --git a/packages/components/Button/contants.ts b/packages/components/Button/contants.ts new file mode 100644 index 0000000..3542bd4 --- /dev/null +++ b/packages/components/Button/contants.ts @@ -0,0 +1,6 @@ +import type { InjectionKey } from "vue"; +import type { ButtonGroupContext } from "./types"; + +export const BUTTON_GROUP_CTX_KEY: InjectionKey = + Symbol("BUTTON_GROUP_CTX_KEY"); + \ No newline at end of file diff --git a/packages/components/Button/index.ts b/packages/components/Button/index.ts index ff7b42d..657e5c2 100644 --- a/packages/components/Button/index.ts +++ b/packages/components/Button/index.ts @@ -1,6 +1,8 @@ import Button from './Button.vue' +import ButtonGroup from './ButtonGroup.vue' import { withInstall } from '@moyu-ui/utils' export const MyButton = withInstall(Button) +export const MyButtonGroup = withInstall(ButtonGroup) export * from './types' \ No newline at end of file diff --git a/packages/components/Button/types.ts b/packages/components/Button/types.ts index 4874e6f..463e3aa 100644 --- a/packages/components/Button/types.ts +++ b/packages/components/Button/types.ts @@ -31,4 +31,16 @@ export interface ButtonInstance { // disabled: ComputedRef; // size: ComputedRef; // type: ComputedRef; -} \ No newline at end of file +} + +export interface ButtonGroupProps { + size?: string; + disabled?: boolean; + type?: string; +} + +export interface ButtonGroupContext { + size?: string; + disabled?: boolean; + type? : string; +} \ No newline at end of file diff --git a/packages/core/components.ts b/packages/core/components.ts index b781427..5c0a3ea 100644 --- a/packages/core/components.ts +++ b/packages/core/components.ts @@ -1,4 +1,4 @@ -import { MyButton, MyIcon } from "@moyu-ui/components"; +import { MyButton, MyIcon, MyButtonGroup } from "@moyu-ui/components"; import type { Plugin } from "vue"; -export default [MyButton, MyIcon ] as Plugin[]; \ No newline at end of file +export default [MyButton, MyIcon, MyButtonGroup ] as Plugin[]; \ No newline at end of file diff --git a/packages/play/src/stories/Button.stories.ts b/packages/play/src/stories/Button.stories.ts index 6258bde..9f260c2 100644 --- a/packages/play/src/stories/Button.stories.ts +++ b/packages/play/src/stories/Button.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj, ArgTypes } from "@storybook/vue3"; import { fn, within, userEvent, expect } from "@storybook/test"; -import { MyButton } from "moyu-ui"; +import { MyButton, MyButtonGroup } from "moyu-ui"; // import "moyu-ui/dist/theme/Button.css"; type Story = StoryObj & { argTypes?: ArgTypes }; @@ -87,80 +87,80 @@ export const Default: Story & { args: { content: string } } = { }, }; -// export const Circle: Story = { -// args: { -// icon: "search", -// }, -// render: (args) => ({ -// components: { MyButton }, -// setup() { -// return { args }; -// }, -// template: container(` -// -// `), -// }), -// play: async ({ canvasElement, args, step }) => { -// const canvas = within(canvasElement); -// await step("click button", async () => { -// await userEvent.click(canvas.getByRole("button")); -// }); +export const Circle: Story = { + args: { + icon: "search", + }, + render: (args) => ({ + components: { MyButton }, + setup() { + return { args }; + }, + template: container(` + + `), + }), + play: async ({ canvasElement, args, step }) => { + const canvas = within(canvasElement); + await step("click button", async () => { + await userEvent.click(canvas.getByRole("button")); + }); -// expect(args.onClick).toHaveBeenCalled(); -// }, -// }; + expect(args.onClick).toHaveBeenCalled(); + }, +}; -// Circle.parameters = {}; +Circle.parameters = {}; -// export const Group: Story & { args: { content1: string; content2: string } } = { -// argTypes: { -// groupType: { -// control: { type: "select" }, -// options: ["primary", "success", "warning", "danger", "info", ""], -// }, -// groupSize: { -// control: { type: "select" }, -// options: ["large", "default", "small", ""], -// }, -// groupDisabled: { -// control: "boolean", -// }, -// content1: { -// control: { type: "text" }, -// defaultValue: "Button1", -// }, -// content2: { -// control: { type: "text" }, -// defaultValue: "Button2", -// }, -// }, -// args: { -// round: true, -// content1: "Button1", -// content2: "Button2", -// }, -// render: (args) => ({ -// components: { ErButton, ErButtonGroup }, -// setup() { -// return { args }; -// }, -// template: container(` -// -// {{args.content1}} -// {{args.content2}} -// -// `), -// }), -// play: async ({ canvasElement, args, step }) => { -// const canvas = within(canvasElement); -// await step("click btn1", async () => { -// await userEvent.click(canvas.getByText("Button1")); -// }); -// await step("click btn2", async () => { -// await userEvent.click(canvas.getByText("Button2")); -// }); -// expect(args.onClick).toHaveBeenCalled(); -// }, -// }; +export const Group: Story & { args: { content1: string; content2: string } } = { + argTypes: { + groupType: { + control: { type: "select" }, + options: ["primary", "success", "warning", "danger", "info", ""], + }, + groupSize: { + control: { type: "select" }, + options: ["large", "default", "small", ""], + }, + groupDisabled: { + control: "boolean", + }, + content1: { + control: { type: "text" }, + defaultValue: "Button1", + }, + content2: { + control: { type: "text" }, + defaultValue: "Button2", + }, + }, + args: { + round: true, + content1: "Button1", + content2: "Button2", + }, + render: (args) => ({ + components: { MyButton, MyButtonGroup }, + setup() { + return { args }; + }, + template: container(` + + {{args.content1}} + {{args.content2}} + + `), + }), + play: async ({ canvasElement, args, step }) => { + const canvas = within(canvasElement); + await step("click btn1", async () => { + await userEvent.click(canvas.getByText("Button1")); + }); + await step("click btn2", async () => { + await userEvent.click(canvas.getByText("Button2")); + }); + expect(args.onClick).toHaveBeenCalled(); + }, +}; export default meta; \ No newline at end of file