diff --git a/components/menu/__tests__/demo.test.js b/components/menu/__tests__/demo.test.js
new file mode 100644
index 000000000..ad45d6588
--- /dev/null
+++ b/components/menu/__tests__/demo.test.js
@@ -0,0 +1,3 @@
+import demoTest from '../../../tests/shared/demoTest';
+
+demoTest('menu');
diff --git a/components/menu/__tests__/index.test.js b/components/menu/__tests__/index.test.js
new file mode 100644
index 000000000..ed370bc10
--- /dev/null
+++ b/components/menu/__tests__/index.test.js
@@ -0,0 +1,542 @@
+import { mount } from '@vue/test-utils';
+import { asyncExpect } from '@/tests/utils';
+import Menu from '..';
+import { InboxOutlined, PieChartOutlined } from '@ant-design/icons-vue';
+import mountTest from '../../../tests/shared/mountTest';
+
+const { SubMenu } = Menu;
+function $$(className) {
+  return document.body.querySelectorAll(className);
+}
+describe('Menu', () => {
+  mountTest({
+    render() {
+      return (
+        <div>
+          <Menu>
+            <Menu.Item />
+            <Menu.ItemGroup />
+            <Menu.SubMenu />
+          </Menu>
+        </div>
+      );
+    },
+  });
+  beforeEach(() => {
+    document.body.innerHTML = '';
+    // jest.useFakeTimers()
+  });
+
+  afterEach(() => {
+    // jest.useRealTimers()
+  });
+  it('If has select nested submenu item ,the menu items on the grandfather level should be highlight', async () => {
+    mount(
+      {
+        render() {
+          return (
+            <Menu defaultSelectedKeys={['1-3-2']} mode="vertical">
+              <SubMenu key="1" title="submenu1">
+                <Menu.Item key="1-1">Option 1</Menu.Item>
+                <Menu.Item key="1-2">Option 2</Menu.Item>
+                <SubMenu key="1-3" title="submenu1-3">
+                  <Menu.Item key="1-3-1">Option 3</Menu.Item>
+                  <Menu.Item key="1-3-2">Option 4</Menu.Item>
+                </SubMenu>
+              </SubMenu>
+              <Menu.Item key="2">menu2</Menu.Item>
+            </Menu>
+          );
+        },
+      },
+      { attachTo: 'body', sync: false },
+    );
+    await asyncExpect(() => {
+      expect($$('.ant-menu-submenu-selected').length).toBe(1);
+    });
+  });
+  it('should accept defaultOpenKeys in mode horizontal', async () => {
+    mount(
+      {
+        render() {
+          return (
+            <Menu defaultOpenKeys={['1']} mode="horizontal">
+              <SubMenu key="1" title="submenu1">
+                <Menu.Item key="submenu1">Option 1</Menu.Item>
+                <Menu.Item key="submenu2">Option 2</Menu.Item>
+              </SubMenu>
+              <Menu.Item key="2">menu2</Menu.Item>
+            </Menu>
+          );
+        },
+      },
+      { attachTo: 'body', sync: false },
+    );
+    await asyncExpect(() => {
+      expect($$('.ant-menu-sub')[0].parentElement.style.display).not.toBe('none');
+    });
+  });
+
+  it('should accept defaultOpenKeys in mode inline', async () => {
+    mount(
+      {
+        render() {
+          return (
+            <Menu defaultOpenKeys={['1']} mode="inline">
+              <SubMenu key="1" title="submenu1">
+                <Menu.Item key="submenu1">Option 1</Menu.Item>
+                <Menu.Item key="submenu2">Option 2</Menu.Item>
+              </SubMenu>
+              <Menu.Item key="2">menu2</Menu.Item>
+            </Menu>
+          );
+        },
+      },
+      { attachTo: 'body', sync: false },
+    );
+    await asyncExpect(() => {
+      expect($$('.ant-menu-sub')[0].parentElement.style.display).not.toBe('none');
+    });
+  });
+
+  it('should accept defaultOpenKeys in mode vertical', async () => {
+    mount(
+      {
+        render() {
+          return (
+            <Menu defaultOpenKeys={['1']} mode="vertical">
+              <SubMenu key="1" title="submenu1">
+                <Menu.Item key="submenu1">Option 1</Menu.Item>
+                <Menu.Item key="submenu2">Option 2</Menu.Item>
+              </SubMenu>
+              <Menu.Item key="2">menu2</Menu.Item>
+            </Menu>
+          );
+        },
+      },
+      { attachTo: 'body', sync: false },
+    );
+    await asyncExpect(() => {
+      expect($$('.ant-menu-sub')[0].parentElement.style.display).not.toBe('none');
+    });
+  });
+
+  it('horizontal', async () => {
+    const wrapper = mount(
+      {
+        props: {
+          openKeys: {
+            type: Array,
+            default() {
+              return ['1'];
+            },
+          },
+        },
+        render() {
+          return (
+            <Menu openKeys={this.openKeys} mode="horizontal" openTransitionName="">
+              <SubMenu key="1" title="submenu1">
+                <Menu.Item key="submenu1">Option 1</Menu.Item>
+                <Menu.Item key="submenu2">Option 2</Menu.Item>
+              </SubMenu>
+              <Menu.Item key="2">menu2</Menu.Item>
+            </Menu>
+          );
+        },
+      },
+      { attachTo: 'body', sync: false },
+    );
+    await asyncExpect(() => {
+      expect($$('.ant-menu-sub')[0].parentElement.style.display).not.toBe('none');
+    });
+    wrapper.setProps({ openKeys: [] });
+    await asyncExpect(() => {
+      expect($$('.ant-menu-sub')[0].parentElement.style.display).toBe('none');
+    }, 500);
+
+    wrapper.setProps({ openKeys: ['1'] });
+    await asyncExpect(() => {
+      expect($$('.ant-menu-sub')[0].parentElement.style.display).not.toBe('none');
+    }, 0);
+  });
+
+  it('inline', async () => {
+    const wrapper = mount(
+      {
+        props: {
+          openKeys: {
+            type: Array,
+            default() {
+              return ['1'];
+            },
+          },
+        },
+        render() {
+          return (
+            <Menu openKeys={this.openKeys} mode="inline" openAnimation="">
+              <SubMenu key="1" title="submenu1">
+                <Menu.Item key="submenu1">Option 1</Menu.Item>
+                <Menu.Item key="submenu2">Option 2</Menu.Item>
+              </SubMenu>
+              <Menu.Item key="2">menu2</Menu.Item>
+            </Menu>
+          );
+        },
+      },
+      { attachTo: 'body', sync: false },
+    );
+    await asyncExpect(() => {
+      expect($$('.ant-menu-sub')[0].style.display).not.toBe('none');
+    });
+    wrapper.setProps({ openKeys: [] });
+    await asyncExpect(() => {
+      expect($$('.ant-menu-sub')[0].style.display).toBe('none');
+    }, 0);
+    wrapper.setProps({ openKeys: ['1'] });
+    await asyncExpect(() => {
+      expect($$('.ant-menu-sub')[0].style.display).not.toBe('none');
+    }, 0);
+  });
+
+  it('vertical', async () => {
+    const wrapper = mount(
+      {
+        props: {
+          openKeys: {
+            type: Array,
+            default() {
+              return ['1'];
+            },
+          },
+        },
+        render() {
+          return (
+            <Menu openKeys={this.openKeys} mode="vertical" openTransitionName="">
+              <SubMenu key="1" title="submenu1">
+                <Menu.Item key="submenu1">Option 1</Menu.Item>
+                <Menu.Item key="submenu2">Option 2</Menu.Item>
+              </SubMenu>
+              <Menu.Item key="2">menu2</Menu.Item>
+            </Menu>
+          );
+        },
+      },
+      { attachTo: 'body', sync: false },
+    );
+    await asyncExpect(() => {
+      expect($$('.ant-menu-sub')[0].parentElement.style.display).not.toBe('none');
+    });
+    wrapper.setProps({ openKeys: [] });
+    await asyncExpect(() => {
+      expect($$('.ant-menu-sub')[0].parentElement.style.display).toBe('none');
+    }, 500);
+    wrapper.setProps({ openKeys: ['1'] });
+    await asyncExpect(() => {
+      expect($$('.ant-menu-sub')[0].parentElement.style.display).not.toBe('none');
+    }, 0);
+  });
+
+  // https://github.com/ant-design/ant-design/pulls/4677
+  // https://github.com/ant-design/ant-design/issues/4692
+  // TypeError: Cannot read property 'indexOf' of undefined
+  it('pr #4677 and issue #4692', () => {
+    const wrapper = mount(
+      {
+        render() {
+          return (
+            <Menu mode="horizontal">
+              <SubMenu title="submenu">
+                <Menu.Item key="1">menu1</Menu.Item>
+                <Menu.Item key="2">menu2</Menu.Item>
+              </SubMenu>
+            </Menu>
+          );
+        },
+      },
+      { attachTo: 'body', sync: false },
+    );
+    wrapper.vm.$forceUpdate();
+    // just expect no error emit
+  });
+
+  it('should always follow openKeys when mode is switched', async () => {
+    const wrapper = mount(
+      {
+        props: {
+          mode: {
+            type: String,
+            default: 'inline',
+          },
+        },
+        render() {
+          return (
+            <Menu openKeys={['1']} mode={this.mode}>
+              <SubMenu key="1" title="submenu1">
+                <Menu.Item key="submenu1">Option 1</Menu.Item>
+                <Menu.Item key="submenu2">Option 2</Menu.Item>
+              </SubMenu>
+              <Menu.Item key="2">menu2</Menu.Item>
+            </Menu>
+          );
+        },
+      },
+      { attachTo: 'body', sync: false },
+    );
+    await asyncExpect(() => {
+      expect($$('ul.ant-menu-sub')[0].style.display).not.toBe('none');
+    });
+    wrapper.setProps({ mode: 'vertical' });
+    await asyncExpect(() => {
+      expect($$('ul.ant-menu-sub')[0].parentElement.style.display).not.toBe('none');
+    }, 0);
+    wrapper.setProps({ mode: 'inline' });
+    await asyncExpect(() => {
+      expect($$('ul.ant-menu-sub')[0].style.display).not.toBe('none');
+    }, 0);
+  });
+
+  it('should always follow openKeys when inlineCollapsed is switched', async () => {
+    const wrapper = mount(
+      {
+        props: {
+          inlineCollapsed: {
+            type: Boolean,
+            default: false,
+          },
+        },
+        render() {
+          return (
+            <Menu
+              ref="menu"
+              defaultOpenKeys={['1']}
+              mode="inline"
+              inlineCollapsed={this.inlineCollapsed}
+            >
+              <Menu.Item key="menu1">
+                <InboxOutlined />
+                <span>Option</span>
+              </Menu.Item>
+              <SubMenu key="1" title="submenu1">
+                <Menu.Item key="submenu1">Option</Menu.Item>
+                <Menu.Item key="submenu2">Option</Menu.Item>
+              </SubMenu>
+            </Menu>
+          );
+        },
+      },
+      { attachTo: 'body', sync: false },
+    );
+    await asyncExpect(() => {
+      expect(wrapper.findAll('ul.ant-menu-sub')[0].classes()).toContain('ant-menu-inline');
+      expect($$('ul.ant-menu-sub')[0].style.display).not.toBe('none');
+    }, 0);
+    wrapper.setProps({ inlineCollapsed: true });
+    await asyncExpect(() => {
+      // 动画完成后的回调
+      wrapper.vm.$refs.menu.switchModeFromInline = false;
+      wrapper.vm.$forceUpdate();
+    });
+    // await asyncExpect(() => {
+    //   wrapper.trigger('transitionend', { propertyName: 'width' });
+    // });
+    // await asyncExpect(() => {
+    //   expect(wrapper.findAll('ul.ant-menu-root')[0].classes()).toContain('ant-menu-vertical');
+    //   expect(wrapper.findAll('ul.ant-menu-sub').length).toBe(0);
+    // }, 500);
+    wrapper.setProps({ inlineCollapsed: false });
+    await asyncExpect(() => {
+      expect(wrapper.findAll('ul.ant-menu-sub')[0].classes()).toContain('ant-menu-inline');
+      expect($$('ul.ant-menu-sub')[0].style.display).not.toBe('none');
+    }, 0);
+  });
+
+  it('inlineCollapsed should works well when specify a not existed default openKeys', async () => {
+    const wrapper = mount(
+      {
+        props: {
+          inlineCollapsed: {
+            type: Boolean,
+            default: false,
+          },
+        },
+        render() {
+          return (
+            <Menu
+              ref="menu"
+              defaultOpenKeys={['not-existed']}
+              mode="inline"
+              inlineCollapsed={this.inlineCollapsed}
+            >
+              <Menu.Item key="menu1">
+                <InboxOutlined />
+                <span>Option</span>
+              </Menu.Item>
+              <SubMenu key="1" title="submenu1">
+                <Menu.Item key="submenu1">Option</Menu.Item>
+                <Menu.Item key="submenu2">Option</Menu.Item>
+              </SubMenu>
+            </Menu>
+          );
+        },
+      },
+      { attachTo: 'body', sync: false },
+    );
+    await asyncExpect(() => {
+      expect(wrapper.findAll('.ant-menu-sub').length).toBe(0);
+    });
+    wrapper.setProps({ inlineCollapsed: true });
+    await asyncExpect(() => {
+      // 动画完成后的回调
+      wrapper.vm.$refs.menu.switchModeFromInline = false;
+      wrapper.vm.$forceUpdate();
+    });
+    // await asyncExpect(() => {
+    //   wrapper.trigger('transitionend', { propertyName: 'width' });
+    // });
+    // await asyncExpect(() => {
+    //   $$('.ant-menu-submenu-title')[0].dispatchEvent(new MouseEvent('mouseenter'));
+    // });
+    // await asyncExpect(() => {
+    //   expect(wrapper.findAll('.ant-menu-submenu')[0].classes()).toContain(
+    //     'ant-menu-submenu-vertical',
+    //   );
+    //   expect(wrapper.findAll('.ant-menu-submenu')[0].classes()).toContain('ant-menu-submenu-open');
+    //   expect($$('ul.ant-menu-sub')[0].className).toContain('ant-menu-vertical');
+    //   expect($$('ul.ant-menu-sub')[0].style.display).not.toBe('none');
+    // }, 500);
+  });
+
+  describe('open submenu when click submenu title', () => {
+    beforeEach(() => {
+      document.body.innerHTML = '';
+    });
+
+    const toggleMenu = (wrapper, index, event) => {
+      wrapper.findAll('.ant-menu-submenu-title')[index].trigger(event);
+    };
+
+    it('inline', async () => {
+      const wrapper = mount(
+        {
+          render() {
+            return (
+              <Menu mode="inline">
+                <SubMenu key="1" title="submenu1">
+                  <Menu.Item key="submenu1">Option 1</Menu.Item>
+                  <Menu.Item key="submenu2">Option 2</Menu.Item>
+                </SubMenu>
+                <Menu.Item key="2">menu2</Menu.Item>
+              </Menu>
+            );
+          },
+        },
+        { attachTo: 'body', sync: false },
+      );
+      await asyncExpect(() => {
+        expect($$('.ant-menu-sub').length).toBe(0);
+        toggleMenu(wrapper, 0, 'click');
+      }, 0);
+      await asyncExpect(() => {
+        expect($$('.ant-menu-sub').length).toBe(1);
+        expect($$('.ant-menu-sub')[0].style.display).not.toBe('none');
+        toggleMenu(wrapper, 0, 'click');
+      }, 500);
+      await asyncExpect(() => {
+        expect($$('.ant-menu-sub')[0].style.display).toBe('none');
+      }, 500);
+    });
+
+    it('vertical', async () => {
+      const wrapper = mount(
+        {
+          render() {
+            return (
+              <Menu mode="vertical">
+                <SubMenu key="1" title="submenu1">
+                  <Menu.Item key="submenu1">Option 1</Menu.Item>
+                  <Menu.Item key="submenu2">Option 2</Menu.Item>
+                </SubMenu>
+                <Menu.Item key="2">menu2</Menu.Item>
+              </Menu>
+            );
+          },
+        },
+        { attachTo: 'body', sync: false },
+      );
+      await asyncExpect(() => {
+        expect($$('.ant-menu-sub').length).toBe(0);
+        toggleMenu(wrapper, 0, 'mouseenter');
+      }, 0);
+      await asyncExpect(() => {
+        expect($$('.ant-menu-sub').length).toBe(1);
+        expect($$('.ant-menu-sub')[0].parentElement.style.display).not.toBe('none');
+        toggleMenu(wrapper, 0, 'mouseleave');
+      }, 500);
+      await asyncExpect(() => {
+        expect($$('.ant-menu-sub')[0].parentElement.style.display).toBe('none');
+      }, 500);
+    });
+
+    it('horizontal', async () => {
+      const wrapper = mount(
+        {
+          render() {
+            return (
+              <Menu mode="horizontal">
+                <SubMenu key="1" title="submenu1">
+                  <Menu.Item key="submenu1">Option 1</Menu.Item>
+                  <Menu.Item key="submenu2">Option 2</Menu.Item>
+                </SubMenu>
+                <Menu.Item key="2">menu2</Menu.Item>
+              </Menu>
+            );
+          },
+        },
+        { attachTo: 'body', sync: false },
+      );
+      await asyncExpect(() => {
+        expect($$('.ant-menu-sub').length).toBe(0);
+        toggleMenu(wrapper, 1, 'mouseenter');
+      }, 100);
+      await asyncExpect(() => {
+        expect($$('.ant-menu-sub').length).toBe(1);
+        expect($$('.ant-menu-sub')[0].parentElement.style.display).not.toBe('none');
+        toggleMenu(wrapper, 1, 'mouseleave');
+      }, 500);
+      await asyncExpect(() => {
+        expect($$('.ant-menu-sub')[0].parentElement.style.display).toBe('none');
+      }, 500);
+    });
+  });
+
+  it('inline title', async () => {
+    const wrapper = mount(
+      {
+        render() {
+          return (
+            <Menu mode="inline" inlineCollapsed>
+              <Menu.Item key="1" title="bamboo lucky">
+                <PieChartOutlined />
+                <span>
+                  Option 1
+                  <img
+                    style={{ width: 20 }}
+                    alt="test"
+                    src="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg"
+                  />
+                </span>
+              </Menu.Item>
+            </Menu>
+          );
+        },
+      },
+      { sync: false, attachTo: 'body' },
+    );
+
+    wrapper.find('.ant-menu-item').trigger('mouseenter');
+    await asyncExpect(() => {
+      const text = $$('.ant-tooltip-inner')[0].textContent;
+      expect(text).toBe('bamboo lucky');
+    }, 500);
+  });
+});