Repository: IduxFE/components (ts)

Issue:
[comp:image ]feature image component 
- [x] I have searched the [issues](https://github.com/IduxFE/components/issues) of this repository and believe that this is not a duplicate.

### What problem does this feature solve?
<!-- Explain your use case, context, and rationale behind this feature request. More importantly, what is the end user experience you are trying to build that led to the need for this feature? -->
- 需要展示图片时使用。
- 单击图像可以放大显示 。
- 加载失败显示图像占位符。


### What does the proposed API look like?
<!-- Describe how you propose to solve the problem and provide code samples of how the API would work once implemented. Note that you can use Markdown to format your code blocks. -->
| 属性 | 说明 | 类型  | 全局配置 |版本号|
| --- | --- | --- | --- |---|
| alt | 图像描述| string | - |-|
| fallback | 加载失败容错地址| string | - |-|
|height|图像高度|string \| number|-|-|
|preview|预览参数，为`false` 时禁用|boolean|-|-|
|src|图片地址|string|-|-|
|width|图像宽度|string \| number|-|-|
|fit|确定图片如何适应容器框，同原生 [object-fit](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit)|string|-|-|







Pull request:
feat(comp:image): add image component
fix #30 

## PR Checklist

Please check if your PR fulfills the following requirements:

- [x] The commit message follows [our guidelines](https://github.com/IduxFE/components/blob/main/docs/contributing.zh.md#commit)
- [x] Tests for the changes have been added/updated or not needed
- [x] Docs and demo have been added/updated or not needed

## PR Type

What kind of change does this PR introduce?

- [ ] Bugfix
- [x] Feature
- [ ] Component style update
- [ ] Code style update (formatting, local variables)
- [ ] Refactoring (no functional changes, no api changes)
- [ ] Build related changes
- [ ] CI related changes
- [ ] Documentation content changes
- [ ] Application (the showcase website) / infrastructure changes
- [ ] Other... Please describe:

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->

## What is the new behavior?

## Other information


Patch:
diff --git a/packages/components/components.less b/packages/components/components.less
index bc8282c6..1c6a206f 100644
--- a/packages/components/components.less
+++ b/packages/components/components.less
@@ -2,3 +2,5 @@
 @import './icon/style/index.less';
 @import './badge/style/index.less';
 @import './divider/style/index.less';
+@import './image/style/index.less';
+
diff --git a/packages/components/core/config/types.ts b/packages/components/core/config/types.ts
index d4cec656..baeb2291 100644
--- a/packages/components/core/config/types.ts
+++ b/packages/components/core/config/types.ts
@@ -7,6 +7,7 @@ export interface GlobalConfig {
   icon: IconConfig
   badge: BadgeConfig
   divider: DividerConfig
+  image: ImageConfig
 }
 
 export interface ButtonConfig {
@@ -30,3 +31,8 @@ export interface DividerConfig {
   position: DividerPosition
   type: DividerType
 }
+export interface ImageConfig {
+  width: string | number
+  height: string | number
+  fallback: string
+}
diff --git a/packages/components/core/config/useGlobalConfig.ts b/packages/components/core/config/useGlobalConfig.ts
index ad4b417a..37de7c66 100644
--- a/packages/components/core/config/useGlobalConfig.ts
+++ b/packages/components/core/config/useGlobalConfig.ts
@@ -1,6 +1,14 @@
 import { inject, shallowReactive, provide, readonly } from 'vue'
 import type { DeepReadonly } from 'vue'
-import type { BadgeConfig, ButtonConfig, GlobalConfig, GlobalConfigKey, IconConfig, DividerConfig } from './types'
+import type {
+  BadgeConfig,
+  ButtonConfig,
+  GlobalConfig,
+  GlobalConfigKey,
+  IconConfig,
+  DividerConfig,
+  ImageConfig,
+} from './types'
 
 const button = shallowReactive<ButtonConfig>({ mode: 'default', size: 'medium' })
 const icon = shallowReactive<IconConfig>({})
@@ -12,11 +20,19 @@ const divider = shallowReactive<DividerConfig>({
   type: 'horizontal',
 })
 
+const image: ImageConfig = shallowReactive({
+  width: 100,
+  height: 100,
+  fallback:
+    'data:image/png;base64,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',
+})
+
 const defaultConfig: GlobalConfig = {
   button,
   icon,
   badge,
   divider,
+  image,
 }
 
 const globalConfigToken: Record<GlobalConfigKey, symbol> = {
@@ -24,6 +40,7 @@ const globalConfigToken: Record<GlobalConfigKey, symbol> = {
   icon: Symbol(),
   badge: Symbol(),
   divider: Symbol(),
+  image: Symbol(),
 }
 
 /**
diff --git a/packages/components/icon/src/icons.ts b/packages/components/icon/src/icons.ts
index 08546f21..157b8b63 100644
--- a/packages/components/icon/src/icons.ts
+++ b/packages/components/icon/src/icons.ts
@@ -5,5 +5,22 @@ import { Left } from '../definitions/left'
 import { Right } from '../definitions/right'
 import { Loading } from '../definitions/loading'
 import { Empty } from '../definitions/empty'
+import { RotateLeft } from '../definitions/rotateLeft'
+import { RotateRight } from '../definitions/rotateRight'
+import { ZoomIn } from '../definitions/zoomIn'
+import { ZoomOut } from '../definitions/zoomOut'
+import { Close } from '../definitions/close'
 
-export const innerStaticIcons: IconDefinition[] = [Up, Down, Left, Right, Loading, Empty]
+export const innerStaticIcons: IconDefinition[] = [
+  Up,
+  Down,
+  Left,
+  Right,
+  Loading,
+  Empty,
+  RotateLeft,
+  RotateRight,
+  ZoomIn,
+  ZoomOut,
+  Close,
+]
diff --git a/packages/components/image/demo/basic.md b/packages/components/image/demo/basic.md
new file mode 100644
index 00000000..89c2cacb
--- /dev/null
+++ b/packages/components/image/demo/basic.md
@@ -0,0 +1,47 @@
+---
+order: 0
+title:
+  zh: 基本使用
+ 
+---
+
+## zh
+
+可通过`fit`确定图片如何适应到容器框，同原生 [object-fit](https://developer.mozilla.org/en-US/docs/Web/CSS/object-objectFit)
+
+## demo
+
+```html
+<template>
+  <div style="display:flex;justify-content: space-around">
+    <ix-image objectFit="fill" :width="null" class="demo" src="https://cdn.jsdelivr.net/gh/danranvm/image-hosting/images/idux.jpg" alt="demo"/>
+    <ix-image objectFit="contain" class="demo" src="https://cdn.jsdelivr.net/gh/danranvm/image-hosting/images/idux.jpg" />
+    <ix-image objectFit="contain" class="demo" src="https://cdn.jsdelivr.net/gh/danranvm/image-hosting/images/idux.jpg" />
+    <ix-image objectFit="cover" class="demo" src="https://cdn.jsdelivr.net/gh/danranvm/image-hosting/images/idux.jpg" />
+    <ix-image objectFit="none" class="demo" src="https://cdn.jsdelivr.net/gh/danranvm/image-hosting/images/idux.jpg" />
+    <ix-image objectFit="scale-down" class="demo" src="https://cdn.jsdelivr.net/gh/danranvm/image-hosting/images/idux.jpg" />
+  </div>
+</template>
+
+<style lang="less" >
+  .demo{
+    width:100px;
+    height:100px;
+  }
+</style>
+<script>
+import { defineComponent, ref } from 'vue'
+export default defineComponent({
+ setup(){
+    const width = ref(100);
+    const height = ref(100);
+    return {
+      width,
+      height
+    }
+  }
+})
+
+</script>
+
+```
diff --git a/packages/components/image/demo/fallback.md b/packages/components/image/demo/fallback.md
new file mode 100644
index 00000000..1f593c21
--- /dev/null
+++ b/packages/components/image/demo/fallback.md
@@ -0,0 +1,29 @@
+---
+order: 0
+title:
+  zh: 容错处理
+ 
+---
+## zh
+
+通过设置`fallback`，当图片加载失败的时候会显示此占位内容
+
+## demo
+
+```html
+
+<template>
+    <ix-image  src="" :preview="true" width="200px" height="200px" :fallback="fallback"/>
+</template>
+<script>
+import { defineComponent, ref } from 'vue'
+export default defineComponent({
+ setup(){
+    const fallback = ref('data:image/png;base64,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');
+    return {
+      fallback
+    }
+  }
+})
+</script>
+```
diff --git a/packages/components/image/demo/preview.md b/packages/components/image/demo/preview.md
new file mode 100644
index 00000000..377a9ef5
--- /dev/null
+++ b/packages/components/image/demo/preview.md
@@ -0,0 +1,18 @@
+---
+order: 0
+title:
+  zh: 预览大图
+ 
+---
+
+## zh
+
+通过设置`preview`为`true`,使成功加载的图片有点击预览大图的功能
+
+## demo
+
+```html
+<template>
+    <ix-image  src="https://cdn.jsdelivr.net/gh/danranvm/image-hosting/images/idux.jpg" :preview="true"  width="200px" height="200px" />
+</template>
+```
diff --git a/packages/components/image/docs/index.zh.md b/packages/components/image/docs/index.zh.md
new file mode 100644
index 00000000..ae2d71f5
--- /dev/null
+++ b/packages/components/image/docs/index.zh.md
@@ -0,0 +1,32 @@
+---
+category: components
+type: 通用
+title: Image
+subtitle: 图片
+cover: 可预览的图片
+---
+
+
+
+## 何时使用
+
+- 需要展示图片时使用。
+- 加载大图时显示或加载失败时容错处理。
+
+## API
+
+| 属性 | 说明 | 类型  | 全局配置 |
+| --- | --- | --- | --- |
+| alt | 图像描述| string | - |
+| fallback | 加载失败容错地址| string | - |
+|height|图像高度|string \| number|-|
+|preview|预览参数，为`false` 时禁用|boolean|-|
+|src|图片地址|string|-|
+|width|图像宽度|string \| number|-|
+|objectFit|确定图片如何适应容器框，同原生 [object-fit](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit)|string|-|
+
+### Events
+
+| 事件名称 | 说明 | 参数类型 |
+| --- | --- | --- |
+| statusChange | 图片加载状态改变时触发 | `loading\|loaded\|failed` |
diff --git a/packages/components/image/index.ts b/packages/components/image/index.ts
new file mode 100644
index 00000000..e1ce530e
--- /dev/null
+++ b/packages/components/image/index.ts
@@ -0,0 +1,7 @@
+import { installComponent } from '@idux/components/core/utils'
+import IxImage from './src/Image.vue'
+
+IxImage.install = installComponent(IxImage)
+
+export { IxImage }
+export type { IxImageComponent } from './src/types'
diff --git a/packages/components/image/src/Image.vue b/packages/components/image/src/Image.vue
new file mode 100644
index 00000000..2082cafb
--- /dev/null
+++ b/packages/components/image/src/Image.vue
@@ -0,0 +1,120 @@
+<template>
+  <div class="ix-image">
+    <img
+      v-if="imageStatus !== 'failed'"
+      :class="['ix-image-img', preview ? 'ix-image-preview-is' : '']"
+      :src="src"
+      :style="{ width: imageWidth, height: imageHeight, objectFit: objectFit }"
+      :alt="alt"
+      @error="onError"
+      @load="onLoaded"
+      @click="onClick"
+    />
+    <div
+      v-else-if="imageStatus === 'failed'"
+      class="ix-image-error"
+      :style="{
+        width: imageWidth,
+        height: imageHeight,
+        objectFit: objectFit,
+        background: `url(${fallback || imageConfig.fallback})`,
+      }"
+    ></div>
+    <template v-if="imageStatus !== 'failed' && preview">
+      <img-preview v-if="isShowPreview" :previewSrc="src" @close="onClose"></img-preview>
+    </template>
+  </div>
+</template>
+<script lang="ts">
+import { defineComponent, computed, ref, watchEffect, watch } from 'vue'
+import { ImageProps, ImageStatus } from './types'
+import ImgPreview from './ImgPreview.vue'
+import { useGlobalConfig } from '@idux/components/core/config'
+import { toCssPixel } from '@idux/cdk/utils/convert'
+
+export default defineComponent({
+  name: 'IxImage',
+  components: { ImgPreview },
+  props: {
+    src: {
+      type: String,
+      default: '',
+    },
+    width: {
+      type: [String, Number],
+      default: null,
+    },
+    height: {
+      type: [String, Number],
+      default: null,
+    },
+    preview: {
+      type: Boolean,
+      default: false,
+    },
+    fallback: {
+      type: String,
+      default: undefined,
+    },
+    alt: {
+      type: String,
+      default: '',
+    },
+    objectFit: {
+      type: String,
+      default: 'fill',
+    },
+  },
+  emits: {
+    statusChange: (status: string) => {
+      return !!status
+    },
+  },
+  setup(props: ImageProps, { emit }) {
+    const isShowPreview = ref(false)
+    const imageConfig = useGlobalConfig('image')
+    const imageWidth = computedSize(props, imageConfig, 'width')
+    const imageHeight = computedSize(props, imageConfig, 'height')
+    const imageStatus = ref<ImageStatus>('loading')
+    const onError = () => {
+      imageStatus.value = 'failed'
+    }
+    const onLoaded = () => {
+      imageStatus.value = 'loaded'
+    }
+    const onClick = () => {
+      isShowPreview.value = true
+    }
+    const onClose = () => {
+      isShowPreview.value = false
+    }
+    watchEffect(() => {
+      const status = imageStatus.value
+      emit('statusChange', status)
+    })
+    watch(
+      () => props.src,
+      () => {
+        imageStatus.value = 'loading'
+      },
+    )
+    return {
+      imageWidth,
+      imageHeight,
+      onLoaded,
+      onError,
+      onClick,
+      isShowPreview,
+      onClose,
+      imageStatus,
+      imageConfig,
+    }
+  },
+})
+const computedSize = (props: ImageProps, imageConfig: ImageProps, type: 'width' | 'height') => {
+  return computed(() => {
+    const size = props[type] ?? imageConfig[type]
+    return toCssPixel(size)
+  })
+}
+</script>
diff --git a/packages/components/image/src/ImgPreview.vue b/packages/components/image/src/ImgPreview.vue
new file mode 100644
index 00000000..45fd716b
--- /dev/null
+++ b/packages/components/image/src/ImgPreview.vue
@@ -0,0 +1,85 @@
+<template>
+  <div class="ix-image-preview">
+    <div class="ix-image-preview-mask"></div>
+    <div class="ix-image-preview-tools">
+      <ul class="ix-preview-tools">
+        <li class="ix-tools-item ix-rotate-left" @click="rotateEvent(-1)">
+          <ix-icon name="rotate-left" />
+        </li>
+        <li class="ix-tools-item ix-rotate-right" @click="rotateEvent(1)">
+          <ix-icon name="rotate-right" />
+        </li>
+        <li class="ix-tools-item ix-zoom-in" @click="zoomEvent(1)">
+          <ix-icon name="zoom-in" />
+        </li>
+        <li
+          class="ix-tools-item ix-zoom-out"
+          :class="isZoomOutDisabled ? 'ix-tools-item-disabled' : ''"
+          @click="zoomEvent(-1)"
+        >
+          <ix-icon name="zoom-out" />
+        </li>
+        <li class="ix-tools-item ix-close" @click="close">
+          <ix-icon name="close" />
+        </li>
+      </ul>
+    </div>
+    <div class="ix-image-preview-img">
+      <img :src="previewSrc" alt="" :style="{ transform: transform }" />
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref, computed } from 'vue'
+import { ImagePreviewProps } from './types'
+import { IxIcon } from '@idux/components/icon'
+const minScale = 0.2
+const initScale = 1.0
+const scaleStep = 0.1
+const initRotate = 0
+const rotateStep = 90
+
+export default defineComponent({
+  name: 'IxImgPreview',
+  components: { IxIcon },
+  props: {
+    previewSrc: {
+      type: String,
+      default: '',
+    },
+  },
+  emits: {
+    close: () => {
+      return true
+    },
+  },
+  setup(props: ImagePreviewProps, { emit }) {
+    const scale = ref(initScale)
+    const rotate = ref(initRotate)
+    const transform = computed(() => `scale3d(${scale.value}, ${scale.value}, 1) rotate(${rotate.value}deg)`)
+    const isZoomOutDisabled = computed(() => scale.value < minScale)
+    const close = () => {
+      emit('close')
+    }
+    const rotateEvent = (direction: number) => {
+      rotate.value += direction * rotateStep
+    }
+    const zoomEvent = (direction: number) => {
+      if (scale.value > minScale || direction > 0) {
+        scale.value += direction * scaleStep
+      }
+    }
+
+    return {
+      close,
+      scale,
+      rotate,
+      rotateEvent,
+      zoomEvent,
+      transform,
+      isZoomOutDisabled,
+    }
+  },
+})
+</script>
diff --git a/packages/components/image/src/types.ts b/packages/components/image/src/types.ts
new file mode 100644
index 00000000..79a9a63d
--- /dev/null
+++ b/packages/components/image/src/types.ts
@@ -0,0 +1,16 @@
+export interface ImageProps {
+  readonly src?: string
+  readonly width?: string | number
+  readonly height?: string | number
+  readonly fallback?: string
+  readonly preview?: boolean
+  readonly alt?: string
+  readonly fit?: string
+}
+export interface ImagePreviewProps {
+  readonly previewSrc?: string
+}
+export type ImageStatus = 'loading' | 'loaded' | 'failed'
+
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface IxImageComponent extends ImageProps {}
diff --git a/packages/components/image/style/index.less b/packages/components/image/style/index.less
new file mode 100644
index 00000000..79b7576f
--- /dev/null
+++ b/packages/components/image/style/index.less
@@ -0,0 +1,73 @@
+@import '../../style/default.less';
+@image-prefix: ~'@{idux-prefix}-image';
+@image-preview-prefix: ~'@{image-prefix}-preview';
+    
+@image-mask-bg: ~'rgba(0, 0, 0, 0.45)';
+@image-tools-bg: ~'rgba(0, 0, 0, 0.1)';
+@image-height: 100%;
+@image-width:100%;
+@image-tools-h: 46px;
+
+.ix-image-preview-base-position(@zIndex,@top){
+  position: fixed;
+  left: 0;
+  right: 0;
+  top: @top;
+  bottom: 0;
+  z-index: @zIndex;
+};
+.@{image-prefix} {
+  &-img.@{image-preview-prefix}-is {
+    cursor: pointer;
+  }
+}
+
+.@{image-preview-prefix} {
+  cursor: default;
+  user-select: none;
+  &-mask {
+    .ix-image-preview-base-position(9999,0);
+
+    background-color: @image-mask-bg;
+    height: @image-height;
+  };
+  &-tools {
+    .ix-image-preview-base-position(10000,0);
+
+    height: @image-tools-h;
+    background: @image-tools-bg;
+    .ix-preview-tools {
+      display: flex;
+      align-items: center;
+      justify-content: flex-end;
+      list-style: none;
+      height: @image-height;
+      .ix-tools-item {
+        padding: 0 12px;
+        cursor: pointer;
+        color:#fff;
+        &:not(:first-child) {
+          margin-left: 12px;
+        };
+       
+      };
+      .ix-tools-item-disabled {
+        color: rgba(255, 255, 255, 0.25);
+        pointer-events: none;
+        cursor:not-allowed;
+      };
+    };
+  };
+  .ix-image-preview-img {
+    .ix-image-preview-base-position(9999,@image-tools-h);
+
+    height: @image-height;
+    display: flex;
+    justify-content: center;
+    img {
+      height: @image-height;
+      object-fit: none;
+      transition: transform 0.3s cubic-bezier(0.215, 0.61, 0.355, 1) 0s;
+    };
+  };
+};
diff --git a/packages/components/index.ts b/packages/components/index.ts
index b92b7540..d2af7851 100644
--- a/packages/components/index.ts
+++ b/packages/components/index.ts
@@ -3,8 +3,9 @@ import { IxButton, IxButtonGroup } from './button'
 import { IxIcon } from './icon'
 import { IxBadge } from './badge'
 import { IxDivider } from './divider'
+import { IxImage } from './image'
 
-const components = [IxButton, IxButtonGroup, IxIcon, IxBadge, IxDivider]
+const components = [IxButton, IxButtonGroup, IxIcon, IxBadge, IxDivider, IxImage]
 
 const install = (app: App): void => {
   components.forEach(component => {
@@ -21,9 +22,9 @@ export default {
 
 export * from './core/config'
 export * from './core/types'
-
 export * from './button'
 export * from './i18n'
 export * from './icon'
 export * from './badge'
 export * from './divider'
+export * from './image'


Test patch:
diff --git a/packages/components/image/__tests__/__snapshots__/image.spec.ts.snap b/packages/components/image/__tests__/__snapshots__/image.spec.ts.snap
new file mode 100644
index 00000000..bc123204
--- /dev/null
+++ b/packages/components/image/__tests__/__snapshots__/image.spec.ts.snap
@@ -0,0 +1,15 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Image.vue render alt work 1`] = `"<div class=\\"ix-image\\"><img class=\\"ix-image-img\\" src=\\"https://cdn.jsdelivr.net/gh/danranvm/image-hosting/images/idux.jpg\\" style=\\"width: 100px; height: 100px; object-fit: fill;\\" alt=\\"demo\\"><!--v-if--></div>"`;
+
+exports[`Image.vue render height work 1`] = `"<div class=\\"ix-image\\"><img class=\\"ix-image-img\\" src=\\"https://cdn.jsdelivr.net/gh/danranvm/image-hosting/images/idux.jpg\\" style=\\"width: 100px; height: 100px; object-fit: fill;\\" alt=\\"\\"><!--v-if--></div>"`;
+
+exports[`Image.vue render objectFit work 1`] = `"<div class=\\"ix-image\\"><img class=\\"ix-image-img\\" src=\\"https://cdn.jsdelivr.net/gh/danranvm/image-hosting/images/idux.jpg\\" style=\\"width: 100px; height: 100px; object-fit: fill;\\" alt=\\"\\"><!--v-if--></div>"`;
+
+exports[`Image.vue render preview work 1`] = `"<div class=\\"ix-image\\"><img class=\\"ix-image-img ix-image-preview-is\\" src=\\"https://cdn.jsdelivr.net/gh/danranvm/image-hosting/images/idux.jpg\\" style=\\"width: 200px; height: 200px; object-fit: fill;\\" alt=\\"\\"><!--v-if--></div>"`;
+
+exports[`Image.vue render src work 1`] = `"<div class=\\"ix-image\\"><img class=\\"ix-image-img\\" src=\\"https://cdn.jsdelivr.net/gh/danranvm/image-hosting/images/idux.jpg\\" style=\\"width: 100px; height: 100px; object-fit: fill;\\" alt=\\"\\"><!--v-if--></div>"`;
+
+exports[`Image.vue render width work 1`] = `"<div class=\\"ix-image\\"><img class=\\"ix-image-img\\" src=\\"https://cdn.jsdelivr.net/gh/danranvm/image-hosting/images/idux.jpg\\" style=\\"width: 100px; height: 100px; object-fit: fill;\\" alt=\\"\\"><!--v-if--></div>"`;
+
+exports[`Image.vue render work 1`] = `"<div class=\\"ix-image\\"><img class=\\"ix-image-img\\" src=\\"\\" style=\\"width: 100px; height: 100px; object-fit: fill;\\" alt=\\"\\"><!--v-if--></div>"`;
diff --git a/packages/components/image/__tests__/image.spec.ts b/packages/components/image/__tests__/image.spec.ts
new file mode 100644
index 00000000..9046d2f4
--- /dev/null
+++ b/packages/components/image/__tests__/image.spec.ts
@@ -0,0 +1,144 @@
+import { flushPromises, mount, MountingOptions, VueWrapper } from '@vue/test-utils'
+import { DefineComponent } from 'vue'
+import Image from '../src/Image.vue'
+import { ImageProps } from '../src/types'
+
+describe('Image.vue', () => {
+  let ImageMount: (
+    options?: MountingOptions<Partial<ImageProps>>,
+  ) => VueWrapper<InstanceType<DefineComponent<ImageProps>>>
+
+  beforeEach(() => {
+    ImageMount = (options = {}) => {
+      return mount<ImageProps>(Image, {
+        ...options,
+      })
+    }
+  })
+
+  test('render work', async () => {
+    const wrapper = ImageMount()
+    await flushPromises()
+    await wrapper.find('img').trigger('load')
+    expect(wrapper.html()).toMatchSnapshot()
+  })
+  test('render src work', async () => {
+    const wrapper = ImageMount({
+      props: {
+        src: 'https://cdn.jsdelivr.net/gh/danranvm/image-hosting/images/idux.jpg',
+      },
+    })
+    await flushPromises()
+    expect(wrapper.html()).toMatchSnapshot()
+    await wrapper.setProps({ src: 'https://cdn.jsdelivr.net/gh/danranvm/image-hosting/images/vue.png' })
+  })
+  test('render width work', async () => {
+    const wrapper = ImageMount({
+      props: {
+        width: '100px',
+        src: 'https://cdn.jsdelivr.net/gh/danranvm/image-hosting/images/idux.jpg',
+      },
+    })
+    await flushPromises()
+    expect(wrapper.html()).toMatchSnapshot()
+    await wrapper.setProps({ width: '200px' })
+    expect(wrapper.find('img').attributes()['style']).toMatch('width: 200px')
+    await wrapper.setProps({ width: null })
+
+    expect(wrapper.find('img').attributes()['style']).toMatch(`width: 100px`)
+  })
+  test('render height work', async () => {
+    const wrapper = ImageMount({
+      props: {
+        height: '100px',
+        src: 'https://cdn.jsdelivr.net/gh/danranvm/image-hosting/images/idux.jpg',
+      },
+    })
+    await flushPromises()
+    expect(wrapper.html()).toMatchSnapshot()
+    await wrapper.setProps({ height: '200px' })
+    expect(wrapper.find('img').attributes()['style']).toMatch('height: 200px')
+    await wrapper.setProps({ height: null })
+    expect(wrapper.find('img').attributes()['style']).toMatch('height: 100px')
+  })
+  test('render alt work', async () => {
+    const wrapper = ImageMount({
+      props: {
+        alt: 'demo',
+        src: 'https://cdn.jsdelivr.net/gh/danranvm/image-hosting/images/idux.jpg',
+      },
+    })
+    await flushPromises()
+    expect(wrapper.html()).toMatchSnapshot()
+    expect(wrapper.find('img').attributes()['alt']).toEqual('demo')
+  })
+  test('render objectFit work', async () => {
+    const wrapper = ImageMount({
+      props: {
+        src: 'https://cdn.jsdelivr.net/gh/danranvm/image-hosting/images/idux.jpg',
+      },
+    })
+    await flushPromises()
+    expect(wrapper.html()).toMatchSnapshot()
+    await wrapper.setProps({ objectFit: 'fill' })
+    expect(wrapper.find('img').attributes()['style']).toMatch('fill')
+  })
+
+  test('render preview work', async () => {
+    const wrapper = ImageMount({
+      props: {
+        preview: true,
+        width: 200,
+        height: 200,
+        src: 'https://cdn.jsdelivr.net/gh/danranvm/image-hosting/images/idux.jpg',
+      },
+    })
+    await flushPromises()
+    expect(wrapper.html()).toMatchSnapshot()
+    await wrapper.find('.ix-image-preview-is').trigger('click')
+
+    expect(wrapper.find('.ix-image-preview').exists()).toBe(true)
+    await wrapper.find('.ix-rotate-left').trigger('click')
+    expect(wrapper.find('.ix-image-preview-img>img').attributes()['style']).toEqual(
+      'transform: scale3d(1, 1, 1) rotate(-90deg);',
+    )
+    await wrapper.find('.ix-rotate-right').trigger('click')
+
+    expect(wrapper.find('.ix-image-preview-img>img').attributes()['style']).toEqual(
+      'transform: scale3d(1, 1, 1) rotate(0deg);',
+    )
+    await wrapper.find('.ix-zoom-in').trigger('click')
+    expect(wrapper.find('.ix-image-preview-img>img').attributes()['style']).toEqual(
+      'transform: scale3d(1.1, 1.1, 1) rotate(0deg);',
+    )
+    await wrapper.find('.ix-zoom-out').trigger('click')
+    expect(wrapper.find('.ix-image-preview-img>img').attributes()['style']).toEqual(
+      'transform: scale3d(1, 1, 1) rotate(0deg);',
+    )
+
+    let i = 10
+    while (i) {
+      await wrapper.find('.ix-zoom-out').trigger('click')
+      i--
+    }
+    expect(wrapper.find('.ix-zoom-out').attributes()['class']).toEqual(
+      'ix-tools-item ix-zoom-out ix-tools-item-disabled',
+    )
+
+    await wrapper.find('.ix-close').trigger('click')
+    expect(wrapper.find('ix-image-preview').exists()).toBe(false)
+  })
+  test('render fallback work', async () => {
+    const fallback =
+      'data:image/png;base64,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'
+    const wrapper = ImageMount({
+      props: {
+        fallback,
+        src: '',
+      },
+    })
+    await wrapper.find('img').trigger('error')
+    await flushPromises()
+    expect(wrapper.find('.ix-image-error').isVisible()).toBe(true)
+  })
+})