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