diff --git a/package.json b/package.json index 9cd5fc2ac..87986af2f 100644 --- a/package.json +++ b/package.json @@ -151,6 +151,7 @@ "lint-staged": "^12.1.6", "markdown-it-fence": "^0.1.3", "mockdate": "^3.0.5", + "msw": "^1.0.0", "npm-run-all": "^4.1.5", "nyc": "^15.1.0", "postcss": "^8.3.11", diff --git a/src/_common b/src/_common index 89a2644f0..952aa1321 160000 --- a/src/_common +++ b/src/_common @@ -1 +1 @@ -Subproject commit 89a2644f0f9bda9d9966f6fb7dae11c678b13a13 +Subproject commit 952aa1321483892432a2a8ff1d0c43676c81fc7b diff --git a/src/common.ts b/src/common.ts index 703349082..57992dfe2 100644 --- a/src/common.ts +++ b/src/common.ts @@ -35,12 +35,16 @@ export interface UploadDisplayDragEvents { } export type ImageEvent = SyntheticEvent; -/** 通用全局类型 */ + +/** + * 通用全局类型 + * */ +export type PlainObject = { [key: string]: any }; export type OptionData = { label?: string; value?: string | number; -} & { [key: string]: any }; +} & PlainObject; export type TreeOptionData = { children?: Array>; @@ -50,7 +54,7 @@ export type TreeOptionData = { text?: string; /** option value */ value?: T; -} & { [key: string]: any }; +} & PlainObject; export type SizeEnum = 'small' | 'medium' | 'large'; diff --git a/src/guide/Guide.tsx b/src/guide/Guide.tsx index 615c87cb9..921415b24 100644 --- a/src/guide/Guide.tsx +++ b/src/guide/Guide.tsx @@ -336,7 +336,7 @@ const Guide = (props: GuideProps) => { current: innerCurrent, total: stepsTotal, }; - renderBody = React.cloneElement(content as any, contentProps); + renderBody = isFunction(content) ? content(contentProps) : content; } else { renderBody = renderPopupContent(); } diff --git a/src/guide/__tests__/__snapshots__/vitest-guide.test.jsx.snap b/src/guide/__tests__/__snapshots__/vitest-guide.test.jsx.snap index c529097de..e04059781 100644 --- a/src/guide/__tests__/__snapshots__/vitest-guide.test.jsx.snap +++ b/src/guide/__tests__/__snapshots__/vitest-guide.test.jsx.snap @@ -299,8 +299,6 @@ exports[`Guide Component > GuideStep.children works fine 1`] = ` > TNode @@ -434,8 +432,6 @@ exports[`Guide Component > GuideStep.content works fine 1`] = ` > TNode @@ -1331,1157 +1327,3 @@ exports[`Guide Component > GuideStep.title works fine 1`] = ` `; - -exports[`Guide Component > props.body works fine 1`] = ` - -
-
-
-
-
- Guide 用户引导 -
-
- 按钮用于开启一个闭环的操作任务,如“删除”对象、“购买”商品等。 -
-
-
-
- Label -
-
-
- -
-
-
-
-
- Label -
-
-
- -
-
-
-
- - -
-
-
-
-
-
-
-
-
-
-
-
-
- 新手引导标题 -
-
- - TNode - -
- -
-
-
-
- -`; - -exports[`Guide Component > props.children works fine 1`] = ` - -
-
-
-
-
- Guide 用户引导 -
-
- 按钮用于开启一个闭环的操作任务,如“删除”对象、“购买”商品等。 -
-
-
-
- Label -
-
-
- -
-
-
-
-
- Label -
-
-
- -
-
-
-
- - -
-
-
-
-
-
-
-
-
-
- - TNode - -
-
-
- -`; - -exports[`Guide Component > props.content works fine 1`] = ` - -
-
-
-
-
- Guide 用户引导 -
-
- 按钮用于开启一个闭环的操作任务,如“删除”对象、“购买”商品等。 -
-
-
-
- Label -
-
-
- -
-
-
-
-
- Label -
-
-
- -
-
-
-
- - -
-
-
-
-
-
-
-
-
-
- - TNode - -
-
-
- -`; - -exports[`Guide Component > props.mode is equal to dialog 1`] = ` -
-
-
-
-
- Guide 用户引导 -
-
- 按钮用于开启一个闭环的操作任务,如“删除”对象、“购买”商品等。 -
-
-
-
- Label -
-
-
- -
-
-
-
-
- Label -
-
-
- -
-
-
-
- - -
-
-
-
-`; - -exports[`Guide Component > props.mode is equal to popup 1`] = ` -
-
-
-
-
- Guide 用户引导 -
-
- 按钮用于开启一个闭环的操作任务,如“删除”对象、“购买”商品等。 -
-
-
-
- Label -
-
-
- -
-
-
-
-
- Label -
-
-
- -
-
-
-
- - -
-
-
-
-`; - -exports[`Guide Component > props.placement is equal to bottom-left 1`] = ` - -
-
-
-
-
- Guide 用户引导 -
-
- 按钮用于开启一个闭环的操作任务,如“删除”对象、“购买”商品等。 -
-
-
-
- Label -
-
-
- -
-
-
-
-
- Label -
-
-
- -
-
-
-
- - -
-
-
-
-
-
-
-
-
-
-
-
-
- 新手引导标题 -
-
- 新手引导的说明文案 -
- -
-
-
-
- -`; - -exports[`Guide Component > props.stepOverlayClass is equal to t-test-guide-step-overlay 1`] = ` - -
-
-
-
-
- Guide 用户引导 -
-
- 按钮用于开启一个闭环的操作任务,如“删除”对象、“购买”商品等。 -
-
-
-
- Label -
-
-
- -
-
-
-
-
- Label -
-
-
- -
-
-
-
- - -
-
-
-
-
-
-
-
-
-
-
-
-
- 新手引导标题 -
-
- 新手引导的说明文案 -
- -
-
-
-
- -`; - -exports[`Guide Component > props.title works fine 1`] = ` - -
-
-
-
-
- Guide 用户引导 -
-
- 按钮用于开启一个闭环的操作任务,如“删除”对象、“购买”商品等。 -
-
-
-
- Label -
-
-
- -
-
-
-
-
- Label -
-
-
- -
-
-
-
- - -
-
-
-
-
-
-
-
-
-
-
-
-
- - TNode - -
-
- 新手引导的说明文案 -
- -
-
-
-
- -`; diff --git a/src/guide/__tests__/vitest-guide.test.jsx b/src/guide/__tests__/vitest-guide.test.jsx index 8a6ef02f6..075549218 100644 --- a/src/guide/__tests__/vitest-guide.test.jsx +++ b/src/guide/__tests__/vitest-guide.test.jsx @@ -17,7 +17,7 @@ import { describe('Guide Component', () => { it('props.counter works fine', async () => { getGuideDefaultMount(Guide, { counter: TNode }); - await mockDelay(100); + await mockDelay(10); const tGuideCounterDom = document.querySelector('.t-guide__counter'); expect(tGuideCounterDom).toBeTruthy(); const customNodeDom = document.querySelector('.custom-node'); @@ -27,15 +27,15 @@ describe('Guide Component', () => { it('props.counter is a function with params', async () => { const fn = vi.fn(); getGuideDefaultMount(Guide, { counter: fn }); - await mockDelay(100); - expect(fn).toHaveBeenCalled(1); + await mockDelay(10); + expect(fn).toHaveBeenCalled(); expect(fn.mock.calls[0][0].total).toBe(1); expect(fn.mock.calls[0][0].current).toBe(0); }); it('props.current is equal 0', async () => { getGuideMultipleStepsMount(Guide, { current: 0 }); - await mockDelay(100); + await mockDelay(10); const tGuideCounterDom = document.querySelector('.t-guide__counter'); expect(tGuideCounterDom.textContent).toBe('1/3'); const tGuideTitleDom = document.querySelectorAll('.t-guide__title'); @@ -54,7 +54,7 @@ describe('Guide Component', () => { it('props.current is equal 1', async () => { getGuideMultipleStepsMount(Guide, { current: 1 }); - await mockDelay(100); + await mockDelay(10); const tGuideCounterDom = document.querySelector('.t-guide__counter'); expect(tGuideCounterDom.textContent).toBe('2/3'); const tGuideTitleDom = document.querySelectorAll('.t-guide__title'); @@ -73,7 +73,7 @@ describe('Guide Component', () => { it('props.current is equal 2', async () => { getGuideMultipleStepsMount(Guide, { current: 2 }); - await mockDelay(100); + await mockDelay(10); const tGuideCounterDom = document.querySelector('.t-guide__counter'); expect(tGuideCounterDom.textContent).toBe('3/3'); const tGuideTitleDom = document.querySelectorAll('.t-guide__title'); @@ -92,42 +92,42 @@ describe('Guide Component', () => { it('props.current is equal -1', async () => { getGuideMultipleStepsMount(Guide, { current: -1 }); - await mockDelay(100); + await mockDelay(10); const tGuideCounterDom = document.querySelector('.t-guide__counter'); expect(tGuideCounterDom).toBeFalsy(); }); it(`props.finishButtonProps is equal to {theme: 'warning'}`, async () => { getGuideMultipleStepsMount(Guide, { current: 2, finishButtonProps: { theme: 'warning' } }); - await mockDelay(100); + await mockDelay(10); const domWrapper = document.querySelector('.t-guide__finish'); expect(domWrapper).toHaveClass('t-button--theme-warning'); }); - it('props.hideCounter works fine. `${domInfoText}` should exist', async () => { + it('props.hideCounter works fine. `{"document.t-guide__counter":false}` should exist', async () => { getGuideDefaultMount(Guide, { hideCounter: true }); - await mockDelay(100); + await mockDelay(10); const tGuideCounterDom = document.querySelector('.t-guide__counter'); expect(tGuideCounterDom).toBeFalsy(); }); - it('props.hidePrev works fine. `${domInfoText}` should exist', async () => { + it('props.hidePrev works fine. `{"document.t-guide__action .t-guide__prev":false}` should exist', async () => { getGuideMultipleStepsMount(Guide, { current: 1, hidePrev: true }); - await mockDelay(100); + await mockDelay(10); const tGuideActionTGuidePrevDom = document.querySelector('.t-guide__action .t-guide__prev'); expect(tGuideActionTGuidePrevDom).toBeFalsy(); }); - it('props.hideSkip works fine. `${domInfoText}` should exist', async () => { + it('props.hideSkip works fine. `{"document.t-guide__action .t-guide__skip":false}` should exist', async () => { getGuideMultipleStepsMount(Guide, { current: 1, hideSkip: true }); - await mockDelay(100); + await mockDelay(10); const tGuideActionTGuideSkipDom = document.querySelector('.t-guide__action .t-guide__skip'); expect(tGuideActionTGuideSkipDom).toBeFalsy(); }); it(`props.highlightPadding is equal to 32`, async () => { getGuideDefaultMount(Guide, { highlightPadding: 32 }); - await mockDelay(100); + await mockDelay(10); const domWrapper = document.querySelector('.t-guide__highlight--mask'); expect(domWrapper.style.width).toBe('64px'); expect(domWrapper.style.height).toBe('64px'); @@ -142,42 +142,42 @@ describe('Guide Component', () => { it(`props.nextButtonProps is equal to {theme: 'warning'}`, async () => { getGuideMultipleStepsMount(Guide, { current: 1, nextButtonProps: { theme: 'warning' } }); - await mockDelay(100); + await mockDelay(10); const domWrapper = document.querySelector('.t-guide__next'); expect(domWrapper).toHaveClass('t-button--theme-warning'); }); it(`props.prevButtonProps is equal to {theme: 'warning'}`, async () => { getGuideMultipleStepsMount(Guide, { current: 2, prevButtonProps: { theme: 'warning' } }); - await mockDelay(100); + await mockDelay(10); const domWrapper = document.querySelector('.t-guide__prev'); expect(domWrapper).toHaveClass('t-button--theme-warning'); }); it('props.showOverlay is equal true', async () => { getGuideDefaultMount(Guide, { showOverlay: true }); - await mockDelay(100); + await mockDelay(10); const tGuideHighlightMaskDom = document.querySelectorAll('.t-guide__highlight--mask'); expect(tGuideHighlightMaskDom.length).toBe(1); }); it('props.showOverlay is equal false', async () => { getGuideDefaultMount(Guide, { showOverlay: false }); - await mockDelay(100); + await mockDelay(10); const tGuideHighlightMaskDom = document.querySelector('.t-guide__highlight--mask'); expect(tGuideHighlightMaskDom).toBeFalsy(); }); it(`props.skipButtonProps is equal to {theme: 'warning'}`, async () => { getGuideMultipleStepsMount(Guide, { current: 0, skipButtonProps: { theme: 'warning' } }); - await mockDelay(100); + await mockDelay(10); const domWrapper = document.querySelector('.t-guide__skip'); expect(domWrapper).toHaveClass('t-button--theme-warning'); }); it('props.steps works fine.', async () => { getGuideDefaultMount(Guide); - await mockDelay(100); + await mockDelay(10); const tGuideCounterDom = document.querySelector('.t-guide__counter'); expect(tGuideCounterDom.textContent).toBe('1/1'); const tGuideTitleDom = document.querySelectorAll('.t-guide__title'); @@ -196,7 +196,7 @@ describe('Guide Component', () => { it(`props.zIndex is equal to 5000`, async () => { getGuideDefaultMount(Guide, { zIndex: 5000 }); - await mockDelay(100); + await mockDelay(10); const domWrapper = document.querySelector('.t-guide__overlay'); expect(domWrapper.style.zIndex).toBe('4998'); const domWrapper1 = document.querySelector('.t-guide__highlight--mask'); @@ -206,9 +206,9 @@ describe('Guide Component', () => { it('events.change works fine', async () => { const onChangeFn = vi.fn(); getGuideMultipleStepsMount(Guide, { current: 0 }, { onChange: onChangeFn }); - await mockDelay(100); + await mockDelay(10); fireEvent.click(document.querySelector('.t-guide__next')); - expect(onChangeFn).toHaveBeenCalled(1); + expect(onChangeFn).toHaveBeenCalled(); expect(onChangeFn.mock.calls[0][0]).toBe(1); expect(onChangeFn.mock.calls[0][1].e.type).toBe('click'); expect(onChangeFn.mock.calls[0][1].total).toBe(3); @@ -216,9 +216,9 @@ describe('Guide Component', () => { it('events.change works fine', async () => { const onChangeFn = vi.fn(); getGuideMultipleStepsMount(Guide, { current: 1 }, { onChange: onChangeFn }); - await mockDelay(100); + await mockDelay(10); fireEvent.click(document.querySelector('.t-guide__prev')); - expect(onChangeFn).toHaveBeenCalled(1); + expect(onChangeFn).toHaveBeenCalled(); expect(onChangeFn.mock.calls[0][0]).toBe(0); expect(onChangeFn.mock.calls[0][1].e.type).toBe('click'); expect(onChangeFn.mock.calls[0][1].total).toBe(3); @@ -227,9 +227,9 @@ describe('Guide Component', () => { it('events.finish works fine', async () => { const onFinishFn = vi.fn(); getGuideMultipleStepsMount(Guide, { current: 2 }, { onFinish: onFinishFn }); - await mockDelay(100); + await mockDelay(10); fireEvent.click(document.querySelector('.t-guide__finish')); - expect(onFinishFn).toHaveBeenCalled(1); + expect(onFinishFn).toHaveBeenCalled(); expect(onFinishFn.mock.calls[0][0].current).toBe(2); expect(onFinishFn.mock.calls[0][0].e.type).toBe('click'); expect(onFinishFn.mock.calls[0][0].total).toBe(3); @@ -238,9 +238,9 @@ describe('Guide Component', () => { it('events.nextStepClick works fine', async () => { const onNextStepClickFn = vi.fn(); getGuideMultipleStepsMount(Guide, { current: 1 }, { onNextStepClick: onNextStepClickFn }); - await mockDelay(100); + await mockDelay(10); fireEvent.click(document.querySelector('.t-guide__next')); - expect(onNextStepClickFn).toHaveBeenCalled(1); + expect(onNextStepClickFn).toHaveBeenCalled(); expect(onNextStepClickFn.mock.calls[0][0].current).toBe(1); expect(onNextStepClickFn.mock.calls[0][0].next).toBe(2); expect(onNextStepClickFn.mock.calls[0][0].e.type).toBe('click'); @@ -250,9 +250,9 @@ describe('Guide Component', () => { it('events.prevStepClick works fine', async () => { const onPrevStepClickFn = vi.fn(); getGuideMultipleStepsMount(Guide, { current: 1 }, { onPrevStepClick: onPrevStepClickFn }); - await mockDelay(100); + await mockDelay(10); fireEvent.click(document.querySelector('.t-guide__prev')); - expect(onPrevStepClickFn).toHaveBeenCalled(1); + expect(onPrevStepClickFn).toHaveBeenCalled(); expect(onPrevStepClickFn.mock.calls[0][0].current).toBe(1); expect(onPrevStepClickFn.mock.calls[0][0].prev).toBe(0); expect(onPrevStepClickFn.mock.calls[0][0].e.type).toBe('click'); @@ -262,9 +262,9 @@ describe('Guide Component', () => { it('events.skip works fine', async () => { const onSkipFn = vi.fn(); getGuideMultipleStepsMount(Guide, { current: 0 }, { onSkip: onSkipFn }); - await mockDelay(100); + await mockDelay(10); fireEvent.click(document.querySelector('.t-guide__skip')); - expect(onSkipFn).toHaveBeenCalled(1); + expect(onSkipFn).toHaveBeenCalled(); expect(onSkipFn.mock.calls[0][0].current).toBe(0); expect(onSkipFn.mock.calls[0][0].e.type).toBe('click'); expect(onSkipFn.mock.calls[0][0].total).toBe(3); @@ -274,7 +274,7 @@ describe('Guide Component', () => { describe('Guide Component', () => { it('GuideStep.body works fine', async () => { getCustomGuideStepMount(Guide, { body: TNode }); - await mockDelay(100); + await mockDelay(10); const customNodeDom = document.querySelector('.custom-node'); expect(customNodeDom).toBeTruthy(); expect(document.body).toMatchSnapshot(); @@ -282,7 +282,7 @@ describe('Guide Component', () => { it('GuideStep.children works fine', async () => { getCustomGuideStepMount(Guide, { children: TNode }); - await mockDelay(100); + await mockDelay(10); const customNodeDom = document.querySelector('.custom-node'); expect(customNodeDom).toBeTruthy(); expect(document.body).toMatchSnapshot(); @@ -290,7 +290,7 @@ describe('Guide Component', () => { it('GuideStep.content works fine', async () => { getCustomGuideStepMount(Guide, { content: TNode }); - await mockDelay(100); + await mockDelay(10); const customNodeDom = document.querySelector('.custom-node'); expect(customNodeDom).toBeTruthy(); expect(document.body).toMatchSnapshot(); @@ -298,7 +298,7 @@ describe('Guide Component', () => { it('GuideStep.highlightContent works fine', async () => { getCustomGuideStepMount(Guide, { highlightContent: TNode }); - await mockDelay(100); + await mockDelay(10); const customNodeDom = document.querySelector('.custom-node'); expect(customNodeDom).toBeTruthy(); expect(document.body).toMatchSnapshot(); @@ -306,7 +306,7 @@ describe('Guide Component', () => { it(`GuideStep.highlightPadding is equal to 32`, async () => { getCustomGuideStepMount(Guide, { highlightPadding: 32 }); - await mockDelay(100); + await mockDelay(10); const domWrapper = document.querySelector('.t-guide__highlight--mask'); expect(domWrapper.style.width).toBe('64px'); expect(domWrapper.style.height).toBe('64px'); @@ -323,7 +323,7 @@ describe('Guide Component', () => { ['popup', 'dialog'].forEach((item, index) => { it(`GuideStep.mode is equal to ${item}`, async () => { const { container } = getCustomGuideStepMount(Guide, { mode: item }); - await mockDelay(100); + await mockDelay(10); const modeExpectedDomIndexDom = document.querySelector(modeExpectedDom[index]); expect(modeExpectedDomIndexDom).toBeTruthy(); expect(container).toMatchSnapshot(); @@ -332,14 +332,14 @@ describe('Guide Component', () => { it(`GuideStep.nextButtonProps is equal to {theme: 'warning'}`, async () => { getCustomMultipleGuideStepMount(Guide, { current: 1, nextButtonProps: { theme: 'warning' } }); - await mockDelay(100); + await mockDelay(10); const domWrapper = document.querySelector('.t-guide__next'); expect(domWrapper).toHaveClass('t-button--theme-warning'); }); it(`GuideStep.placement is equal to bottom-left`, async () => { getCustomGuideStepMount(Guide, { placement: 'bottom-left' }); - await mockDelay(100); + await mockDelay(10); const domWrapper = document.querySelector('.t-popup'); expect(domWrapper.getAttribute('data-popper-placement')).toBe('bottom-start'); expect(document.body).toMatchSnapshot(); @@ -347,42 +347,42 @@ describe('Guide Component', () => { it(`GuideStep.popupProps is equal to {placement: 'top-left'}`, async () => { getCustomGuideStepMount(Guide, { popupProps: { placement: 'top-left' } }); - await mockDelay(100); + await mockDelay(10); const domWrapper = document.querySelector('.t-popup'); expect(domWrapper.getAttribute('data-popper-placement')).toBe('top-start'); }); it(`GuideStep.prevButtonProps is equal to {theme: 'warning'}`, async () => { getCustomMultipleGuideStepMount(Guide, { current: 2, prevButtonProps: { theme: 'warning' } }); - await mockDelay(100); + await mockDelay(10); const domWrapper = document.querySelector('.t-guide__prev'); expect(domWrapper).toHaveClass('t-button--theme-warning'); }); it('GuideStep.showOverlay is equal true', async () => { getCustomMultipleGuideStepMount(Guide, { showOverlay: true }); - await mockDelay(100); + await mockDelay(10); const tGuideHighlightMaskDom = document.querySelectorAll('.t-guide__highlight--mask'); expect(tGuideHighlightMaskDom.length).toBe(1); }); it('GuideStep.showOverlay is equal false', async () => { getCustomMultipleGuideStepMount(Guide, { showOverlay: false }); - await mockDelay(100); + await mockDelay(10); const tGuideHighlightMaskDom = document.querySelector('.t-guide__highlight--mask'); expect(tGuideHighlightMaskDom).toBeFalsy(); }); it(`GuideStep.skipButtonProps is equal to {theme: 'warning'}`, async () => { getCustomMultipleGuideStepMount(Guide, { current: 1, skipButtonProps: { theme: 'warning' } }); - await mockDelay(100); + await mockDelay(10); const domWrapper = document.querySelector('.t-guide__skip'); expect(domWrapper).toHaveClass('t-button--theme-warning'); }); it(`GuideStep.stepOverlayClass is equal to t-test-guide-step-overlay`, async () => { getCustomGuideStepMount(Guide, { stepOverlayClass: 't-test-guide-step-overlay' }); - await mockDelay(100); + await mockDelay(10); const domWrapper = document.querySelector('.t-popup'); expect(domWrapper).toHaveClass('t-test-guide-step-overlay'); expect(document.body).toMatchSnapshot(); @@ -390,7 +390,7 @@ describe('Guide Component', () => { it('GuideStep.title works fine', async () => { getCustomGuideStepMount(Guide, { title: TNode }); - await mockDelay(100); + await mockDelay(10); const customNodeDom = document.querySelector('.custom-node'); expect(customNodeDom).toBeTruthy(); expect(document.body).toMatchSnapshot(); diff --git a/src/guide/_example/base.jsx b/src/guide/_example/base.jsx index 7a5ea3a28..f113fe573 100644 --- a/src/guide/_example/base.jsx +++ b/src/guide/_example/base.jsx @@ -55,6 +55,7 @@ export default function BasicGuide() { const steps = [ { element: '.main-title-base', + title: '新手引导标题', body: '新手引导的说明文案', placement: 'bottom-right', stepOverlayClass: 't-test-guide-step-overlay' @@ -142,7 +143,6 @@ export default function BasicGuide() { props.overlayContent works fine 1`] = `
- 图片加载中 -
+ />
props.placeholder works fine 1`] = `
- 图片加载中 -
+ />
diff --git a/src/upload/__tests__/__snapshots__/vitest-upload.test.jsx.snap b/src/upload/__tests__/__snapshots__/vitest-upload.test.jsx.snap new file mode 100644 index 000000000..930421f1c --- /dev/null +++ b/src/upload/__tests__/__snapshots__/vitest-upload.test.jsx.snap @@ -0,0 +1,1132 @@ +// Vitest Snapshot v1 + +exports[`Upload Component > props.draggable: theme=image & draggable=true, fail file render fine 1`] = ` +
+
+ +
+
+
+ +
+
+
+ + image4.png + + + + +
+ + 文件大小 + : + + + 上传日期 + : + - + +
+
+ + +
+
+
+
+
+
+`; + +exports[`Upload Component > props.draggable: theme=image & draggable=true, progress file render fine 1`] = ` +
+
+ +
+
+
+ +
+
+
+ + image2.png + +
+
+ + +
+ + +
+ + 80 + % + + + + + 文件大小 + : + + + 上传日期 + : + - + +
+ +
+ + + + + +`; + +exports[`Upload Component > props.draggable: theme=image & draggable=true, success file render fine 1`] = ` +
+
+ +
+
+
+ +
+
+
+ + image1.png + + + + +
+ + 文件大小 + : + + + 上传日期 + : + - + +
+
+ + +
+
+
+
+
+
+`; + +exports[`Upload Component > props.draggable: theme=image & draggable=true, success file render fine with file.response.url 1`] = ` +
+
+ +
+
+
+ +
+
+
+ + image1.png + + + + +
+ + 文件大小 + : + + + 上传日期 + : + - + +
+
+ + +
+
+
+
+
+
+`; + +exports[`Upload Component > props.draggable: theme=image & draggable=true, waiting file render fine 1`] = ` +
+
+ +
+
+
+ +
+
+
+ + image3.png + +
+ + 文件大小 + : + + + 上传日期 + : + - + +
+ +
+
+
+
+
+
+`; + +exports[`Upload Component > props.theme: theme=file-flow works fine 1`] = ` +
+
+ +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 文件名 + + 文件大小 + + 状态 + + 操作 +
+ file1.txt + + +
+ + + + + 上传成功 + +
+
+ +
+ file2.txt + + +
+ + + + + 待上传 + +
+
+ +
+ file3.txt + + +
+ + + + + 上传失败 + +
+
+ +
+ file4.txt + + +
+
+ + +
+ + +
+ + 上传中 + 90% + + +
+ +
+
+
+
+`; + +exports[`Upload Component > props.theme: theme=image-flow works fine 1`] = ` +
+
+ +
+
+
+ +
+
+
+
    +
  • +
    + +
    + + + + + + +
    +
    +

    + img.txt +

    +
  • +
  • +
    + +
    + + + + + + + + + + + + + +
    +
    +

    + img1.txt +

    +
  • +
  • +
    + +
    + + + + + + + + + + + + + +
    +
    +

    + img2.txt +

    +
  • +
  • +
    +
    + + + +

    + 上传失败 +

    +
    +
    + + + + + + + + + + + + + +
    +
    +

    + img3.txt +

    +
  • +
  • +
    +
    +
    + + +
    + + +
    +

    + 上传中 +

    + +
    + + + + + + + + + + + + + +
    + +

    + img4.txt +

    + + + + + + +`; diff --git a/src/upload/__tests__/request/index.js b/src/upload/__tests__/request/index.js new file mode 100644 index 000000000..5f1e87c01 --- /dev/null +++ b/src/upload/__tests__/request/index.js @@ -0,0 +1,37 @@ +import { setupServer } from 'msw/node'; +import { rest } from 'msw'; + +export function getUploadServer() { + return setupServer(...[ + // mock file upload success + rest.post('https://tdesign.test.com/upload/file_success', (req, res, ctx) => { + return res(ctx.status(200), ctx.json({ + ret: 0, + data: { + name: 'tdesign.min.js', + url: 'https://tdesign.gtimg.com/site/spline/script/tdesign.min.js' + } + })) + }), + // mock image upload success + rest.post('https://tdesign.test.com/upload/image_success', (req, res, ctx) => { + return res(ctx.status(200), ctx.json({ + code: 200, + name: 'demo-image-1.png', + url: 'https://tdesign.gtimg.com/demo/demo-image-1.png' + })) + }), + // mock upload failed on status + rest.post('https://tdesign.test.com/upload/fail/status_error', (req, res, ctx) => { + return res(ctx.status(500), ctx.json({})) + }), + // mock upload failed in response + rest.post('https://tdesign.test.com/upload/fail/response_error', (req, res, ctx) => { + return res(ctx.status(200), ctx.json({ + code: 1001, + name: 'file-name.txt', + error: 'upload failed' + })) + }), + ]) +} diff --git a/src/upload/__tests__/upload.test.tsx b/src/upload/__tests__/upload.test.tsx deleted file mode 100644 index a36c0df96..000000000 --- a/src/upload/__tests__/upload.test.tsx +++ /dev/null @@ -1,13 +0,0 @@ -// import { render } from '@test/utils'; -// import React from 'react'; -import MockDate from 'mockdate'; -// import Upload from '../index'; - -MockDate.set('2022-08-27'); - -// TODO -describe('Upload 组件测试', () => { - test('dom', () => { - expect(true).toBe(true); - }); -}); diff --git a/src/upload/__tests__/vitest-upload.test.jsx b/src/upload/__tests__/vitest-upload.test.jsx new file mode 100644 index 000000000..9bd38896b --- /dev/null +++ b/src/upload/__tests__/vitest-upload.test.jsx @@ -0,0 +1,1746 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/** + * 该文件由脚本自动生成,如需修改请联系 PMC + * This file generated by scripts of tdesign-api. `npm run api:docs Upload React(PC) vitest,finalProject` + * If you need to modify this file, contact PMC first please. + */ +import React from 'react'; +import { + fireEvent, + vi, + render, + mockDelay, + simulateFileChange, + getFakeFileList, + simulateDragFileChange, +} from '@test/utils'; +import { Upload } from '..'; +import { getUploadServer } from './request'; + +describe('Upload Component', () => { + const server = getUploadServer(); + + beforeAll(() => { + server.listen({ onUnhandledRequest: 'error' }); + }); + + afterEach(() => { + server.resetHandlers(); + }); + + afterAll(() => { + server.close(); + }); + + it('props.abridgeName: props.abridgeName works fine if theme=file-input', () => { + const { container } = render( + , + ); + expect(container.querySelector('.t-upload__single-input-text').textContent).toBe('this_is_…me.png'); + }); + + it('props.abridgeName: props.abridgeName works fine if theme=file and file url exists', () => { + const { container } = render( + , + ); + expect(container.querySelector('.t-upload__single-name').textContent).toBe('this_is_…me.png'); + }); + + it('props.abridgeName: props.abridgeName works fine if theme=file and file url does not exist', () => { + const { container } = render( + , + ); + expect(container.querySelector('.t-upload__single-name').textContent).toBe('this_is_…me.png'); + }); + + it('props.abridgeName: props.abridgeName works fine if theme=image', () => { + const { container } = render( + , + ); + expect(container.querySelector('.t-upload__card-name').textContent).toBe('this_is_…me.png'); + }); + + it('props.abridgeName: props.abridgeName works fine if theme=file&draggable=true', () => { + const { container } = render( + , + ); + expect(container.querySelector('.t-upload__single-name').textContent).toBe('this_is_…me.png'); + }); + + it('props.abridgeName: props.abridgeName works fine if theme=image&draggable=true', () => { + const { container } = render( + , + ); + expect(container.querySelector('.t-upload__single-name').textContent).toBe('this_is_…me.png'); + }); + + it('props.abridgeName: props.abridgeName works fine if theme=image-flow', () => { + const { container } = render( + , + ); + expect(container.querySelector('.t-upload__card-name').textContent).toBe('this_is_…me.jpg'); + }); + + it('props.abridgeName: props.abridgeName works fine if theme=file-flow and file url exists', () => { + const { container } = render( + , + ); + expect(container.querySelector('.t-upload__file-name > a').textContent).toBe('this_is_…me.jpg'); + }); + + it('props.abridgeName: props.abridgeName works fine if theme=file-flow and file url does not exist', () => { + const { container } = render( + , + ); + expect(container.querySelector('.t-upload__file-name').textContent).toBe('this_is_…me.jpg'); + }); + + it('props.accept works fine', () => { + const wrapper = render(); + const container = wrapper.container.querySelector('input'); + expect(container.getAttribute('accept')).toBe('image/*'); + }); + + it('props.action works fine', async () => { + const onSelectChangeFn = vi.fn(); + const onChangeFn = vi.fn(); + const { container } = render( + , + ); + const inputDom = container.querySelector('input'); + const fileList = simulateFileChange(inputDom); + await mockDelay(); + expect(onSelectChangeFn).toHaveBeenCalled(); + expect(onSelectChangeFn.mock.calls[0][0]).toEqual(fileList); + expect(onSelectChangeFn.mock.calls[0][1].currentSelectedFiles).toEqual([ + { + lastModified: 1674355700444, + name: 'file-name.txt', + percent: 0, + raw: fileList[0], + size: 22, + type: 'image/png', + status: undefined, + }, + ]); + expect(onChangeFn).toHaveBeenCalled(); + expect(onChangeFn.mock.calls[0][0][0].lastModified).toBe(1674355700444); + expect(onChangeFn.mock.calls[0][0][0].response).toBeTruthy(); + expect(onChangeFn.mock.calls[0][0][0].name).toBe('file-name.txt'); + expect(onChangeFn.mock.calls[0][0][0].percent).toBe(100); + expect(onChangeFn.mock.calls[0][0][0].status).toBe('success'); + expect(onChangeFn.mock.calls[0][0][0].raw).toEqual(fileList[0]); + expect(onChangeFn.mock.calls[0][0][0].uploadTime).toBeTruthy(); + expect(onChangeFn.mock.calls[0][1].trigger).toBe('add'); + expect(onChangeFn.mock.calls[0][1].file.raw).toEqual(fileList[0]); + expect(onChangeFn.mock.calls[0][1].file.url).toBe('https://tdesign.gtimg.com/demo/demo-image-1.png'); + expect(onChangeFn.mock.calls[0][1].file.name).toBe('file-name.txt'); + expect(onChangeFn.mock.calls[0][1].file.uploadTime).toBeTruthy(); + expect(onChangeFn.mock.calls[0][1].file.response).toBeTruthy(); + }); + + it('props.allowUploadDuplicateFile: allowUploadDuplicateFile is equal to false', async () => { + const onValidateFn = vi.fn(); + const { container } = render( + , + ); + const inputDom = container.querySelector('input'); + const fileList = simulateFileChange(inputDom); + await mockDelay(); + expect(onValidateFn).toHaveBeenCalled(); + expect(onValidateFn.mock.calls[0][0].type).toBe('FILTER_FILE_SAME_NAME'); + expect(onValidateFn.mock.calls[0][0].files[0].raw).toEqual(fileList[0]); + }); + it('props.allowUploadDuplicateFile: allowUploadDuplicateFile is equal to true', async () => { + const onValidateFn = vi.fn(); + const { container } = render( + , + ); + const inputDom = container.querySelector('input'); + simulateFileChange(inputDom); + await mockDelay(); + expect(onValidateFn).not.toHaveBeenCalled(); + }); + + it('props.autoUpload: autoUpload is equal false', async () => { + const onChangeFn = vi.fn(); + const { container } = render( + , + ); + const inputDom = container.querySelector('input'); + const fileList = simulateFileChange(inputDom); + await mockDelay(); + expect(onChangeFn).toHaveBeenCalled(); + expect(onChangeFn.mock.calls[0][0][0].response).toBe(undefined); + expect(onChangeFn.mock.calls[0][0][0].raw).toEqual(fileList[0]); + expect(onChangeFn.mock.calls[0][0][0].name).toBe('file-name.txt'); + expect(onChangeFn.mock.calls[0][0][0].status).toBe('waiting'); + expect(onChangeFn.mock.calls[0][0][0].percent).toBe(0); + }); + it('props.autoUpload: autoUpload=false & theme=file-flow, cancel upload works fine', () => { + const onChangeFn1 = vi.fn(); + const onRemoveFn1 = vi.fn(); + const { container } = render( + , + ); + fireEvent.click(container.querySelector('.t-upload__continue')); + fireEvent.click(container.querySelector('.t-upload__cancel')); + expect(onChangeFn1).toHaveBeenCalled(); + expect(onChangeFn1.mock.calls[0][0]).toEqual([ + { name: 'file1.txt', status: 'waiting', uploadTime: '2023-01-27', lastModified: 1674830942522 }, + { name: 'file2.txt', status: 'success', uploadTime: '2023-01-27', lastModified: 1674831204354 }, + { name: 'file3.txt', status: 'waiting', uploadTime: '2023-01-27', lastModified: 1674831204354 }, + ]); + expect(onChangeFn1.mock.calls[0][1].trigger).toBe('abort'); + expect(onRemoveFn1).not.toHaveBeenCalled(); + }); + it('props.autoUpload: autoUpload=false & theme=image & draggable = true, cancel upload works fine', async () => { + const onSuccessFn = vi.fn(); + const { container } = render( + , + ); + fireEvent.click(container.querySelector('.t-upload__dragger-upload-btn')); + await mockDelay(); + expect(onSuccessFn).toHaveBeenCalled(); + expect(onSuccessFn.mock.calls[0][0].fileList[0].url).toBe('https://tdesign.gtimg.com/demo/demo-image-1.png'); + expect(onSuccessFn.mock.calls[0][0].currentFiles[0].url).toBe('https://tdesign.gtimg.com/demo/demo-image-1.png'); + expect(onSuccessFn.mock.calls[0][0].file.url).toBe('https://tdesign.gtimg.com/demo/demo-image-1.png'); + expect(onSuccessFn.mock.calls[0][0].results).toBe(undefined); + expect(onSuccessFn.mock.calls[0][0].response).toBeTruthy(); + expect(onSuccessFn.mock.calls[0][0].XMLHttpRequest).toBeTruthy(); + }); + + it('props.beforeAllFilesUpload: beforeAllFilesUpload can stop uploading', async () => { + const onChangeFn = vi.fn(); + const onValidateFn = vi.fn(); + const { container } = render( + false} + action="https://tdesign.test.com/upload/file_success" + onChange={onChangeFn} + onValidate={onValidateFn} + >, + ); + const inputDom = container.querySelector('input'); + const fileList = simulateFileChange(inputDom, 'file', 3); + await mockDelay(); + expect(onChangeFn).not.toHaveBeenCalled(); + expect(onValidateFn).toHaveBeenCalled(); + expect(onValidateFn.mock.calls[0][0].type).toBe('BEFORE_ALL_FILES_UPLOAD'); + expect(onValidateFn.mock.calls[0][0].files.map((t) => t.raw)).toEqual(fileList); + }); + + it('props.beforeUpload: beforeUpload can skip all files to upload, just like beforeAllFilesUpload', async () => { + const onChangeFn = vi.fn(); + const onValidateFn = vi.fn(); + const { container } = render( + false} + action="https://tdesign.test.com/upload/file_success" + onChange={onChangeFn} + onValidate={onValidateFn} + >, + ); + const inputDom = container.querySelector('input'); + const fileList = simulateFileChange(inputDom, 'file', 3); + await mockDelay(); + expect(onChangeFn).not.toHaveBeenCalled(); + expect(onValidateFn).toHaveBeenCalled(); + expect(onValidateFn.mock.calls[0][0].type).toBe('CUSTOM_BEFORE_UPLOAD'); + expect(onValidateFn.mock.calls[0][0].files.map((t) => t.raw)).toEqual(fileList); + }); + it('props.beforeUpload: beforeUpload can skip some of files to upload', async () => { + const onChangeFn = vi.fn(); + const onValidateFn = vi.fn(); + const { container } = render( + file.name === 'file-name1.txt'} + action="https://tdesign.test.com/upload/file_success" + onChange={onChangeFn} + onValidate={onValidateFn} + >, + ); + const inputDom = container.querySelector('input'); + const fileList = simulateFileChange(inputDom, 'file', 3); + await mockDelay(); + expect(onChangeFn).toHaveBeenCalled(); + expect(onChangeFn.mock.calls[0][0][0].raw).toEqual(fileList[1]); + expect(onValidateFn).toHaveBeenCalled(); + expect(onValidateFn.mock.calls[0][0].type).toBe('CUSTOM_BEFORE_UPLOAD'); + expect(onValidateFn.mock.calls[0][0].files.map((t) => t.raw)).toEqual([fileList[0], fileList[2]]); + }); + + it('props.children: children works fine if theme = file', () => { + const { container } = render( + + TNode + , + ); + expect(container.querySelector('.custom-node')).toBeTruthy(); + }); + + it('props.children: children works fine if theme = custom', () => { + const { container } = render( + + TNode + , + ); + expect(container.querySelector('.custom-node')).toBeTruthy(); + }); + + it('props.children: children works fine if theme = custom & draggable=true', () => { + const { container } = render( + + TNode + , + ); + expect(container.querySelector('.custom-node')).toBeTruthy(); + }); + + it('props.children is a function with params, props.children: children works fine if theme = custom & draggable=true', () => { + const fn = vi.fn(); + render( + , + ); + expect(fn).toHaveBeenCalled(); + expect(fn.mock.calls[0][0].dragActive).toBe(false); + expect(fn.mock.calls[0][0].files).toEqual([]); + }); + + it('props.data: upload request can send extra data', async () => { + const onFailFn = vi.fn(); + const { container } = render( + , + ); + const inputDom = container.querySelector('input'); + const fileList = simulateFileChange(inputDom); + await mockDelay(700); + expect(onFailFn).toHaveBeenCalled(); + expect(onFailFn.mock.calls[0][0].XMLHttpRequest.upload.requestParams).toEqual({ + file_name: 'custom-file-name.excel', + file: fileList[0], + length: 1, + }); + }); + + it('props.disabled works fine. `".t-input.t-is-disabled"` should exist', () => { + const { container } = render(); + expect(container.querySelector('.t-input.t-is-disabled')).toBeTruthy(); + }); + + it('props.disabled works fine. `".t-upload__trigger .t-button.t-is-disabled"` should exist', () => { + const { container } = render(); + expect(container.querySelector('.t-upload__trigger .t-button.t-is-disabled')).toBeTruthy(); + }); + + it('props.disabled works fine. `{".t-upload__delete":false}` should exist', () => { + const { container } = render( + , + ); + expect(container.querySelector('.t-upload__delete')).toBeFalsy(); + }); + + it('props.disabled works fine. `{".t-upload__delete":false}` should exist', () => { + const { container } = render( + , + ); + expect(container.querySelector('.t-upload__delete')).toBeFalsy(); + }); + + it('props.disabled: disabled upload can not trigger onSelectChange', () => { + const onSelectChangeFn = vi.fn(); + const { container } = render(); + const inputDom = container.querySelector('input'); + simulateFileChange(inputDom); + expect(onSelectChangeFn).not.toHaveBeenCalled(); + }); + it('props.disabled: disabled upload can not remove file', () => { + const { container } = render(); + expect(container.querySelector('.t-upload__icon-delete')).toBeFalsy(); + }); + it('props.disabled: disabled upload can not remove image', () => { + const { container } = render( + , + ); + expect(container.querySelector('.t-upload__icon-delete')).toBeFalsy(); + }); + + it('props.dragContent works fine', () => { + const { container } = render( + TNode} + theme="custom" + draggable={true} + action="https://tdesign.test.com/upload/file_success" + >, + ); + expect(container.querySelector('.custom-node')).toBeTruthy(); + }); + + it('props.draggable: theme=image & draggable=true, success file render fine', () => { + const { container } = render( + , + ); + expect(container.querySelectorAll('.t-icon-check-circle-filled').length).toBe(1); + const attrDom = document.querySelector('.t-upload__dragger-img-wrap img'); + expect(attrDom.getAttribute('src')).toBe('https://tdesign.gtimg.com/demo/demo-image-1.png'); + expect(container).toMatchSnapshot(); + }); + + it('props.draggable: theme=image & draggable=true, success file render fine with file.response.url', () => { + const { container } = render( + , + ); + expect(container.querySelectorAll('.t-icon-check-circle-filled').length).toBe(1); + const attrDom = document.querySelector('.t-upload__dragger-img-wrap img'); + expect(attrDom.getAttribute('src')).toBe('https://tdesign.gtimg.com/demo/demo-image-1.png'); + expect(container).toMatchSnapshot(); + }); + + it('props.draggable: theme=image & draggable=true, fail file render fine', () => { + const { container } = render( + , + ); + expect(container.querySelectorAll('.t-icon-error-circle-filled').length).toBe(1); + expect(container).toMatchSnapshot(); + }); + + it('props.draggable: theme=image & draggable=true, progress file render fine', () => { + const { container } = render( + , + ); + expect(container.querySelector('.t-upload__single-percent').textContent).toBe('80%'); + expect(container).toMatchSnapshot(); + }); + + it('props.draggable: theme=image & draggable=true, waiting file render fine', () => { + const { container } = render( + , + ); + expect(container.querySelectorAll('.t-upload__dragger-progress-cancel').length).toBe(1); + expect(container).toMatchSnapshot(); + }); + + it('props.draggable: theme=image & draggable=true & autoUpload=false, waiting file render fine', () => { + const { container } = render( + , + ); + expect(container.querySelectorAll('.t-upload__dragger-progress-cancel').length).toBe(1); + }); + + it('props.draggable: theme=image & draggable=true & autoUpload=false, cancel upload works fine', () => { + const onCancelUploadFn = vi.fn(); + const onRemoveFn = vi.fn(); + const { container } = render( + , + ); + fireEvent.click(container.querySelector('.t-upload__dragger-progress-cancel')); + expect(onCancelUploadFn).toHaveBeenCalled(); + expect(onRemoveFn).toHaveBeenCalled(); + expect(onRemoveFn.mock.calls[0][0].file).toEqual({ + url: 'https://image3.png', + name: 'image3.png', + status: 'waiting', + }); + expect(onRemoveFn.mock.calls[0][0].e.type).toBe('click'); + expect(onRemoveFn.mock.calls[0][0].index).toBe(0); + }); + + it('props.fileListDisplay: theme=file, fileListDisplay works fine', () => { + const fileList = getFakeFileList('file', 3); + const { container } = render( + TNode} + files={fileList} + theme="file" + action="https://tdesign.test.com/upload/file_success" + >, + ); + expect(container.querySelector('.custom-node')).toBeTruthy(); + }); + + it('props.fileListDisplay is a function with params, props.fileListDisplay: theme=file, fileListDisplay works fine', () => { + const fileList = getFakeFileList('file', 3); + const fn = vi.fn(); + render( + , + ); + expect(fn).toHaveBeenCalled(); + expect(fn.mock.calls[0][0].files).toEqual(fileList); + }); + + it('props.fileListDisplay: theme=image-flow && multiple=true && draggable=true, fileListDisplay works fine', () => { + const fileList = [{ url: 'https://tdesign.gtimg.com/demo/demo-image-1.png' }]; + const { container } = render( + TNode} + files={fileList} + theme="image-flow" + multiple={true} + draggable={true} + action="https://tdesign.test.com/upload/file_success" + >, + ); + expect(container.querySelector('.custom-node')).toBeTruthy(); + }); + + it('props.fileListDisplay is a function with params, props.fileListDisplay: theme=image-flow && multiple=true && draggable=true, fileListDisplay works fine', () => { + const fileList = [{ url: 'https://tdesign.gtimg.com/demo/demo-image-1.png' }]; + const fn = vi.fn(); + render( + , + ); + expect(fn).toHaveBeenCalled(); + expect(fn.mock.calls[0][0].files).toEqual(fileList); + }); + + it('props.fileListDisplay: theme=file-flow && multiple=true && draggable=true, fileListDisplay works fine', () => { + const fileList = [{ url: 'https://tdesign.gtimg.com/demo/demo-image-1.png' }]; + const { container } = render( + TNode} + files={fileList} + theme="file-flow" + multiple={true} + draggable={true} + action="https://tdesign.test.com/upload/file_success" + >, + ); + expect(container.querySelector('.custom-node')).toBeTruthy(); + }); + + it('props.fileListDisplay is a function with params, props.fileListDisplay: theme=file-flow && multiple=true && draggable=true, fileListDisplay works fine', () => { + const fileList = [{ url: 'https://tdesign.gtimg.com/demo/demo-image-1.png' }]; + const fn = vi.fn(); + render( + , + ); + expect(fn).toHaveBeenCalled(); + expect(fn.mock.calls[0][0].files).toEqual(fileList); + }); + + it('props.fileListDisplay: theme=file && draggable=true, fileListDisplay works fine', () => { + const { container } = render( + TNode} + theme="file" + draggable={true} + files={[{ name: 'file1.txt', status: 'waiting', uploadTime: 1674897038406 }]} + >, + ); + expect(container.querySelector('.custom-node')).toBeTruthy(); + }); + + it('props.fileListDisplay is a function with params, props.fileListDisplay: theme=file && draggable=true, fileListDisplay works fine', () => { + const fn = vi.fn(); + render( + , + ); + expect(fn).toHaveBeenCalled(); + expect(fn.mock.calls[0][0].files).toEqual([{ name: 'file1.txt', status: 'waiting', uploadTime: 1674897038406 }]); + }); + + it('props.fileListDisplay: theme=image && draggable=true, fileListDisplay works fine', () => { + const { container } = render( + TNode} + theme="image" + draggable={true} + files={[{ url: 'https://img1.txt', status: 'waiting', uploadTime: 1674897038406 }]} + >, + ); + expect(container.querySelector('.custom-node')).toBeTruthy(); + }); + + it('props.fileListDisplay is a function with params, props.fileListDisplay: theme=image && draggable=true, fileListDisplay works fine', () => { + const fn = vi.fn(); + render( + , + ); + expect(fn).toHaveBeenCalled(); + expect(fn.mock.calls[0][0].files).toEqual([ + { url: 'https://img1.txt', status: 'waiting', uploadTime: 1674897038406 }, + ]); + }); + + it('props.format works fine', () => { + const onSelectChangeFn = vi.fn(); + const { container } = render( + ({ field_custom: 'a new file field', name: 'another name', raw: fileRaw })} + action="https://tdesign.test.com/upload/file_success" + onSelectChange={onSelectChangeFn} + >, + ); + const inputDom = container.querySelector('input'); + const fileList = simulateFileChange(inputDom); + expect(onSelectChangeFn).toHaveBeenCalled(); + expect(onSelectChangeFn.mock.calls[0][0]).toEqual(fileList); + expect(onSelectChangeFn.mock.calls[0][1].currentSelectedFiles[0].name).toBe('another name'); + expect(onSelectChangeFn.mock.calls[0][1].currentSelectedFiles[0].field_custom).toBe('a new file field'); + expect(onSelectChangeFn.mock.calls[0][1].currentSelectedFiles[0].raw).toEqual(fileList[0]); + }); + + it('props.formatRequest: upload request data can be changed through formatRequest', async () => { + const onFailFn = vi.fn(); + const { container } = render( + ({ requestData, more_field: 'more custom field' })} + action="https://tdesign.test.com/upload/fail/status_error" + onFail={onFailFn} + >, + ); + const inputDom = container.querySelector('input'); + const fileList = simulateFileChange(inputDom); + await mockDelay(700); + expect(onFailFn).toHaveBeenCalled(); + expect(onFailFn.mock.calls[0][0].XMLHttpRequest.upload.requestParams.requestData).toEqual({ + file: fileList[0], + length: 1, + }); + expect(onFailFn.mock.calls[0][0].XMLHttpRequest.upload.requestParams.more_field).toBe('more custom field'); + }); + + it('props.formatResponse: format upload success response', async () => { + const onChangeFn = vi.fn(); + const { container } = render( + ({ + responseData: { ret: response.ret, data: response.data }, + url: response.data.url, + extra_field: 'extra value', + })} + action="https://tdesign.test.com/upload/file_success" + onChange={onChangeFn} + >, + ); + const inputDom = container.querySelector('input'); + simulateFileChange(inputDom); + await mockDelay(); + expect(onChangeFn).toHaveBeenCalled(); + expect(onChangeFn.mock.calls[0][0][0].response.responseData).toEqual({ + ret: 0, + data: { name: 'tdesign.min.js', url: 'https://tdesign.gtimg.com/site/spline/script/tdesign.min.js' }, + }); + expect(onChangeFn.mock.calls[0][0][0].response.url).toBe( + 'https://tdesign.gtimg.com/site/spline/script/tdesign.min.js', + ); + expect(onChangeFn.mock.calls[0][0][0].response.extra_field).toBe('extra value'); + }); + it('props.formatResponse: format upload fail response', async () => { + const onFailFn = vi.fn(); + const { container } = render( + ({ error: response.error, name: response.name })} + onFail={onFailFn} + >, + ); + const inputDom = container.querySelector('input'); + const fileList = simulateFileChange(inputDom); + await mockDelay(); + expect(onFailFn).toHaveBeenCalled(); + expect(onFailFn.mock.calls[0][0].failedFiles[0].raw).toEqual(fileList[0]); + expect(onFailFn.mock.calls[0][0].currentFiles[0].raw).toEqual(fileList[0]); + expect(onFailFn.mock.calls[0][0].file.raw).toEqual(fileList[0]); + expect(onFailFn.mock.calls[0][0].e.type).toBe('load'); + expect(onFailFn.mock.calls[0][0].XMLHttpRequest).toBeTruthy(); + expect(onFailFn.mock.calls[0][0].response).toEqual({ error: 'upload failed', name: 'file-name.txt' }); + }); + + it('props.headers works fine', async () => { + const onFailFn = vi.fn(); + const { container } = render( + , + ); + const inputDom = container.querySelector('input'); + simulateFileChange(inputDom); + await mockDelay(); + expect(onFailFn).toHaveBeenCalled(); + expect(onFailFn.mock.calls[0][0].XMLHttpRequest.upload.requestHeaders['XML-HTTP-REQUEST']).toBe('tdesign_token'); + }); + + it('props.isBatchUpload works fine', async () => { + const onChangeFn = vi.fn(); + const { container } = render( + , + ); + const inputDom = container.querySelector('input'); + simulateFileChange(inputDom, 'file', 3); + await mockDelay(300); + expect(onChangeFn).toHaveBeenCalled(); + expect(onChangeFn.mock.calls[0][0].length).toBe(3); + }); + + it('props.locale: props.locale works fine if theme=file-flow', () => { + const { container } = render( + , + ); + expect(container.querySelector('.t-upload__file-flow-progress').textContent).toBe('uploading 80%'); + }); + + it('props.locale: props.locale works fine if theme=image', () => { + const { container } = render( + , + ); + expect(container.querySelector('.t-upload__image-progress').textContent).toBe('uploading 80%'); + }); + + it('props.max: can not show image add trigger if count of image is over than max', () => { + const { container } = render( + , + ); + expect(container.querySelector('.t-upload__image-add')).toBeFalsy(); + }); + + it('props.max works fine', async () => { + const onChangeFn = vi.fn(); + const { container } = render( + , + ); + const inputDom = container.querySelector('input'); + simulateFileChange(inputDom, 'file', 1); + await mockDelay(300); + expect(onChangeFn).not.toHaveBeenCalled(); + }); + it('props.max: max=0 means any count of files are allowed', async () => { + const onChangeFn = vi.fn(); + const { container } = render( + , + ); + const inputDom = container.querySelector('input'); + simulateFileChange(inputDom, 'file', 3); + await mockDelay(300); + expect(onChangeFn).toHaveBeenCalled(); + expect(onChangeFn.mock.calls[0][0].length).toBe(3); + }); + + it('props.name: rename file in request data to be file_name', async () => { + const onFailFn = vi.fn(); + const { container } = render( + , + ); + const inputDom = container.querySelector('input'); + const fileList = simulateFileChange(inputDom); + await mockDelay(700); + expect(onFailFn).toHaveBeenCalled(); + expect(onFailFn.mock.calls[0][0].XMLHttpRequest.upload.requestParams).toEqual({ + file_name: fileList[0], + length: 1, + }); + }); + + it('props.placeholder: theme=file works fine', () => { + const { container } = render(); + expect(container.querySelector('.t-upload__placeholder').textContent).toBe('this is placeholder'); + }); + + it('props.placeholder: theme=file-input works fine', () => { + const { container } = render(); + expect(container.querySelector('.t-upload__placeholder').textContent).toBe('this is placeholder'); + }); + + it('props.placeholder: theme=image-flow works fine', () => { + const { container } = render(); + expect(container.querySelector('.t-upload__placeholder').textContent).toBe('this is placeholder'); + }); + + it('props.placeholder: theme=file-flow works fine', () => { + const { container } = render(); + expect(container.querySelector('.t-upload__placeholder').textContent).toBe('this is placeholder'); + }); + + it('props.requestMethod works fine', async () => { + const onChangeFn = vi.fn(); + const { container } = render( + + Promise.resolve({ status: 'success', response: { url: 'https://tdesign.gtimg.com/demo/demo-image-1.png' } }) + } + onChange={onChangeFn} + >, + ); + const inputDom = container.querySelector('input'); + const fileList = simulateFileChange(inputDom, 'image'); + await mockDelay(300); + expect(onChangeFn).toHaveBeenCalled(); + expect(onChangeFn.mock.calls[0][0][0].raw).toEqual(fileList[0]); + expect(onChangeFn.mock.calls[0][0][0].response.url).toBe('https://tdesign.gtimg.com/demo/demo-image-1.png'); + }); + it('props.requestMethod works fine', async () => { + const onFailFn = vi.fn(); + const { container } = render( + Promise.resolve({ status: 'fail', error: 'upload failed' })} + onFail={onFailFn} + >, + ); + const inputDom = container.querySelector('input'); + const fileList = simulateFileChange(inputDom); + await mockDelay(300); + expect(onFailFn).toHaveBeenCalled(); + expect(onFailFn.mock.calls[0][0].failedFiles.map((t) => t.raw)).toEqual(fileList); + expect(onFailFn.mock.calls[0][0].currentFiles.map((t) => t.raw)).toEqual(fileList); + }); + + it('props.showUploadProgress works fine. `{".t-upload__file-flow-progress":{"text":"上传中"}}` should exist', () => { + const { container } = render( + , + ); + expect(container.querySelector('.t-upload__file-flow-progress').textContent).toBe('上传中'); + }); + + it('props.showUploadProgress works fine. `{".t-upload__image-progress":{"text":"上传中"}}` should exist', () => { + const { container } = render( + , + ); + expect(container.querySelector('.t-upload__image-progress').textContent).toBe('上传中'); + }); + + it('props.sizeLimit: file size is over than 23B, show default error tips', async () => { + const onValidateFn = vi.fn(); + const { container } = render( + , + ); + const inputDom = container.querySelector('input'); + simulateFileChange(inputDom, 'file', 5); + await mockDelay(100); + expect(onValidateFn).toHaveBeenCalled(); + expect(onValidateFn.mock.calls[0][0].type).toBe('FILE_OVER_SIZE_LIMIT'); + expect(onValidateFn.mock.calls[0][0].files.length).toBe(3); + }); + it('props.sizeLimit: file size is over than 23B, show custom error tips', async () => { + const onValidateFn = vi.fn(); + const { container } = render( + , + ); + const inputDom = container.querySelector('input'); + simulateFileChange(inputDom, 'file', 5); + await mockDelay(100); + expect(onValidateFn).toHaveBeenCalled(); + expect(onValidateFn.mock.calls[0][0].type).toBe('FILE_OVER_SIZE_LIMIT'); + expect(onValidateFn.mock.calls[0][0].files.length).toBe(3); + }); + it('props.sizeLimit: file size is over than 0.023KB, show default error tips (KB is default unit)', async () => { + const onValidateFn = vi.fn(); + const { container } = render( + , + ); + const inputDom = container.querySelector('input'); + simulateFileChange(inputDom, 'file', 5); + await mockDelay(100); + expect(onValidateFn).toHaveBeenCalled(); + expect(onValidateFn.mock.calls[0][0].type).toBe('FILE_OVER_SIZE_LIMIT'); + expect(onValidateFn.mock.calls[0][0].files.length).toBe(3); + }); + + it('props.theme: show image add trigger even if count of image is over than max', () => { + const { container } = render( + , + ); + expect(container.querySelector('.t-upload__image-add')).toBeTruthy(); + }); + + it('props.theme: theme=file and file status is fail works fine', () => { + const { container } = render( + , + ); + expect(container.querySelector('.t-icon-error-circle-filled')).toBeTruthy(); + }); + + it('props.theme: theme=file-input and file status is progress works fine', () => { + const { container } = render( + , + ); + expect(container.querySelector('.t-upload__single-progress')).toBeTruthy(); + }); + + it('props.theme: theme=file-input and file status is waiting works fine', () => { + const { container } = render( + , + ); + expect(container.querySelector('.t-upload__file-waiting.t-icon-time-filled')).toBeTruthy(); + }); + + it('props.theme: theme=file-input and file status is fail works fine', () => { + const { container } = render(); + expect(container.querySelector('.t-icon-error-circle-filled')).toBeTruthy(); + }); + + it('props.theme: theme=file-input and file status is success works fine', () => { + const { container } = render( + , + ); + expect(container.querySelector('.t-icon-check-circle-filled')).toBeTruthy(); + }); + + it('props.theme: theme=file-flow works fine', () => { + const { container } = render( + , + ); + expect(container.querySelectorAll('.t-upload__flow-table tbody > tr').length).toBe(4); + expect(container).toMatchSnapshot(); + }); + + it('props.theme: theme=image-flow works fine', () => { + const { container } = render( + , + ); + expect(container.querySelectorAll('.t-upload__card-item').length).toBe(5); + expect(container).toMatchSnapshot(); + }); + + it('props.tips works fine', () => { + const { container } = render( + TNode} + action="https://tdesign.test.com/upload/file_success" + >, + ); + expect(container.querySelector('.custom-node')).toBeTruthy(); + expect(container.querySelector('.t-upload__tips')).toBeTruthy(); + }); + + it('props.trigger: theme = file, trigger works fine', () => { + const { container } = render(TNode} theme="file">); + expect(container.querySelector('.custom-node')).toBeTruthy(); + }); + + it('props.trigger: theme = custom & draggable = true, trigger works fine', () => { + const { container } = render( + TNode} theme="custom" draggable={true}>, + ); + expect(container.querySelector('.custom-node')).toBeTruthy(); + }); + + it('props.trigger is a function with params, props.trigger: theme = custom & draggable = true, trigger works fine', () => { + const fn = vi.fn(); + render(); + expect(fn).toHaveBeenCalled(); + expect(fn.mock.calls[0][0].dragActive).toBe(false); + expect(fn.mock.calls[0][0].files).toEqual([]); + }); + + it('props.trigger: theme = custom, trigger works fine', () => { + const { container } = render(TNode} theme="custom">); + expect(container.querySelector('.custom-node')).toBeTruthy(); + }); + + it('props.trigger: theme = custom, trigger is right with files', () => { + const { container } = render( + TNode} + theme="custom" + draggable={true} + files={[{ name: 'file-name.txt', status: 'progress' }]} + >, + ); + expect(container.querySelector('.custom-node')).toBeTruthy(); + }); + + it('props.trigger is a function with params, props.trigger: theme = custom, trigger is right with files', () => { + const fn = vi.fn(); + render( + , + ); + expect(fn).toHaveBeenCalled(); + expect(fn.mock.calls[0][0].dragActive).toBe(false); + expect(fn.mock.calls[0][0].files).toEqual([{ name: 'file-name.txt', status: 'progress' }]); + }); + + it('props.triggerButtonProps is equal { theme: warning }', () => { + const { container } = render( + , + ); + expect(container.querySelectorAll('.t-button--theme-warning').length).toBe(1); + }); + + it('props.withCredentials works fine', async () => { + const onFailFn = vi.fn(); + const { container } = render( + , + ); + const inputDom = container.querySelector('input'); + simulateFileChange(inputDom); + await mockDelay(); + expect(onFailFn).toHaveBeenCalled(); + expect(onFailFn.mock.calls[0][0].XMLHttpRequest.withCredentials).toBeTruthy(); + }); + + it('events.cancelUpload works fine', async () => { + const onChangeFn = vi.fn(); + const onCancelUploadFn = vi.fn(); + const { container } = render( + , + ); + fireEvent.click(container.querySelector('.t-upload__dragger-progress-cancel')); + await mockDelay(); + expect(onChangeFn).not.toHaveBeenCalled(); + expect(onCancelUploadFn).toHaveBeenCalled(); + }); + + it('events.change: can trigger change if autoUpload is false for image', async () => { + const onChangeFn = vi.fn(); + const { container } = render( + , + ); + const inputDom = container.querySelector('input'); + const fileList = simulateFileChange(inputDom, 'image', 1); + await mockDelay(100); + expect(onChangeFn).toHaveBeenCalled(); + expect(onChangeFn.mock.calls[0][0][0].raw).toEqual(fileList[0]); + expect(onChangeFn.mock.calls[0][1].trigger).toBe('add'); + expect(onChangeFn.mock.calls[0][1].index).toBe(0); + expect(onChangeFn.mock.calls[0][1].file.raw).toEqual(fileList[0]); + }); + it('events.change: can trigger change if autoUpload is false for image-flow', async () => { + const onChangeFn = vi.fn(); + const { container } = render( + , + ); + const inputDom = container.querySelector('input'); + const fileList = simulateFileChange(inputDom, 'image', 1); + await mockDelay(100); + expect(onChangeFn).toHaveBeenCalled(); + expect(onChangeFn.mock.calls[0][0][0]).toEqual({ url: 'https://image1.png', status: 'success' }); + expect(onChangeFn.mock.calls[0][0][1].raw).toEqual(fileList[0]); + expect(onChangeFn.mock.calls[0][1].trigger).toBe('add'); + expect(onChangeFn.mock.calls[0][1].index).toBe(1); + expect(onChangeFn.mock.calls[0][1].file.raw).toEqual(fileList[0]); + expect(onChangeFn.mock.calls[0][1].files.map((t) => t.raw)).toEqual(fileList); + }); + + it('events.dragenter: drag image enter, trigger onDragenter event', () => { + const onDragenterFn = vi.fn(); + const onDragleaveFn2 = vi.fn(); + const { container } = render( + , + ); + const tUploadDraggerDom = container.querySelector('.t-upload__dragger'); + const files = simulateDragFileChange(tUploadDraggerDom, 'dragEnter', 'image'); + expect(onDragenterFn).toHaveBeenCalled(); + expect(onDragenterFn.mock.calls[0][0].e.type).toBe('dragenter'); + expect(onDragenterFn.mock.calls[0][0].e.dataTransfer.files).toEqual(files); + const tUploadDraggerDom1 = container.querySelector('.t-upload__dragger'); + simulateDragFileChange(tUploadDraggerDom1, 'dragOver'); + const tUploadDraggerDom2 = container.querySelector('.t-upload__dragger'); + simulateDragFileChange(tUploadDraggerDom2, 'dragLeave'); + expect(onDragleaveFn2).toHaveBeenCalled(); + expect(onDragleaveFn2.mock.calls[0][0].e.type).toBe('dragleave'); + expect(onDragleaveFn2.mock.calls[0][0].e.dataTransfer.files).toEqual(files); + }); + it('events.dragenter: drag file enter, trigger onDragenter event', () => { + const onDragenterFn = vi.fn(); + const onDragleaveFn2 = vi.fn(); + const { container } = render( + , + ); + const tUploadDraggerDom = container.querySelector('.t-upload__dragger'); + const files = simulateDragFileChange(tUploadDraggerDom, 'dragEnter'); + expect(onDragenterFn).toHaveBeenCalled(); + expect(onDragenterFn.mock.calls[0][0].e.type).toBe('dragenter'); + expect(onDragenterFn.mock.calls[0][0].e.dataTransfer.files).toEqual(files); + const tUploadDraggerDom1 = container.querySelector('.t-upload__dragger'); + simulateDragFileChange(tUploadDraggerDom1, 'dragOver'); + const tUploadDraggerDom2 = container.querySelector('.t-upload__dragger'); + simulateDragFileChange(tUploadDraggerDom2, 'dragLeave'); + expect(onDragleaveFn2).toHaveBeenCalled(); + expect(onDragleaveFn2.mock.calls[0][0].e.type).toBe('dragleave'); + expect(onDragleaveFn2.mock.calls[0][0].e.dataTransfer.files).toEqual(files); + }); + + it('events.dragleave: can not trigger dragleave event if drag leave other dom', () => { + const onDragleaveFn1 = vi.fn(); + const { container } = render( + , + ); + const tUploadDraggerDom = container.querySelector('.t-upload__dragger'); + simulateDragFileChange(tUploadDraggerDom, 'dragEnter'); + const tUploadTriggerDom1 = container.querySelector('.t-upload__trigger'); + simulateDragFileChange(tUploadTriggerDom1, 'dragLeave'); + expect(onDragleaveFn1).not.toHaveBeenCalled(); + }); + + it('events.drop: drag image drop, trigger onDrop event', () => { + const onDropFn = vi.fn(); + const { container } = render( + , + ); + const tUploadDraggerDom = container.querySelector('.t-upload__dragger'); + const files = simulateDragFileChange(tUploadDraggerDom, 'drop', 'image'); + expect(onDropFn).toHaveBeenCalled(); + expect(onDropFn.mock.calls[0][0].e.type).toBe('drop'); + expect(onDropFn.mock.calls[0][0].e.dataTransfer.files).toEqual(files); + }); + it('events.drop: drag file drop, trigger onDrop event', () => { + const onDropFn = vi.fn(); + const { container } = render( + , + ); + const tUploadDraggerDom = container.querySelector('.t-upload__dragger'); + const files = simulateDragFileChange(tUploadDraggerDom, 'drop'); + expect(onDropFn).toHaveBeenCalled(); + expect(onDropFn.mock.calls[0][0].e.type).toBe('drop'); + expect(onDropFn.mock.calls[0][0].e.dataTransfer.files).toEqual(files); + }); + + it('events.fail works fine', async () => { + const onFailFn = vi.fn(); + const { container } = render( + , + ); + const inputDom = container.querySelector('input'); + const fileList = simulateFileChange(inputDom); + await mockDelay(700); + expect(onFailFn).toHaveBeenCalled(); + expect(onFailFn.mock.calls[0][0].XMLHttpRequest.upload.requestParams).toEqual({ file: fileList[0], length: 1 }); + }); + + it('events.preview: single image preview works fine', async () => { + const onPreviewFn1 = vi.fn(); + const { container } = render( + , + ); + fireEvent.mouseEnter(container.querySelector('.t-upload__card-item')); + await mockDelay(); + fireEvent.click(container.querySelector('.t-icon-browse')); + await mockDelay(300); + const attrDom1 = document.querySelector('.t-image-viewer__modal-image'); + expect(attrDom1.getAttribute('src')).toBe('https://tdesign.gtimg.com/demo/demo-image-1.png'); + expect(onPreviewFn1).toHaveBeenCalled(); + expect(onPreviewFn1.mock.calls[0][0].file).toEqual({ + url: 'https://tdesign.gtimg.com/demo/demo-image-1.png', + name: 'demo-image-1.png', + }); + expect(onPreviewFn1.mock.calls[0][0].index).toBe(0); + expect(onPreviewFn1.mock.calls[0][0].e.type).toBe('click'); + }); + it('events.preview: multiple image preview works fine', async () => { + const onPreviewFn1 = vi.fn(); + const { container } = render( + , + ); + fireEvent.mouseEnter(container.querySelector('.t-upload__card-item:last-child')); + await mockDelay(); + fireEvent.click(container.querySelector('.t-upload__card-item:nth-child(2) .t-icon-browse')); + await mockDelay(300); + const attrDom1 = document.querySelector('.t-image-viewer__modal-image'); + expect(attrDom1.getAttribute('src')).toBe('https://tdesign.gtimg.com/site/avatar.jpg'); + expect(onPreviewFn1).toHaveBeenCalled(); + expect(onPreviewFn1.mock.calls[0][0].file).toEqual({ + url: 'https://tdesign.gtimg.com/site/avatar.jpg', + name: 'avatar.jpg', + }); + expect(onPreviewFn1.mock.calls[0][0].index).toBe(1); + expect(onPreviewFn1.mock.calls[0][0].e.type).toBe('click'); + }); + it('events.preview: theme=image-flow, image preview works fine', async () => { + const onPreviewFn1 = vi.fn(); + const { container } = render( + , + ); + fireEvent.mouseEnter(container.querySelector('.t-upload__card-item:nth-child(2)')); + await mockDelay(); + fireEvent.click(container.querySelector('.t-upload__card-item:nth-child(2) .t-icon-browse')); + await mockDelay(300); + const attrDom1 = document.querySelector('.t-image-viewer__modal-image'); + expect(attrDom1.getAttribute('src')).toBe('https://tdesign.gtimg.com/site/avatar.jpg'); + expect(onPreviewFn1).toHaveBeenCalled(); + expect(onPreviewFn1.mock.calls[0][0].file).toEqual({ + url: 'https://tdesign.gtimg.com/site/avatar.jpg', + name: 'avatar.jpg', + }); + expect(onPreviewFn1.mock.calls[0][0].index).toBe(1); + expect(onPreviewFn1.mock.calls[0][0].e.type).toBe('click'); + }); + + it('events.remove: remove single file, trigger remove event', () => { + const onChangeFn = vi.fn(); + const onRemoveFn = vi.fn(); + const { container } = render( + , + ); + fireEvent.click(container.querySelector('.t-upload__icon-delete')); + expect(onChangeFn).toHaveBeenCalled(); + expect(onChangeFn.mock.calls[0][0]).toEqual([]); + expect(onChangeFn.mock.calls[0][1].e.type).toBe('click'); + expect(onRemoveFn).toHaveBeenCalled(); + expect(onRemoveFn.mock.calls[0][0].index).toBe(0); + expect(onRemoveFn.mock.calls[0][0].file).toBeTruthy(); + expect(onRemoveFn.mock.calls[0][0].e.type).toBe('click'); + }); + it('events.remove: remove only one of file list, trigger remove event', () => { + const onChangeFn = vi.fn(); + const onRemoveFn = vi.fn(); + const { container } = render( + , + ); + fireEvent.click(container.querySelector('.t-upload__single-display-text .t-upload__icon-delete')); + expect(onChangeFn).toHaveBeenCalled(); + expect(onChangeFn.mock.calls[0][0]).toEqual([ + { name: 'file2.txt', url: 'https://xxx2.txt' }, + { name: 'file3.txt', url: 'https://xxx3.txt' }, + ]); + expect(onChangeFn.mock.calls[0][1].index).toBe(0); + expect(onChangeFn.mock.calls[0][1].file).toBeTruthy(); + expect(onChangeFn.mock.calls[0][1].e.type).toBe('click'); + expect(onRemoveFn).toHaveBeenCalled(); + expect(onRemoveFn.mock.calls[0][0].index).toBe(0); + expect(onRemoveFn.mock.calls[0][0].file).toBeTruthy(); + expect(onRemoveFn.mock.calls[0][0].e.type).toBe('click'); + }); + it('events.remove: failed image file can be removed', () => { + const onChangeFn = vi.fn(); + const onRemoveFn = vi.fn(); + const { container } = render( + , + ); + fireEvent.click(container.querySelector('.t-upload__card-mask-item .t-icon-delete')); + expect(onChangeFn).toHaveBeenCalled(); + expect(onChangeFn.mock.calls[0][0]).toEqual([]); + expect(onChangeFn.mock.calls[0][1].index).toBe(0); + expect(onChangeFn.mock.calls[0][1].file).toBeTruthy(); + expect(onChangeFn.mock.calls[0][1].e.type).toBe('click'); + expect(onRemoveFn).toHaveBeenCalled(); + expect(onRemoveFn.mock.calls[0][0].index).toBe(0); + expect(onRemoveFn.mock.calls[0][0].file).toBeTruthy(); + expect(onRemoveFn.mock.calls[0][0].e.type).toBe('click'); + }); + it('events.remove: success status image can be removed', () => { + const onChangeFn = vi.fn(); + const onRemoveFn = vi.fn(); + const { container } = render( + , + ); + fireEvent.click(container.querySelector('.t-upload__card-mask-item .t-icon-delete')); + expect(onChangeFn).toHaveBeenCalled(); + expect(onChangeFn.mock.calls[0][0]).toEqual([]); + expect(onChangeFn.mock.calls[0][1].index).toBe(0); + expect(onChangeFn.mock.calls[0][1].file).toBeTruthy(); + expect(onChangeFn.mock.calls[0][1].e.type).toBe('click'); + expect(onRemoveFn).toHaveBeenCalled(); + expect(onRemoveFn.mock.calls[0][0].index).toBe(0); + expect(onRemoveFn.mock.calls[0][0].file).toBeTruthy(); + expect(onRemoveFn.mock.calls[0][0].e.type).toBe('click'); + }); + it('events.remove: theme=file-input, file can be removed to be empty', () => { + const onChangeFn = vi.fn(); + const onRemoveFn = vi.fn(); + const { container } = render( + , + ); + fireEvent.click(container.querySelector('.t-upload__single-input-clear')); + expect(onChangeFn).toHaveBeenCalled(); + expect(onChangeFn.mock.calls[0][0]).toEqual([]); + expect(onRemoveFn).toHaveBeenCalled(); + expect(onRemoveFn.mock.calls[0][0].e.type).toBe('click'); + }); + it('events.remove: theme=file-flow, remove file, trigger remove event', () => { + const onChangeFn = vi.fn(); + const onRemoveFn = vi.fn(); + const { container } = render( + , + ); + fireEvent.click(container.querySelector('.t-upload__delete')); + expect(onChangeFn).toHaveBeenCalled(); + expect(onChangeFn.mock.calls[0][0]).toEqual([]); + expect(onChangeFn.mock.calls[0][1].e.type).toBe('click'); + expect(onRemoveFn).toHaveBeenCalled(); + expect(onRemoveFn.mock.calls[0][0].index).toBe(0); + expect(onRemoveFn.mock.calls[0][0].file).toBeTruthy(); + expect(onRemoveFn.mock.calls[0][0].e.type).toBe('click'); + }); + it('events.remove: theme=image-flow, remove file, trigger remove event', () => { + const onChangeFn = vi.fn(); + const onRemoveFn = vi.fn(); + const { container } = render( + , + ); + fireEvent.click(container.querySelector('.t-upload__delete')); + expect(onChangeFn).toHaveBeenCalled(); + expect(onChangeFn.mock.calls[0][0]).toEqual([]); + expect(onChangeFn.mock.calls[0][1].e.type).toBe('click'); + expect(onRemoveFn).toHaveBeenCalled(); + expect(onRemoveFn.mock.calls[0][0].index).toBe(0); + expect(onRemoveFn.mock.calls[0][0].file).toBeTruthy(); + expect(onRemoveFn.mock.calls[0][0].e.type).toBe('click'); + }); + it('events.remove: theme=file-flow & isBatchUpload=true, remove all files if click delete node', () => { + const onChangeFn = vi.fn(); + const onRemoveFn = vi.fn(); + const { container } = render( + , + ); + fireEvent.click(container.querySelector('.t-upload__delete')); + expect(onChangeFn).toHaveBeenCalled(); + expect(onChangeFn.mock.calls[0][0]).toEqual([]); + expect(onChangeFn.mock.calls[0][1].e.type).toBe('click'); + expect(onRemoveFn).toHaveBeenCalled(); + expect(onRemoveFn.mock.calls[0][0].index).toBe(-1); + expect(onRemoveFn.mock.calls[0][0].file).toBe(undefined); + expect(onRemoveFn.mock.calls[0][0].e.type).toBe('click'); + }); + it('events.remove: theme=image & draggable=true, success file can be removed', () => { + const onChangeFn = vi.fn(); + const onRemoveFn = vi.fn(); + const { container } = render( + , + ); + fireEvent.click(container.querySelector('.t-upload__dragger-delete-btn')); + expect(onChangeFn).toHaveBeenCalled(); + expect(onChangeFn.mock.calls[0][0]).toEqual([]); + expect(onChangeFn.mock.calls[0][1].e.type).toBe('click'); + expect(onRemoveFn).toHaveBeenCalled(); + expect(onRemoveFn.mock.calls[0][0].index).toBe(0); + expect(onRemoveFn.mock.calls[0][0].file).toEqual({ url: 'https://www.image.png', status: 'success' }); + expect(onRemoveFn.mock.calls[0][0].e.type).toBe('click'); + }); + it('events.remove: theme=file & multiple=true & autoUpload=false', () => { + const onChangeFn = vi.fn(); + const onRemoveFn = vi.fn(); + const { container } = render( + , + ); + fireEvent.click(container.querySelector('.t-upload__single-display-text:last-child .t-upload__icon-delete')); + expect(onChangeFn).toHaveBeenCalled(); + expect(onChangeFn.mock.calls[0][0]).toEqual([{ name: 'file1.txt' }, { name: 'file2.txt', status: 'success' }]); + expect(onRemoveFn).toHaveBeenCalled(); + expect(onRemoveFn.mock.calls[0][0].index).toBe(2); + expect(onRemoveFn.mock.calls[0][0].file).toEqual({ name: 'file3.txt', status: 'waiting' }); + expect(onRemoveFn.mock.calls[0][0].e.type).toBe('click'); + }); + it('events.remove: theme=file-flow & multiple=true & autoUpload=true, remove success file', () => { + const onChangeFn = vi.fn(); + const onRemoveFn = vi.fn(); + const { container } = render( + , + ); + fireEvent.click(container.querySelector('.t-upload__flow-table tbody tr:nth-child(2) .t-upload__delete')); + expect(onChangeFn).toHaveBeenCalled(); + expect(onChangeFn.mock.calls[0][0]).toEqual([ + { name: 'file1.txt' }, + { name: 'file3.txt', status: 'waiting' }, + { name: 'file4.txt', status: 'fail' }, + ]); + expect(onRemoveFn).toHaveBeenCalled(); + expect(onRemoveFn.mock.calls[0][0].index).toBe(1); + expect(onRemoveFn.mock.calls[0][0].file).toEqual({ name: 'file2.txt', status: 'success' }); + expect(onRemoveFn.mock.calls[0][0].e.type).toBe('click'); + }); + it('events.remove: theme=file-flow & multiple=true & autoUpload=true, remove fail file', async () => { + const onChangeFn1 = vi.fn(); + const onRemoveFn1 = vi.fn(); + const { container } = render( + , + ); + const inputDom = container.querySelector('input'); + simulateFileChange(inputDom); + await mockDelay(); + fireEvent.click(container.querySelector('.t-upload__flow-table tbody tr:last-child .t-upload__delete')); + expect(onChangeFn1).not.toHaveBeenCalled(); + expect(onRemoveFn1).toHaveBeenCalled(); + expect(onRemoveFn1.mock.calls[0][0].index).toBe(2); + expect(onRemoveFn1.mock.calls[0][0].file.name).toBe('file-name.txt'); + expect(onRemoveFn1.mock.calls[0][0].file.status).toBe('fail'); + expect(onRemoveFn1.mock.calls[0][0].e.type).toBe('click'); + }); +}); diff --git a/src/upload/_example/base.jsx b/src/upload/_example/base.jsx index 6c7170b0a..d6c296378 100644 --- a/src/upload/_example/base.jsx +++ b/src/upload/_example/base.jsx @@ -73,16 +73,17 @@ export default function UploadExample() { // 有文件数量超出时会触发,文件大小超出限制、文件同名时会触发等场景。注意如果设置允许上传同名文件,则此事件不会触发 const onValidate = (params) => { const { files, type } = params; - console.log('onValidate', params); - if (type === 'FILE_OVER_SIZE_LIMIT') { - files.map((t) => t.name).join('、'); - MessagePlugin.warning(`${files.map((t) => t.name).join('、')} 等文件大小超出限制,已自动过滤`, 5000); - } else if (type === 'FILES_OVER_LENGTH_LIMIT') { - MessagePlugin.warning('文件数量超出限制,仅上传未超出数量的文件'); - } else if (type === 'FILTER_FILE_SAME_NAME') { - // 如果希望支持上传同名文件,请设置 allowUploadDuplicateFile={true} - MessagePlugin.warning('不允许上传同名文件'); - } + console.log('onValidate', type, files); + const messageMap = { + FILE_OVER_SIZE_LIMIT: '文件大小超出限制,已自动过滤', + FILES_OVER_LENGTH_LIMIT: '文件数量超出限制,仅上传未超出数量的文件', + // if you need same name files, setting allowUploadDuplicateFile={true} please + FILTER_FILE_SAME_NAME: '不允许上传同名文件', + BEFORE_ALL_FILES_UPLOAD: 'beforeAllFilesUpload 方法拦截了文件', + CUSTOM_BEFORE_UPLOAD: 'beforeUpload 方法拦截了文件', + }; + // you can also set Upload.tips and Upload.status to show warning message. + messageMap[type] && MessagePlugin.warning(messageMap[type]); }; // 仅自定义文件列表所需 diff --git a/src/upload/_example/draggable.jsx b/src/upload/_example/draggable.jsx index f81b8c5af..7e794564b 100644 --- a/src/upload/_example/draggable.jsx +++ b/src/upload/_example/draggable.jsx @@ -68,6 +68,8 @@ export default function UploadExample() { onChange={setFiles} onFail={onFail} onSuccess={onSuccess} + // use fileListDisplay to define any file info + // fileListDisplay={({ files }) =>
    {JSON.stringify(files)}
    } /> ([]); const [toUploadFiles, setToUploadFiles] = useState([]); @@ -33,6 +33,7 @@ export default function useUpload(props: TdUploadProps) { const tipsClasses = `${classPrefix}-upload__tips ${classPrefix}-size-s`; const errorClasses = [tipsClasses].concat(`${classPrefix}-upload__tips-error`); + const placeholderClass = `${classPrefix}-upload__placeholder`; // 单文件场景:触发元素文本 const triggerUploadText = useMemo(() => { @@ -123,15 +124,16 @@ export default function useUpload(props: TdUploadProps) { : `${t(locale.sizeLimitMessage, { sizeLimit: limit.size })} ${limit.unit}`; } - const handleNonAutoUpload = (toFiles: UploadFile[]) => { + const handleNotAutoUpload = (toFiles: UploadFile[]) => { const tmpFiles = props.multiple && !isBatchUpload ? uploadValue.concat(toFiles) : toFiles; + if (!tmpFiles.length) return; // 图片需要本地预览 if (['image', 'image-flow'].includes(props.theme)) { const list = tmpFiles.map( (file) => new Promise((resolve) => { getFileUrlByFileRaw(file.raw).then((url) => { - resolve({ ...file, url }); + resolve({ ...file, url: file.url || url }); }); }), ); @@ -139,14 +141,16 @@ export default function useUpload(props: TdUploadProps) { setUploadValue(files, { trigger: 'add', index: uploadValue.length, - file: files[0], + file: toFiles[0], + files: toFiles, }); }); } else { setUploadValue(tmpFiles, { trigger: 'add', index: uploadValue.length, - file: tmpFiles[0], + file: toFiles[0], + files: toFiles, }); } // toUploadFiles.current = []; @@ -156,7 +160,7 @@ export default function useUpload(props: TdUploadProps) { const onFileChange = (files: FileList) => { if (disabled) return; // @ts-ignore - props.onSelectChange?.([...files], { currentSelectedFiles: toUploadFiles }); + props.onSelectChange?.([...files], { currentSelectedFiles: formatToUploadFile([...files], props.format) }); validateFile({ uploadValue, // @ts-ignore @@ -171,34 +175,44 @@ export default function useUpload(props: TdUploadProps) { beforeAllFilesUpload: props.beforeAllFilesUpload, }).then((args) => { // 自定义全文件校验不通过 - if (args.validateResult?.type === 'BEFORE_ALL_FILES_UPLOAD') return; + if (args.validateResult?.type === 'BEFORE_ALL_FILES_UPLOAD') { + props.onValidate?.({ type: 'BEFORE_ALL_FILES_UPLOAD', files: args.files }); + return; + } // 文件数量校验不通过 if (args.lengthOverLimit) { props.onValidate?.({ type: 'FILES_OVER_LENGTH_LIMIT', files: args.files }); + if (!args.files.length) return; } // 过滤相同的文件名 if (args.hasSameNameFile) { props.onValidate?.({ type: 'FILTER_FILE_SAME_NAME', files: args.files }); } - // 文件大小校验结果处理 + // 文件大小校验结果处理(已过滤超出限制的文件) if (args.fileValidateList instanceof Array) { - const { sizeLimitErrors, toFiles } = getFilesAndErrors(args.fileValidateList, getSizeLimitError); + const { sizeLimitErrors, beforeUploadErrorFiles, toFiles } = getFilesAndErrors( + args.fileValidateList, + getSizeLimitError, + ); const tmpWaitingFiles = autoUpload ? toFiles : toUploadFiles.concat(toFiles); props.onWaitingUploadFilesChange?.({ files: tmpWaitingFiles, trigger: 'validate' }); - // 错误信息处理 + // 文件大小处理 if (sizeLimitErrors[0]) { setSizeOverLimitMessage(sizeLimitErrors[0].file.response.error); props.onValidate?.({ type: 'FILE_OVER_SIZE_LIMIT', files: sizeLimitErrors.map((t) => t.file) }); } else { setSizeOverLimitMessage(''); + // 自定义方法 beforeUpload 拦截的文件 + if (beforeUploadErrorFiles.length) { + props.onValidate?.({ type: 'CUSTOM_BEFORE_UPLOAD', files: beforeUploadErrorFiles }); + } } // 如果是自动上传 if (autoUpload) { - // toUploadFiles.current = tmpWaitingFiles; setToUploadFiles(tmpWaitingFiles); uploadFiles(tmpWaitingFiles); } else { - handleNonAutoUpload(tmpWaitingFiles); + handleNotAutoUpload(tmpWaitingFiles); } } }); @@ -216,8 +230,8 @@ export default function useUpload(props: TdUploadProps) { } /** - * 上传文件 - * 对外暴露方法,修改时需谨慎 + * 上传文件。对外暴露方法,修改时需谨慎 + * @param toFiles 本地上传的文件列表 */ function uploadFiles(toFiles?: UploadFile[]) { const notUploadedFiles = uploadValue.filter((t) => t.status !== 'success'); @@ -238,6 +252,7 @@ export default function useUpload(props: TdUploadProps) { uploadAllFilesInOneRequest: props.uploadAllFilesInOneRequest, useMockProgress: props.useMockProgress, data: props.data, + mockProgressDuration: props.mockProgressDuration, requestMethod: props.requestMethod, formatRequest: props.formatRequest, formatResponse: props.formatResponse, @@ -248,48 +263,44 @@ export default function useUpload(props: TdUploadProps) { if (xhr.files[0]?.raw && xhrReq.current.find((item) => item.files[0].raw === xhr.files[0].raw)) return; xhrReq.current = xhrReq.current.concat(xhr); }, - }).then( - ({ status, data, list, failedFiles }) => { - setUploading(false); - if (status === 'success') { - // 全部上传成功后,一次性添加(非自动上传已在上一步添加) - if (props.autoUpload) { - setUploadValue([...data.files], { - trigger: 'add', - file: data.files[0], - }); - } - props.onSuccess?.({ - fileList: data.files, - currentFiles: files, - file: files[0], - // 只有全部请求完成后,才会存在该字段 - results: list?.map((t) => t.data), - // 单文件单请求有一个 response,多文件多请求有多个 response - response: data.response || list.map((t) => t.data.response), - }); - xhrReq.current = []; - } else if (failedFiles?.[0]) { - props.onFail?.({ - e: data.event, - file: failedFiles[0], - failedFiles, - currentFiles: files, - response: data.response, + }).then(({ status, data, list, failedFiles }) => { + setUploading(false); + if (status === 'success') { + // 全部上传成功后,一次性添加(非自动上传已在上一步添加) + if (props.autoUpload) { + setUploadValue([...data.files], { + trigger: 'add', + file: data.files[0], }); } + props.onSuccess?.({ + fileList: data.files, + currentFiles: files, + file: files[0], + // 只有全部请求完成后,才会存在该字段 + results: list?.map((t) => t.data), + // 单文件单请求有一个 response,多文件多请求有多个 response + response: data.response || list.map((t) => t.data.response), + XMLHttpRequest: data.XMLHttpRequest, + }); + xhrReq.current = []; + } else if (failedFiles?.[0]) { + props.onFail?.({ + e: data.event, + file: failedFiles[0], + failedFiles, + currentFiles: files, + response: data.response, + XMLHttpRequest: data.XMLHttpRequest, + }); + } - // 非自动上传,文件都在 uploadValue,不涉及 toUploadFiles - if (autoUpload) { - setToUploadFiles(failedFiles); - props.onWaitingUploadFilesChange?.({ files: failedFiles, trigger: 'uploaded' }); - } - }, - (p) => { - onResponseError(p); - setUploading(false); - }, - ); + // 非自动上传,文件都在 uploadValue,不涉及 toUploadFiles + if (autoUpload) { + setToUploadFiles(failedFiles); + props.onWaitingUploadFilesChange?.({ files: failedFiles, trigger: 'uploaded' }); + } + }); } function onRemove(p: UploadRemoveContext) { @@ -304,13 +315,13 @@ export default function useUpload(props: TdUploadProps) { if (isBatchUpload || !props.multiple) { props.onWaitingUploadFilesChange?.({ files: [], trigger: 'remove' }); setUploadValue([], changePrams); - // toUploadFiles.current = []; setToUploadFiles([]); xhrReq.current = []; } else if (!props.autoUpload) { uploadValue.splice(p.index, 1); setUploadValue([...uploadValue], changePrams); } else if (p.index < uploadValue.length) { + // autoUpload 场景下, p.index < uploadValue.length 表示移除已经上传成功的文件;反之表示移除待上传列表文件 uploadValue.splice(p.index, 1); setUploadValue([...uploadValue], changePrams); } else { @@ -324,7 +335,7 @@ export default function useUpload(props: TdUploadProps) { } const triggerUpload = () => { - if (disabled) return; + if (disabled || !inputRef.current) return; (inputRef.current as HTMLInputElement).click(); }; @@ -349,9 +360,11 @@ export default function useUpload(props: TdUploadProps) { ); } - if (context?.file) { + if (context?.file && !autoUpload) { onRemove?.({ file: context.file, e: context.e, index: 0 }); } + + props.onCancelUpload?.(); }; return { @@ -366,6 +379,7 @@ export default function useUpload(props: TdUploadProps) { uploading, tipsClasses, errorClasses, + placeholderClass, inputRef, disabled, xhrReq, diff --git a/src/upload/interface.ts b/src/upload/interface.ts index 38566e6e2..4a621755c 100644 --- a/src/upload/interface.ts +++ b/src/upload/interface.ts @@ -19,6 +19,8 @@ export interface CommonDisplayFileProps { uploading?: boolean; tipsClasses?: string; errorClasses?: string[]; + placeholderClass?: string; + showUploadProgress?: boolean; children?: ReactNode; fileListDisplay?: TdUploadProps['fileListDisplay']; onRemove?: (p: UploadRemoveContext) => void; diff --git a/src/upload/themes/CustomFile.tsx b/src/upload/themes/CustomFile.tsx index 71b3538ef..d4d9a1130 100644 --- a/src/upload/themes/CustomFile.tsx +++ b/src/upload/themes/CustomFile.tsx @@ -29,19 +29,21 @@ const CustomFile = (props: CustomFileProps) => { } : {}; - const renderDragContent = () => ( -
    -
    - {parseTNode(props.dragContent, { dragActive, files: displayFiles }) || - props.trigger?.({ dragActive, files: displayFiles }) || - props.childrenNode} + const renderDragContent = () => { + const childrenContent = parseTNode(props.childrenNode, { dragActive, files: displayFiles }); + const triggerContent = parseTNode(props.trigger, { dragActive, files: displayFiles }); + return ( +
    +
    + {parseTNode(props.dragContent, { dragActive, files: displayFiles }) || triggerContent || childrenContent} +
    -
    - ); + ); + }; return ( <> diff --git a/src/upload/themes/DraggerFile.tsx b/src/upload/themes/DraggerFile.tsx index 4493bca3a..625baf5ac 100644 --- a/src/upload/themes/DraggerFile.tsx +++ b/src/upload/themes/DraggerFile.tsx @@ -6,13 +6,14 @@ import { } from 'tdesign-icons-react'; import { abridgeName, getFileSizeText } from '../../_common/js/upload/utils'; import { TdUploadProps, UploadFile } from '../type'; -import Link from '../../link'; +import Button from '../../button'; import { CommonDisplayFileProps } from '../interface'; import useCommonClassName from '../../hooks/useCommonClassName'; import TLoading from '../../loading'; import useDrag, { UploadDragEvents } from '../hooks/useDrag'; import useGlobalIcon from '../../hooks/useGlobalIcon'; import ImageViewer from '../../image-viewer'; +import parseTNode from '../../_util/parseTNode'; export interface DraggerProps extends CommonDisplayFileProps { trigger?: TdUploadProps['trigger']; @@ -49,8 +50,7 @@ const DraggerFile: FC = (props) => { const renderImage = () => { const file = displayFiles[0]; - if (!file) return null; - const url = file.url || file.response?.url; + const url = file?.url || file?.response?.url; return (
    {url && }>} @@ -60,8 +60,7 @@ const DraggerFile: FC = (props) => { const renderUploading = () => { const file = displayFiles[0]; - if (!file) return null; - if (file.status === 'progress') { + if (file?.status === 'progress') { return (
    @@ -73,29 +72,33 @@ const DraggerFile: FC = (props) => { const renderMainPreview = () => { const file = displayFiles[0]; - if (!file) return null; const fileName = props.abridgeName ? abridgeName(file.name, ...props.abridgeName) : file.name; + const fileInfo = ( + <> +
    + {fileName} + {file.status === 'progress' && renderUploading()} + {file.status === 'success' && } + {file.status === 'fail' && } +
    + + {locale.file.fileSizeText}:{getFileSizeText(file.size)} + + + {locale.file.fileOperationDateText}:{file.uploadTime || '-'} + + + ); return (
    {props.theme === 'image' && renderImage()}
    -
    - {fileName} - {file.status === 'progress' && renderUploading()} - {file.status === 'success' && } - {file.status === 'fail' && } -
    - - {locale.file.fileSizeText}:{getFileSizeText(file.size)} - - - {locale.file.fileOperationDateText}:{file.uploadTime || '-'} - + {props.fileListDisplay ? parseTNode(props.fileListDisplay, { files: displayFiles }) : fileInfo}
    {['progress', 'waiting'].includes(file.status) && !disabled && ( - @@ -106,40 +109,40 @@ const DraggerFile: FC = (props) => { } > {locale?.cancelUploadText} - + )} {!props.autoUpload && file.status === 'waiting' && ( - props.uploadFiles?.()} className={`${uploadPrefix}__dragger-upload-btn`} > {locale.triggerUploadText.normal} - + )}
    {['fail', 'success'].includes(file?.status) && !disabled && (
    - {locale.triggerUploadText.reupload} - - +
    )}
    @@ -160,7 +163,7 @@ const DraggerFile: FC = (props) => { const getContent = () => { const file = displayFiles[0]; - if (file && ['progress', 'success', 'fail', 'waiting'].includes(file.status)) { + if (file && (['progress', 'success', 'fail', 'waiting'].includes(file.status) || !file.status)) { return renderMainPreview(); } return ( diff --git a/src/upload/themes/ImageCard.tsx b/src/upload/themes/ImageCard.tsx index 74650b04c..9f395f346 100644 --- a/src/upload/themes/ImageCard.tsx +++ b/src/upload/themes/ImageCard.tsx @@ -5,6 +5,7 @@ import { AddIcon as TdAddIcon, ErrorCircleFilledIcon as TdErrorCircleFilledIcon, } from 'tdesign-icons-react'; +import classNames from 'classnames'; import Loading from '../../loading'; import useGlobalIcon from '../../hooks/useGlobalIcon'; import ImageViewer from '../../image-viewer'; @@ -70,7 +71,7 @@ const ImageCard = (props: ImageCardUploadProps) => { ); const renderProgressFile = (file: UploadFile, loadCard: string) => ( -
    +

    {locale?.progress?.uploadingText} @@ -85,7 +86,7 @@ const ImageCard = (props: ImageCardUploadProps) => {

    {file.response?.error || locale?.progress?.failText}

    e.stopPropagation()}> - props?.onRemove?.({ e, file, index })} /> + props.onRemove?.({ e, file, index })} />
    @@ -109,7 +110,9 @@ const ImageCard = (props: ImageCardUploadProps) => { })} {showTrigger && (
  • -
    +

    {locale?.triggerUploadText?.image}

    diff --git a/src/upload/themes/MultipleFlowList.tsx b/src/upload/themes/MultipleFlowList.tsx index ab085b6ed..bea41a844 100644 --- a/src/upload/themes/MultipleFlowList.tsx +++ b/src/upload/themes/MultipleFlowList.tsx @@ -16,6 +16,7 @@ import useDrag, { UploadDragEvents } from '../hooks/useDrag'; import { abridgeName, returnFileSize } from '../../_common/js/upload/utils'; import TLoading from '../../loading'; import Link from '../../link'; +import parseTNode from '../../_util/parseTNode'; export interface ImageFlowListProps extends CommonDisplayFileProps { uploadFiles?: (toFiles?: UploadFile[]) => void; @@ -84,6 +85,7 @@ const ImageFlowList = (props: ImageFlowListProps) => { const renderImgItem = (file: UploadFile, index: number) => { const { iconMap, textMap } = getStatusMap(); + const fileName = props.abridgeName && file.name ? abridgeName(file.name, ...props.abridgeName) : file.name; return (
  • { ])} > {['fail', 'progress'].includes(file.status) && ( -
    +
    {iconMap[file.status as 'fail' | 'progress']}

    {textMap[file.status as 'fail' | 'progress']}

    @@ -124,15 +131,15 @@ const ImageFlowList = (props: ImageFlowListProps) => { )} {!disabled && ( props.onRemove({ e, index, file })} + className={`${uploadPrefix}__card-mask-item ${uploadPrefix}__delete`} + onClick={(e) => props.onRemove({ e, index, file })} > )}
    -

    {abridgeName(file.name)}

    +

    {fileName}

  • ); }; @@ -142,9 +149,9 @@ const ImageFlowList = (props: ImageFlowListProps) => { return (
    {iconMap[file.status]} - + {textMap[file.status]} - {file.status === 'progress' ? ` ${file.percent}%` : ''} + {props.showUploadProgress && file.status === 'progress' ? ` ${file.percent || 0}%` : ''}
    ); @@ -152,7 +159,12 @@ const ImageFlowList = (props: ImageFlowListProps) => { const renderNormalActionCol = (file: UploadFile, index: number) => ( - props.onRemove({ e, index, file })}> + props.onRemove({ e, index, file })} + > {locale?.triggerUploadText?.delete} @@ -166,7 +178,8 @@ const ImageFlowList = (props: ImageFlowListProps) => { props.onRemove({ e, index: -1, file: null })} + className={`${uploadPrefix}__delete`} + onClick={(e) => props.onRemove({ e, index: -1, file: undefined })} > {locale?.triggerUploadText?.delete} @@ -175,11 +188,10 @@ const ImageFlowList = (props: ImageFlowListProps) => { const renderFileList = () => { if (props.fileListDisplay) { - const list = props.fileListDisplay({ + return parseTNode(props.fileListDisplay, { files: displayFiles, dragEvents: innerDragEvents, }); - return list; } return ( @@ -201,13 +213,13 @@ const ImageFlowList = (props: ImageFlowListProps) => { // 合并操作出现条件为:当前为合并上传模式且列表内没有待上传文件 const showBatchUploadAction = props.isBatchUpload; const deleteNode = - showBatchUploadAction && !displayFiles.find((item) => item.status !== 'success') + showBatchUploadAction && displayFiles.every((item) => item.status === 'success' || !item.status) ? renderBatchActionCol(index) : renderNormalActionCol(file, index); const fileName = props.abridgeName?.length ? abridgeName(file.name, ...props.abridgeName) : file.name; return ( -
    + {file.url ? ( {fileName} @@ -227,6 +239,20 @@ const ImageFlowList = (props: ImageFlowListProps) => { ); }; + const renderImageList = () => { + if (props.fileListDisplay) { + return parseTNode(props.fileListDisplay, { + files: displayFiles, + dragEvents: innerDragEvents, + }); + } + return ( +
      + {displayFiles.map((file, index) => renderImgItem(file, index))} +
    + ); + }; + const cardClassName = `${uploadPrefix}__flow-card-area`; return (
    @@ -241,13 +267,7 @@ const ImageFlowList = (props: ImageFlowListProps) => { {props.theme === 'image-flow' && (
    - {displayFiles.length ? ( -
      - {displayFiles.map((file, index) => renderImgItem(file, index))} -
    - ) : ( - renderEmpty() - )} + {displayFiles.length ? renderImageList() : renderEmpty()}
    )} @@ -262,13 +282,20 @@ const ImageFlowList = (props: ImageFlowListProps) => { {!props.autoUpload && (
    - props.cancelUpload?.({ e })} disabled={disabled || !uploading}> + props.cancelUpload?.({ e })} + > {locale?.cancelUploadText} + props.uploadFiles?.()} > {uploadText} diff --git a/src/upload/themes/NormalFile.tsx b/src/upload/themes/NormalFile.tsx index d9f58fac3..2b6a36424 100644 --- a/src/upload/themes/NormalFile.tsx +++ b/src/upload/themes/NormalFile.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import isFunction from 'lodash/isFunction'; import { CloseIcon as TdCloseIcon, TimeFilledIcon as TdTimeFilledIcon, @@ -42,42 +43,44 @@ export default function NormalFile(props: NormalFileProps) { // 文本型预览 const renderFilePreviewAsText = (files: UploadFile[]) => { if (theme !== 'file') return null; - if (!props.multiple && files[0]?.status === 'fail') { + if (!props.multiple && files[0]?.status === 'fail' && props.autoUpload) { return null; } - return files.map((file, index) => ( -
    - {file.url ? ( - - {file.name} - - ) : ( - {file.name} - )} - {file.status === 'fail' && ( -
    - -
    - )} - {file.status === 'waiting' && ( -
    - -
    - )} - {file.status === 'progress' && renderProgress(file.percent)} - {!disabled && file.status !== 'progress' && ( - props.onRemove({ e, file, index })} /> - )} -
    - )); + return files.map((file, index) => { + const fileName = props.abridgeName && file.name ? abridgeName(file.name, ...props.abridgeName) : file.name; + return ( +
    + {file.url ? ( + + {fileName} + + ) : ( + {fileName} + )} + {file.status === 'fail' && ( +
    + +
    + )} + {file.status === 'waiting' && ( +
    + +
    + )} + {file.status === 'progress' && renderProgress(file.percent)} + {!disabled && file.status !== 'progress' && ( + props.onRemove({ e, file, index })} /> + )} +
    + ); + }); }; // 输入框型预览 const renderFilePreviewAsInput = () => { - if (theme !== 'file-input') return; const file = props.displayFiles[0]; const inputTextClass = [ `${classPrefix}-input__inner`, @@ -89,13 +92,23 @@ export default function NormalFile(props: NormalFileProps) { return (
    - {file?.name ? fileName : props.placeholder} + + {file?.name ? fileName : props.placeholder} + {file?.status === 'progress' && renderProgress(file.percent)} - {file?.status === 'waiting' && } - {file?.url && file.status === 'success' && ( + {file?.status === 'waiting' && ( + + )} + {file?.name && file.status === 'success' && ( )} - {file?.name && file.status === 'fail' && } + {file?.name && file.status === 'fail' && ( + + )} {!disabled && ( {props.placeholder} + {props.placeholder} )} {fileListDisplay || renderFilePreviewAsText(displayFiles)} diff --git a/src/upload/type.ts b/src/upload/type.ts index 9db89826b..6e0bf63be 100644 --- a/src/upload/type.ts +++ b/src/upload/type.ts @@ -6,7 +6,7 @@ import { UploadConfig } from '../config-provider/type'; import { ButtonProps } from '../button'; -import { TNode, UploadDisplayDragEvents } from '../common'; +import { PlainObject, TNode, UploadDisplayDragEvents } from '../common'; import { MouseEvent, DragEvent } from 'react'; export interface TdUploadProps { @@ -20,7 +20,7 @@ export interface TdUploadProps { */ accept?: string; /** - * 上传接口。设接口响应数据为字段 `response`,那么 `response.error` 存在时会判断此次上传失败,并显示错误文本信息;`response.url` 会作为文件上传成功后的地址,并使用该地址显示图片 + * 上传接口。设接口响应数据为字段 `response`,那么 `response.error` 存在时会判断此次上传失败,并显示错误文本信息;`response.url` 会作为文件上传成功后的地址,并使用该地址显示图片或文件 * @default '' */ action?: string; @@ -30,16 +30,16 @@ export interface TdUploadProps { */ allowUploadDuplicateFile?: boolean; /** - * 是否选取文件后自动上传 + * 是否在选择文件后自动发起请求上传文件 * @default true */ autoUpload?: boolean; /** - * 全部文件上传之前的钩子,参数为上传的文件,返回值决定是否继续上传,若返回值为 `false` 则终止上传 + * 如果是自动上传模式 `autoUpload=true`,表示全部文件上传之前的钩子函数,函数参数为上传的文件,函数返回值决定是否继续上传,若返回值为 `false` 则终止上传。
    如果是非自动上传模式 `autoUpload=false`,则函数返回值为 `false` 时表示不触发文件变化 */ beforeAllFilesUpload?: (file: UploadFile[]) => boolean | Promise; /** - * 单文件上传之前的钩子,参数为上传的文件,返回值决定是否继续上传,若返回值为 `false` 则终止上传 + * 如果是自动上传模式 `autoUpload=true`,表示单个文件上传之前的钩子函数,若函数返回值为 `false` 则表示不上传当前文件。
    如果是非自动上传模式 `autoUpload=false`,函数返回值为 `false` 时表示从上传文件中剔除当前文件 */ beforeUpload?: (file: UploadFile) => boolean | Promise; /** @@ -47,19 +47,19 @@ export interface TdUploadProps { */ children?: TNode; /** - * 上传文件时所需的额外数据 + * 上传请求所需的额外字段,默认字段有 `file`,表示文件信息。可以添加额外的文件名字段,如:`{file_name: "custom-file-name.txt"}`。`autoUpload=true` 时有效。也可以使用 `formatRequest` 完全自定义上传请求的字段 */ - data?: Record | ((file: File) => Record); + data?: Record | ((files: UploadFile[]) => Record); /** * 是否禁用 */ disabled?: boolean; /** - * 用于自定义拖拽区域 + * 用于自定义拖拽区域,`theme=custom` 且 `draggable=true` 时有效 */ dragContent?: TNode | TNode; /** - * 是否启用拖拽上传,不同的组件风格默认值不同 + * 是否启用拖拽上传,不同的组件风格默认值不同。`theme=file` 或 `theme=image` 时有效 */ draggable?: boolean; /** @@ -67,38 +67,38 @@ export interface TdUploadProps { */ fileListDisplay?: TNode<{ files: UploadFile[]; dragEvents?: UploadDisplayDragEvents }>; /** - * 已上传文件列表,同 `value` + * 已上传文件列表,同 `value`。TS 类型:`UploadFile` * @default [] */ files?: Array; /** - * 已上传文件列表,同 `value`,非受控属性 + * 已上传文件列表,同 `value`。TS 类型:`UploadFile`,非受控属性 * @default [] */ defaultFiles?: Array; /** - * 文件上传前转换文件的数据结构,可新增或修改文件对象的属性 + * 转换文件 `UploadFile` 的数据结构,可新增或修改 `UploadFile` 的属性,注意不能删除 `UploadFile` 属性。`action` 存在时有效 */ format?: (file: File) => UploadFile; /** - * 用于新增或修改文件上传请求参数 + * 用于新增或修改文件上传请求参数。`action` 存在时有效。一个请求上传一个文件时,默认请求字段有 `file`;
    一个请求上传多个文件时,默认字段有 `file[0]/file[1]/file[2]/.../length`,其中 `length` 表示本次上传的文件数量。
    ⚠️非常注意,此处的 `file[0]/file[1]` 仅仅是一个字段名,并非表示 `file` 是一个数组,接口获取字段时注意区分。
    可以使用 `name` 定义 `file` 字段的别名,也可以使用 `formatRequest` 自定义任意字段 */ formatRequest?: (requestData: { [key: string]: any }) => { [key: string]: any }; /** - * 用于格式化文件上传后的接口响应数据,`response` 便是接口响应的原始数据。
    此函数的返回值 `error` 或 `response.error` 会作为错误文本提醒,如果存在会判定为本次上传失败。
    此函数的返回值 `url` 或 `response.url` 会作为上传成功后的链接 + * 用于格式化文件上传后的接口响应数据,`response` 便是接口响应的原始数据。`action` 存在时有效。
    此函数的返回值 `error` 或 `response.error` 会作为错误文本提醒,如果存在会判定为本次上传失败。
    此函数的返回值 `url` 或 `response.url` 会作为上传成功后的链接 */ formatResponse?: (response: any, context: FormatResponseContext) => ResponseType; /** - * 设置上传的请求头部 + * 设置上传的请求头部,`action` 存在时有效 */ headers?: { [key: string]: string }; /** - * 文件是否作为一个独立文件包,整体替换,整体删除。不允许追加文件,只允许替换文件 + * 多个文件是否作为一个独立文件包,整体替换,整体删除。不允许追加文件,只允许替换文件。`theme=file-flow` 时有效 * @default false */ isBatchUpload?: boolean; /** - * 上传组件文本语言配置,支持自定义配置组件中的全部文本 + * 上传组件文本语言配置,支持自定义配置组件中的全部文本。优先级高于全局配置中语言 */ locale?: UploadConfig; /** @@ -112,7 +112,11 @@ export interface TdUploadProps { */ method?: 'POST' | 'GET' | 'PUT' | 'OPTION' | 'PATCH' | 'post' | 'get' | 'put' | 'option' | 'patch'; /** - * 是否支持多选文件 + * 模拟进度间隔时间,单位:毫秒,默认:300。由于原始的上传请求,小文件上传进度只有 0 和 100,故而新增模拟进度,每间隔 `mockProgressDuration` 毫秒刷新一次模拟进度。小文件设置小一点,大文件设置大一点。注意:当 `useMockProgress` 为真时,当前设置有效 + */ + mockProgressDuration?: number; + /** + * 支持多文件上传 * @default false */ multiple?: boolean; @@ -127,7 +131,7 @@ export interface TdUploadProps { */ placeholder?: string; /** - * 自定义上传方法。返回值 `status` 表示上传成功或失败,`error` 或 `response.error` 表示上传失败的原因,`response` 表示请求上传成功后的返回数据,`response.url` 表示上传成功后的图片地址。示例一:`{ status: 'fail', error: '上传失败', response }`。示例二:`{ status: 'success', response: { url: 'https://tdesign.gtimg.com/site/avatar.jpg' } }` + * 自定义上传方法。返回值 `status` 表示上传成功或失败,`error` 或 `response.error` 表示上传失败的原因,`response` 表示请求上传成功后的返回数据,`response.url` 表示上传成功后的图片地址。
    示例一:`{ status: 'fail', error: '上传失败', response }`。
    示例二:`{ status: 'success', response: { url: 'https://tdesign.gtimg.com/site/avatar.jpg' } }` */ requestMethod?: (files: UploadFile | UploadFile[]) => Promise; /** @@ -136,7 +140,7 @@ export interface TdUploadProps { */ showUploadProgress?: boolean; /** - * 图片文件大小限制,单位 KB。可选单位有:`'B' | 'KB' | 'MB' | 'GB'`。示例一:`1000`。示例二:`{ size: 2, unit: 'MB', message: '图片大小不超过 {sizeLimit} MB' }` + * 图片文件大小限制,默认单位 KB。可选单位有:`'B' | 'KB' | 'MB' | 'GB'`。示例一:`1000`。示例二:`{ size: 2, unit: 'MB', message: '图片大小不超过 {sizeLimit} MB' }` */ sizeLimit?: number | SizeLimitObj; /** @@ -150,9 +154,8 @@ export interface TdUploadProps { theme?: 'custom' | 'file' | 'file-input' | 'file-flow' | 'image' | 'image-flow'; /** * 组件下方文本提示,可以使用 `status` 定义文本 - * @default '' */ - tips?: string; + tips?: TNode; /** * 触发上传的元素,`files` 指本次显示的全部文件 */ @@ -162,7 +165,7 @@ export interface TdUploadProps { */ triggerButtonProps?: ButtonProps; /** - * 是否在同一个请求中上传全部文件,默认一个请求上传一个文件 + * 是否在同一个请求中上传全部文件,默认一个请求上传一个文件。多文件上传时有效 * @default false */ uploadAllFilesInOneRequest?: boolean; @@ -171,16 +174,6 @@ export interface TdUploadProps { * @default true */ useMockProgress?: boolean; - /** - * 已上传文件列表,同 `files` - * @default [] - */ - value?: Array; - /** - * 已上传文件列表,同 `files`,非受控属性 - * @default [] - */ - defaultValue?: Array; /** * 上传请求时是否携带 cookie * @default false @@ -217,7 +210,7 @@ export interface TdUploadProps { /** * 单个文件上传成功后触发,在多文件场景下会触发多次。`context.file` 表示当前上传成功的单个文件,`context.response` 表示上传请求的返回数据 */ - onOneFileSuccess?: (context: Pick) => void; + onOneFileSuccess?: (context: Pick) => void; /** * 点击图片预览时触发,文件没有预览 */ @@ -235,11 +228,11 @@ export interface TdUploadProps { */ onSelectChange?: (files: File[], context: UploadSelectChangeContext) => void; /** - * 上传成功后触发。
    `context.currentFiles` 表示当次请求上传的文件,`context.fileList` 表示上传成功后的文件,`context.response` 表示上传请求的返回数据。
    `context.results` 表示单次选择全部文件上传成功后的响应结果,可以在这个字段存在时提醒用户上传成功或失败。
    + * 上传成功后触发。
    `context.currentFiles` 表示当次请求上传的文件(无论成功或失败),`context.fileList` 表示上传成功后的文件,`context.response` 表示上传请求的返回数据。
    `context.results` 表示单次选择全部文件上传成功后的响应结果,可以在这个字段存在时提醒用户上传成功或失败。
    */ onSuccess?: (context: SuccessContext) => void; /** - * 文件上传校验结束事件,有文件数量超出时会触发,文件大小超出限制、文件同名时会触发等场景。注意如果设置允许上传同名文件,则此事件不会触发 + * 文件上传校验结束事件,文件数量超出、文件大小超出限制、文件同名、`beforeAllFilesUpload` 返回值为假、`beforeUpload` 返回值为假等场景会触发。
    注意:如果设置允许上传同名文件,即 `allowUploadDuplicateFile=true`,则不会因为文件重名触发该事件。
    结合 `status` 和 `tips` 可以在组件中呈现不同类型的错误(或告警)提示 */ onValidate?: (context: { type: UploadValidateType; files: UploadFile[] }) => void; /** @@ -258,12 +251,12 @@ export interface UploadInstanceFunctions { */ triggerUpload: () => void; /** - * 组件实例方法,执行后默认上传未成功上传过的所有文件,也可以上传指定文件 + * 组件实例方法,默认上传未成功上传过的所有文件。带参数时,表示上传指定文件 */ uploadFiles: (files?: UploadFile[]) => void; } -export interface UploadFile { +export interface UploadFile extends PlainObject { /** * 上一次变更的时间 */ @@ -340,21 +333,23 @@ export interface TriggerContext { } export interface UploadChangeContext { - e?: MouseEvent | ProgressEvent; + e?: MouseEvent | ProgressEvent; response?: any; trigger: UploadChangeTrigger; index?: number; file?: UploadFile; + files?: UploadFile[]; } export type UploadChangeTrigger = 'add' | 'remove' | 'abort' | 'progress-success' | 'progress' | 'progress-fail'; export interface UploadFailContext { - e: ProgressEvent; + e?: ProgressEvent; failedFiles: UploadFile[]; currentFiles: UploadFile[]; response?: any; file: UploadFile; + XMLHttpRequest?: XMLHttpRequest; } export interface ProgressContext { @@ -371,7 +366,7 @@ export type UploadProgressType = 'real' | 'mock'; export interface UploadRemoveContext { index?: number; file?: UploadFile; - e: MouseEvent; + e: MouseEvent; } export interface UploadSelectChangeContext { @@ -385,6 +380,12 @@ export interface SuccessContext { currentFiles?: UploadFile[]; response?: any; results?: SuccessContext[]; + XMLHttpRequest?: XMLHttpRequest; } -export type UploadValidateType = 'FILE_OVER_SIZE_LIMIT' | 'FILES_OVER_LENGTH_LIMIT' | 'FILTER_FILE_SAME_NAME'; +export type UploadValidateType = + | 'FILE_OVER_SIZE_LIMIT' + | 'FILES_OVER_LENGTH_LIMIT' + | 'FILTER_FILE_SAME_NAME' + | 'BEFORE_ALL_FILES_UPLOAD' + | 'CUSTOM_BEFORE_UPLOAD'; diff --git a/src/upload/upload.en-US.md b/src/upload/upload.en-US.md index aa000254d..e83fc83ed 100644 --- a/src/upload/upload.en-US.md +++ b/src/upload/upload.en-US.md @@ -12,13 +12,13 @@ accept | String | - | \- | N action | String | - | upload action url | N allowUploadDuplicateFile | Boolean | false | \- | N autoUpload | Boolean | true | \- | N -beforeAllFilesUpload | Function | - | before all files upload, return false can stop upload continue。Typescript:`(file: UploadFile[]) => boolean \| Promise` | N +beforeAllFilesUpload | Function | - | before all files upload, return false can stop uploading file。Typescript:`(file: UploadFile[]) => boolean \| Promise` | N beforeUpload | Function | - | Typescript:`(file: UploadFile) => boolean \| Promise` | N children | TNode | - | Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N -data | Object | - | Typescript:`Record \| ((file: File) => Record)` | N +data | Object | - | Typescript:`Record \| ((files: UploadFile[]) => Record)` | N disabled | Boolean | - | \- | N -dragContent | TNode | - | drag content。Typescript:`TNode \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N -draggable | Boolean | undefined | \- | N +dragContent | TNode | - | define drag content nodes, it works on `theme=custom`。Typescript:`TNode \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +draggable | Boolean | undefined | if drag uploading allowed, works on `theme=file` or `theme=image` | N fileListDisplay | TElement | - | Typescript:`TNode<{ files: UploadFile[]; dragEvents?: UploadDisplayDragEvents }>`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N files | Array | [] | Typescript:`Array` | N defaultFiles | Array | [] | uncontrolled property。Typescript:`Array` | N @@ -27,9 +27,10 @@ formatRequest | Function | - | Typescript:`(requestData: { [key: string]: any formatResponse | Function | - | Typescript:`(response: any, context: FormatResponseContext) => ResponseType ` `type ResponseType = { error?: string; url?: string } & Record` `interface FormatResponseContext { file: UploadFile; currentFiles?: UploadFile[] }`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts) | N headers | Object | - | Typescript:`{[key: string]: string}` | N isBatchUpload | Boolean | false | \- | N -locale | Object | - | Typescript:`UploadConfig` `import { UploadConfig } from '../config-provider/type'`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts) | N +locale | Object | - | upload language config, priority of `locale` is higher than global language config。Typescript:`UploadConfig` `import { UploadConfig } from '../config-provider/type'`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts) | N max | Number | 0 | \- | N method | String | POST | options:POST/GET/PUT/OPTION/PATCH/post/get/put/option/patch | N +mockProgressDuration | Number | - | \- | N multiple | Boolean | false | \- | N name | String | file | \- | N placeholder | String | - | \- | N @@ -38,28 +39,26 @@ showUploadProgress | Boolean | true | \- | N sizeLimit | Number / Object | - | Typescript:`number \| SizeLimitObj` `interface SizeLimitObj { size: number; unit: SizeUnit ; message?: string }` `type SizeUnitArray = ['B', 'KB', 'MB', 'GB']` `type SizeUnit = SizeUnitArray[number]`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts) | N status | String | - | tips status。options:default/success/warning/error | N theme | String | file | options:custom/file/file-input/file-flow/image/image-flow | N -tips | String | - | tips text below upload component, define it's color with `status` | N +tips | TNode | - | tips text below upload component, define it's color with `status`。Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N trigger | TElement | - | Typescript:`TNode` `interface TriggerContext { dragActive?: boolean; files: UploadFile[] }`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts)。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts) | N triggerButtonProps | Object | - | Typescript:`ButtonProps`,[Button API Documents](./button?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts) | N uploadAllFilesInOneRequest | Boolean | false | \- | N useMockProgress | Boolean | true | \- | N -value | Array | [] | Typescript:`Array` | N -defaultValue | Array | [] | uncontrolled property。Typescript:`Array` | N withCredentials | Boolean | false | \- | N onCancelUpload | Function | | Typescript:`() => void`
    | N -onChange | Function | | Typescript:`(value: Array, context: UploadChangeContext) => void`
    [see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts)。
    `interface UploadChangeContext { e?: MouseEvent \| ProgressEvent; response?: any; trigger: UploadChangeTrigger; index?: number; file?: UploadFile }`

    `type UploadChangeTrigger = 'add' \| 'remove' \| 'abort' \| 'progress-success' \| 'progress' \| 'progress-fail'`
    | N +onChange | Function | | Typescript:`(value: Array, context: UploadChangeContext) => void`
    [see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts)。
    `interface UploadChangeContext { e?: MouseEvent \| ProgressEvent; response?: any; trigger: UploadChangeTrigger; index?: number; file?: UploadFile; files?: UploadFile[] }`

    `type UploadChangeTrigger = 'add' \| 'remove' \| 'abort' \| 'progress-success' \| 'progress' \| 'progress-fail'`
    | N onDragenter | Function | | Typescript:`(context: { e: DragEvent }) => void`
    | N onDragleave | Function | | Typescript:`(context: { e: DragEvent }) => void`
    | N onDrop | Function | | Typescript:`(context: { e: DragEvent }) => void`
    | N -onFail | Function | | Typescript:`(options: UploadFailContext) => void`
    `response.error` used for error tips, `formatResponse` can format `response`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts)。
    `interface UploadFailContext { e: ProgressEvent; failedFiles: UploadFile[]; currentFiles: UploadFile[]; response?: any; file: UploadFile }`
    | N +onFail | Function | | Typescript:`(options: UploadFailContext) => void`
    `response.error` used for error tips, `formatResponse` can format `response`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts)。
    `interface UploadFailContext { e?: ProgressEvent; failedFiles: UploadFile[]; currentFiles: UploadFile[]; response?: any; file: UploadFile; XMLHttpRequest?: XMLHttpRequest}`
    | N onOneFileFail | Function | | Typescript:`(options: UploadFailContext) => void`
    trigger on one file upload failed | N -onOneFileSuccess | Function | | Typescript:`(context: Pick) => void`
    | N +onOneFileSuccess | Function | | Typescript:`(context: Pick) => void`
    | N onPreview | Function | | Typescript:`(options: { file: UploadFile; index: number; e: MouseEvent }) => void`
    | N onProgress | Function | | Typescript:`(options: ProgressContext) => void`
    [see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts)。
    `interface ProgressContext { e?: ProgressEvent; file?: UploadFile; currentFiles: UploadFile[]; percent: number; type: UploadProgressType; XMLHttpRequest?: XMLHttpRequest }`

    `type UploadProgressType = 'real' \| 'mock'`
    | N onRemove | Function | | Typescript:`(context: UploadRemoveContext) => void`
    [see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts)。
    `interface UploadRemoveContext { index?: number; file?: UploadFile; e: MouseEvent }`
    | N onSelectChange | Function | | Typescript:`(files: File[], context: UploadSelectChangeContext) => void`
    trigger after file choose and before upload。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts)。
    `interface UploadSelectChangeContext { currentSelectedFiles: UploadFile[] }`
    | N -onSuccess | Function | | Typescript:`(context: SuccessContext) => void`
    [see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts)。
    `interface SuccessContext { e?: ProgressEvent; file?: UploadFile; fileList?: UploadFile[]; currentFiles?: UploadFile[]; response?: any; results?: SuccessContext[] }`
    | N -onValidate | Function | | Typescript:`(context: { type: UploadValidateType, files: UploadFile[] }) => void`
    trigger on length over limit, or trigger on file size over limit。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts)。
    `type UploadValidateType = 'FILE_OVER_SIZE_LIMIT' \| 'FILES_OVER_LENGTH_LIMIT' \| 'FILTER_FILE_SAME_NAME'`
    | N +onSuccess | Function | | Typescript:`(context: SuccessContext) => void`
    [see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts)。
    `interface SuccessContext { e?: ProgressEvent; file?: UploadFile; fileList?: UploadFile[]; currentFiles?: UploadFile[]; response?: any; results?: SuccessContext[]; XMLHttpRequest?: XMLHttpRequest }`
    | N +onValidate | Function | | Typescript:`(context: { type: UploadValidateType, files: UploadFile[] }) => void`
    trigger on length over limit, or trigger on file size over limit。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts)。
    `type UploadValidateType = 'FILE_OVER_SIZE_LIMIT' \| 'FILES_OVER_LENGTH_LIMIT' \| 'FILTER_FILE_SAME_NAME' \| 'BEFORE_ALL_FILES_UPLOAD' \| 'CUSTOM_BEFORE_UPLOAD'`
    | N onWaitingUploadFilesChange | Function | | Typescript:`(context: { files: Array, trigger: 'validate' \| 'remove' \| 'uploaded' }) => void`
    trigger on waiting upload files changed | N ### UploadInstanceFunctions 组件实例方法 @@ -85,3 +84,4 @@ status | String | - | Typescript:` 'success' \| 'fail' \| 'progress' \| 'waiti type | String | - | \- | N uploadTime | String | - | upload time | N url | String | - | \- | N +`PlainObject` | \- | - | \- | N diff --git a/src/upload/upload.md b/src/upload/upload.md index 8a55a2495..666ea5f52 100644 --- a/src/upload/upload.md +++ b/src/upload/upload.md @@ -9,57 +9,56 @@ className | String | - | 类名 | N style | Object | - | 样式,TS 类型:`React.CSSProperties` | N abridgeName | Array | - | 文件名过长时,需要省略中间的文本,保留首尾文本。示例:[10, 7],表示首尾分别保留的文本长度。TS 类型:`Array` | N accept | String | - | 接受上传的文件类型,[查看 W3C示例](https://www.w3schools.com/tags/att_input_accept.asp),[查看 MDN 示例](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/Input/file) | N -action | String | - | 上传接口。设接口响应数据为字段 `response`,那么 `response.error` 存在时会判断此次上传失败,并显示错误文本信息;`response.url` 会作为文件上传成功后的地址,并使用该地址显示图片 | N +action | String | - | 上传接口。设接口响应数据为字段 `response`,那么 `response.error` 存在时会判断此次上传失败,并显示错误文本信息;`response.url` 会作为文件上传成功后的地址,并使用该地址显示图片或文件 | N allowUploadDuplicateFile | Boolean | false | 是否允许重复上传相同文件名的文件 | N -autoUpload | Boolean | true | 是否选取文件后自动上传 | N -beforeAllFilesUpload | Function | - | 全部文件上传之前的钩子,参数为上传的文件,返回值决定是否继续上传,若返回值为 `false` 则终止上传。TS 类型:`(file: UploadFile[]) => boolean \| Promise` | N -beforeUpload | Function | - | 单文件上传之前的钩子,参数为上传的文件,返回值决定是否继续上传,若返回值为 `false` 则终止上传。TS 类型:`(file: UploadFile) => boolean \| Promise` | N +autoUpload | Boolean | true | 是否在选择文件后自动发起请求上传文件 | N +beforeAllFilesUpload | Function | - | 如果是自动上传模式 `autoUpload=true`,表示全部文件上传之前的钩子函数,函数参数为上传的文件,函数返回值决定是否继续上传,若返回值为 `false` 则终止上传。
    如果是非自动上传模式 `autoUpload=false`,则函数返回值为 `false` 时表示不触发文件变化。TS 类型:`(file: UploadFile[]) => boolean \| Promise` | N +beforeUpload | Function | - | 如果是自动上传模式 `autoUpload=true`,表示单个文件上传之前的钩子函数,若函数返回值为 `false` 则表示不上传当前文件。
    如果是非自动上传模式 `autoUpload=false`,函数返回值为 `false` 时表示从上传文件中剔除当前文件。TS 类型:`(file: UploadFile) => boolean \| Promise` | N children | TNode | - | 非拖拽场景,指触发上传的元素,如:“选择文件”。如果是拖拽场景,则是指拖拽区域。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N -data | Object | - | 上传文件时所需的额外数据。TS 类型:`Record \| ((file: File) => Record)` | N +data | Object | - | 上传请求所需的额外字段,默认字段有 `file`,表示文件信息。可以添加额外的文件名字段,如:`{file_name: "custom-file-name.txt"}`。`autoUpload=true` 时有效。也可以使用 `formatRequest` 完全自定义上传请求的字段。TS 类型:`Record \| ((files: UploadFile[]) => Record)` | N disabled | Boolean | - | 是否禁用 | N -dragContent | TNode | - | 用于自定义拖拽区域。TS 类型:`TNode \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N -draggable | Boolean | undefined | 是否启用拖拽上传,不同的组件风格默认值不同 | N +dragContent | TNode | - | 用于自定义拖拽区域,`theme=custom` 且 `draggable=true` 时有效。TS 类型:`TNode \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +draggable | Boolean | undefined | 是否启用拖拽上传,不同的组件风格默认值不同。`theme=file` 或 `theme=image` 时有效 | N fileListDisplay | TElement | - | 用于完全自定义文件列表内容。TS 类型:`TNode<{ files: UploadFile[]; dragEvents?: UploadDisplayDragEvents }>`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N -files | Array | [] | 已上传文件列表,同 `value`。TS 类型:`Array` | N -defaultFiles | Array | [] | 已上传文件列表,同 `value`。非受控属性。TS 类型:`Array` | N -format | Function | - | 文件上传前转换文件的数据结构,可新增或修改文件对象的属性。TS 类型:`(file: File) => UploadFile` | N -formatRequest | Function | - | 用于新增或修改文件上传请求参数。TS 类型:`(requestData: { [key: string]: any }) => { [key: string]: any }` | N -formatResponse | Function | - | 用于格式化文件上传后的接口响应数据,`response` 便是接口响应的原始数据。
    此函数的返回值 `error` 或 `response.error` 会作为错误文本提醒,如果存在会判定为本次上传失败。
    此函数的返回值 `url` 或 `response.url` 会作为上传成功后的链接。TS 类型:`(response: any, context: FormatResponseContext) => ResponseType ` `type ResponseType = { error?: string; url?: string } & Record` `interface FormatResponseContext { file: UploadFile; currentFiles?: UploadFile[] }`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts) | N -headers | Object | - | 设置上传的请求头部。TS 类型:`{[key: string]: string}` | N -isBatchUpload | Boolean | false | 文件是否作为一个独立文件包,整体替换,整体删除。不允许追加文件,只允许替换文件 | N -locale | Object | - | 上传组件文本语言配置,支持自定义配置组件中的全部文本。TS 类型:`UploadConfig` `import { UploadConfig } from '../config-provider/type'`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts) | N +files | Array | [] | 已上传文件列表,同 `value`。TS 类型:`UploadFile`。TS 类型:`Array` | N +defaultFiles | Array | [] | 已上传文件列表,同 `value`。TS 类型:`UploadFile`。非受控属性。TS 类型:`Array` | N +format | Function | - | 转换文件 `UploadFile` 的数据结构,可新增或修改 `UploadFile` 的属性,注意不能删除 `UploadFile` 属性。`action` 存在时有效。TS 类型:`(file: File) => UploadFile` | N +formatRequest | Function | - | 用于新增或修改文件上传请求参数。`action` 存在时有效。一个请求上传一个文件时,默认请求字段有 `file`;
    一个请求上传多个文件时,默认字段有 `file[0]/file[1]/file[2]/.../length`,其中 `length` 表示本次上传的文件数量。
    ⚠️非常注意,此处的 `file[0]/file[1]` 仅仅是一个字段名,并非表示 `file` 是一个数组,接口获取字段时注意区分。
    可以使用 `name` 定义 `file` 字段的别名,也可以使用 `formatRequest` 自定义任意字段。TS 类型:`(requestData: { [key: string]: any }) => { [key: string]: any }` | N +formatResponse | Function | - | 用于格式化文件上传后的接口响应数据,`response` 便是接口响应的原始数据。`action` 存在时有效。
    此函数的返回值 `error` 或 `response.error` 会作为错误文本提醒,如果存在会判定为本次上传失败。
    此函数的返回值 `url` 或 `response.url` 会作为上传成功后的链接。TS 类型:`(response: any, context: FormatResponseContext) => ResponseType ` `type ResponseType = { error?: string; url?: string } & Record` `interface FormatResponseContext { file: UploadFile; currentFiles?: UploadFile[] }`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts) | N +headers | Object | - | 设置上传的请求头部,`action` 存在时有效。TS 类型:`{[key: string]: string}` | N +isBatchUpload | Boolean | false | 多个文件是否作为一个独立文件包,整体替换,整体删除。不允许追加文件,只允许替换文件。`theme=file-flow` 时有效 | N +locale | Object | - | 上传组件文本语言配置,支持自定义配置组件中的全部文本。优先级高于全局配置中语言。TS 类型:`UploadConfig` `import { UploadConfig } from '../config-provider/type'`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts) | N max | Number | 0 | 用于控制文件上传数量,值为 0 则不限制 | N method | String | POST | HTTP 请求类型。可选项:POST/GET/PUT/OPTION/PATCH/post/get/put/option/patch | N -multiple | Boolean | false | 是否支持多选文件 | N +mockProgressDuration | Number | - | 模拟进度间隔时间,单位:毫秒,默认:300。由于原始的上传请求,小文件上传进度只有 0 和 100,故而新增模拟进度,每间隔 `mockProgressDuration` 毫秒刷新一次模拟进度。小文件设置小一点,大文件设置大一点。注意:当 `useMockProgress` 为真时,当前设置有效 | N +multiple | Boolean | false | 支持多文件上传 | N name | String | file | 文件上传时的名称 | N placeholder | String | - | 占位符 | N -requestMethod | Function | - | 自定义上传方法。返回值 `status` 表示上传成功或失败,`error` 或 `response.error` 表示上传失败的原因,`response` 表示请求上传成功后的返回数据,`response.url` 表示上传成功后的图片地址。示例一:`{ status: 'fail', error: '上传失败', response }`。示例二:`{ status: 'success', response: { url: 'https://tdesign.gtimg.com/site/avatar.jpg' } }`。TS 类型:`(files: UploadFile \| UploadFile[]) => Promise` `interface RequestMethodResponse { status: 'success' \| 'fail'; error?: string; response: { url?: string; [key: string]: any } }`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts) | N +requestMethod | Function | - | 自定义上传方法。返回值 `status` 表示上传成功或失败,`error` 或 `response.error` 表示上传失败的原因,`response` 表示请求上传成功后的返回数据,`response.url` 表示上传成功后的图片地址。
    示例一:`{ status: 'fail', error: '上传失败', response }`。
    示例二:`{ status: 'success', response: { url: 'https://tdesign.gtimg.com/site/avatar.jpg' } }`。TS 类型:`(files: UploadFile \| UploadFile[]) => Promise` `interface RequestMethodResponse { status: 'success' \| 'fail'; error?: string; response: { url?: string; [key: string]: any } }`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts) | N showUploadProgress | Boolean | true | 是否显示上传进度 | N -sizeLimit | Number / Object | - | 图片文件大小限制,单位 KB。可选单位有:`'B' \| 'KB' \| 'MB' \| 'GB'`。示例一:`1000`。示例二:`{ size: 2, unit: 'MB', message: '图片大小不超过 {sizeLimit} MB' }`。TS 类型:`number \| SizeLimitObj` `interface SizeLimitObj { size: number; unit: SizeUnit ; message?: string }` `type SizeUnitArray = ['B', 'KB', 'MB', 'GB']` `type SizeUnit = SizeUnitArray[number]`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts) | N +sizeLimit | Number / Object | - | 图片文件大小限制,默认单位 KB。可选单位有:`'B' \| 'KB' \| 'MB' \| 'GB'`。示例一:`1000`。示例二:`{ size: 2, unit: 'MB', message: '图片大小不超过 {sizeLimit} MB' }`。TS 类型:`number \| SizeLimitObj` `interface SizeLimitObj { size: number; unit: SizeUnit ; message?: string }` `type SizeUnitArray = ['B', 'KB', 'MB', 'GB']` `type SizeUnit = SizeUnitArray[number]`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts) | N status | String | - | 文件上传提示文本状态。可选项:default/success/warning/error | N theme | String | file | 组件风格。custom 表示完全自定义风格;file 表示默认文件上传风格;file-input 表示输入框形式的文件上传;file-flow 表示文件批量上传;image 表示默认图片上传风格;image-flow 表示图片批量上传。可选项:custom/file/file-input/file-flow/image/image-flow | N -tips | String | - | 组件下方文本提示,可以使用 `status` 定义文本 | N +tips | TNode | - | 组件下方文本提示,可以使用 `status` 定义文本。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N trigger | TElement | - | 触发上传的元素,`files` 指本次显示的全部文件。TS 类型:`TNode` `interface TriggerContext { dragActive?: boolean; files: UploadFile[] }`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts)。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts) | N triggerButtonProps | Object | - | 透传选择按钮全部属性。TS 类型:`ButtonProps`,[Button API Documents](./button?tab=api)。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts) | N -uploadAllFilesInOneRequest | Boolean | false | 是否在同一个请求中上传全部文件,默认一个请求上传一个文件 | N +uploadAllFilesInOneRequest | Boolean | false | 是否在同一个请求中上传全部文件,默认一个请求上传一个文件。多文件上传时有效 | N useMockProgress | Boolean | true | 是否在请求时间超过 300ms 后显示模拟进度。上传进度有模拟进度和真实进度两种。一般大小的文件上传,真实的上传进度只有 0 和 100,不利于交互呈现,因此组件内置模拟上传进度。真实上传进度一般用于大文件上传。 | N -value | Array | [] | 已上传文件列表,同 `files`。TS 类型:`Array` | N -defaultValue | Array | [] | 已上传文件列表,同 `files`。非受控属性。TS 类型:`Array` | N withCredentials | Boolean | false | 上传请求时是否携带 cookie | N onCancelUpload | Function | | TS 类型:`() => void`
    点击「取消上传」时触发 | N -onChange | Function | | TS 类型:`(value: Array, context: UploadChangeContext) => void`
    已上传文件列表发生变化时触发,`trigger` 表示触发本次的来源。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts)。
    `interface UploadChangeContext { e?: MouseEvent \| ProgressEvent; response?: any; trigger: UploadChangeTrigger; index?: number; file?: UploadFile }`

    `type UploadChangeTrigger = 'add' \| 'remove' \| 'abort' \| 'progress-success' \| 'progress' \| 'progress-fail'`
    | N +onChange | Function | | TS 类型:`(value: Array, context: UploadChangeContext) => void`
    已上传文件列表发生变化时触发,`trigger` 表示触发本次的来源。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts)。
    `interface UploadChangeContext { e?: MouseEvent \| ProgressEvent; response?: any; trigger: UploadChangeTrigger; index?: number; file?: UploadFile; files?: UploadFile[] }`

    `type UploadChangeTrigger = 'add' \| 'remove' \| 'abort' \| 'progress-success' \| 'progress' \| 'progress-fail'`
    | N onDragenter | Function | | TS 类型:`(context: { e: DragEvent }) => void`
    进入拖拽区域时触发 | N onDragleave | Function | | TS 类型:`(context: { e: DragEvent }) => void`
    离开拖拽区域时触发 | N onDrop | Function | | TS 类型:`(context: { e: DragEvent }) => void`
    拖拽结束时触发 | N -onFail | Function | | TS 类型:`(options: UploadFailContext) => void`
    上传失败后触发。`response` 指接口响应结果,`response.error` 会作为错误文本提醒。如果希望判定为上传失败,但接口响应数据不包含 `error` 字段,可以使用 `formatResponse` 格式化 `response` 数据结构。如果是多文件多请求上传场景,请到事件 `onOneFileFail` 中查看 `response`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts)。
    `interface UploadFailContext { e: ProgressEvent; failedFiles: UploadFile[]; currentFiles: UploadFile[]; response?: any; file: UploadFile }`
    | N +onFail | Function | | TS 类型:`(options: UploadFailContext) => void`
    上传失败后触发。`response` 指接口响应结果,`response.error` 会作为错误文本提醒。如果希望判定为上传失败,但接口响应数据不包含 `error` 字段,可以使用 `formatResponse` 格式化 `response` 数据结构。如果是多文件多请求上传场景,请到事件 `onOneFileFail` 中查看 `response`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts)。
    `interface UploadFailContext { e?: ProgressEvent; failedFiles: UploadFile[]; currentFiles: UploadFile[]; response?: any; file: UploadFile; XMLHttpRequest?: XMLHttpRequest}`
    | N onOneFileFail | Function | | TS 类型:`(options: UploadFailContext) => void`
    多文件/图片场景下,单个文件上传失败后触发,如果一个请求上传一个文件,则会触发多次。单文件/图片不会触发 | N -onOneFileSuccess | Function | | TS 类型:`(context: Pick) => void`
    单个文件上传成功后触发,在多文件场景下会触发多次。`context.file` 表示当前上传成功的单个文件,`context.response` 表示上传请求的返回数据 | N +onOneFileSuccess | Function | | TS 类型:`(context: Pick) => void`
    单个文件上传成功后触发,在多文件场景下会触发多次。`context.file` 表示当前上传成功的单个文件,`context.response` 表示上传请求的返回数据 | N onPreview | Function | | TS 类型:`(options: { file: UploadFile; index: number; e: MouseEvent }) => void`
    点击图片预览时触发,文件没有预览 | N onProgress | Function | | TS 类型:`(options: ProgressContext) => void`
    上传进度变化时触发,真实进度和模拟进度都会触发。`type=real` 表示真实上传进度,`type=mock` 表示模拟上传进度。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts)。
    `interface ProgressContext { e?: ProgressEvent; file?: UploadFile; currentFiles: UploadFile[]; percent: number; type: UploadProgressType; XMLHttpRequest?: XMLHttpRequest }`

    `type UploadProgressType = 'real' \| 'mock'`
    | N onRemove | Function | | TS 类型:`(context: UploadRemoveContext) => void`
    移除文件时触发。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts)。
    `interface UploadRemoveContext { index?: number; file?: UploadFile; e: MouseEvent }`
    | N onSelectChange | Function | | TS 类型:`(files: File[], context: UploadSelectChangeContext) => void`
    选择文件或图片之后,上传之前,触发该事件。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts)。
    `interface UploadSelectChangeContext { currentSelectedFiles: UploadFile[] }`
    | N -onSuccess | Function | | TS 类型:`(context: SuccessContext) => void`
    上传成功后触发。
    `context.currentFiles` 表示当次请求上传的文件,`context.fileList` 表示上传成功后的文件,`context.response` 表示上传请求的返回数据。
    `context.results` 表示单次选择全部文件上传成功后的响应结果,可以在这个字段存在时提醒用户上传成功或失败。
    。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts)。
    `interface SuccessContext { e?: ProgressEvent; file?: UploadFile; fileList?: UploadFile[]; currentFiles?: UploadFile[]; response?: any; results?: SuccessContext[] }`
    | N -onValidate | Function | | TS 类型:`(context: { type: UploadValidateType, files: UploadFile[] }) => void`
    文件上传校验结束事件,有文件数量超出时会触发,文件大小超出限制、文件同名时会触发等场景。注意如果设置允许上传同名文件,则此事件不会触发。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts)。
    `type UploadValidateType = 'FILE_OVER_SIZE_LIMIT' \| 'FILES_OVER_LENGTH_LIMIT' \| 'FILTER_FILE_SAME_NAME'`
    | N +onSuccess | Function | | TS 类型:`(context: SuccessContext) => void`
    上传成功后触发。
    `context.currentFiles` 表示当次请求上传的文件(无论成功或失败),`context.fileList` 表示上传成功后的文件,`context.response` 表示上传请求的返回数据。
    `context.results` 表示单次选择全部文件上传成功后的响应结果,可以在这个字段存在时提醒用户上传成功或失败。
    。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts)。
    `interface SuccessContext { e?: ProgressEvent; file?: UploadFile; fileList?: UploadFile[]; currentFiles?: UploadFile[]; response?: any; results?: SuccessContext[]; XMLHttpRequest?: XMLHttpRequest }`
    | N +onValidate | Function | | TS 类型:`(context: { type: UploadValidateType, files: UploadFile[] }) => void`
    文件上传校验结束事件,文件数量超出、文件大小超出限制、文件同名、`beforeAllFilesUpload` 返回值为假、`beforeUpload` 返回值为假等场景会触发。
    注意:如果设置允许上传同名文件,即 `allowUploadDuplicateFile=true`,则不会因为文件重名触发该事件。
    结合 `status` 和 `tips` 可以在组件中呈现不同类型的错误(或告警)提示。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts)。
    `type UploadValidateType = 'FILE_OVER_SIZE_LIMIT' \| 'FILES_OVER_LENGTH_LIMIT' \| 'FILTER_FILE_SAME_NAME' \| 'BEFORE_ALL_FILES_UPLOAD' \| 'CUSTOM_BEFORE_UPLOAD'`
    | N onWaitingUploadFilesChange | Function | | TS 类型:`(context: { files: Array, trigger: 'validate' \| 'remove' \| 'uploaded' }) => void`
    待上传文件列表发生变化时触发。`context.files` 表示事件参数为待上传文件,`context.trigger` 引起此次变化的触发来源 | N ### UploadInstanceFunctions 组件实例方法 @@ -69,7 +68,7 @@ onWaitingUploadFilesChange | Function | | TS 类型:`(context: { files: Array className | String | - | 类名 | N style | Object | - | 样式,TS 类型:`React.CSSProperties` | N triggerUpload | \- | \- | 必需。组件实例方法,打开文件选择器 -uploadFiles | `(files?: UploadFile[])` | \- | 必需。组件实例方法,执行后默认上传未成功上传过的所有文件,也可以上传指定文件 +uploadFiles | `(files?: UploadFile[])` | \- | 必需。组件实例方法,默认上传未成功上传过的所有文件。带参数时,表示上传指定文件 ### UploadFile @@ -85,3 +84,4 @@ status | String | - | 文件上传状态:上传成功,上传失败,上传 type | String | - | 文件类型 | N uploadTime | String | - | 上传时间 | N url | String | - | 文件上传成功后的下载/访问地址 | N +`PlainObject` | \- | - | 继承 `PlainObject` 中的全部 API | N diff --git a/src/upload/upload.tsx b/src/upload/upload.tsx index 0a63e3d51..6535100a0 100644 --- a/src/upload/upload.tsx +++ b/src/upload/upload.tsx @@ -28,6 +28,7 @@ function TdUpload(props: UploadProps, ref: uploading, tipsClasses, errorClasses, + placeholderClass, inputRef, disabled, onRemove, @@ -82,8 +83,10 @@ function TdUpload(props: UploadProps, ref: classPrefix, tipsClasses, errorClasses, + placeholderClass, locale, autoUpload: props.autoUpload, + showUploadProgress: props.showUploadProgress, fileListDisplay: props.fileListDisplay, onRemove, }; diff --git a/test/snap/__snapshots__/csr.test.jsx.snap b/test/snap/__snapshots__/csr.test.jsx.snap index 161a236f2..d95fe195c 100644 --- a/test/snap/__snapshots__/csr.test.jsx.snap +++ b/test/snap/__snapshots__/csr.test.jsx.snap @@ -99740,9 +99740,7 @@ exports[`csr snapshot test > csr test src/image/_example/extra-always.jsx 1`] =
    - 图片加载中 -
    + />
    csr test src/image/_example/extra-always.jsx 1`] =
    - 图片加载中 -
    + />
    csr test src/image/_example/extra-always.jsx 1`] =
    - 图片加载中 -
    + />
    csr test src/image/_example/extra-always.jsx 1`] =
    - 图片加载中 -
    + />
    csr test src/image/_example/extra-hover.jsx 1`] = `
    - 图片加载中 -
    + />
    csr test src/image/_example/extra-hover.jsx 1`] = `
    - 图片加载中 -
    + />
    csr test src/image/_example/fill-mode.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -100455,9 +100441,7 @@ exports[`csr snapshot test > csr test src/image/_example/fill-mode.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -100521,9 +100505,7 @@ exports[`csr snapshot test > csr test src/image/_example/fill-mode.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -100587,9 +100569,7 @@ exports[`csr snapshot test > csr test src/image/_example/fill-mode.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -100653,9 +100633,7 @@ exports[`csr snapshot test > csr test src/image/_example/fill-mode.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -100727,9 +100705,7 @@ exports[`csr snapshot test > csr test src/image/_example/fill-mode.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -100793,9 +100769,7 @@ exports[`csr snapshot test > csr test src/image/_example/fill-mode.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -100859,9 +100833,7 @@ exports[`csr snapshot test > csr test src/image/_example/fill-mode.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -100925,9 +100897,7 @@ exports[`csr snapshot test > csr test src/image/_example/fill-mode.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -100991,9 +100961,7 @@ exports[`csr snapshot test > csr test src/image/_example/fill-mode.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -101137,9 +101105,7 @@ exports[`csr snapshot test > csr test src/image/_example/fill-position.jsx 1`] =
    - 图片加载中 -
    + /> @@ -101203,9 +101169,7 @@ exports[`csr snapshot test > csr test src/image/_example/fill-position.jsx 1`] =
    - 图片加载中 -
    + /> @@ -101269,9 +101233,7 @@ exports[`csr snapshot test > csr test src/image/_example/fill-position.jsx 1`] =
    - 图片加载中 -
    + /> @@ -101344,9 +101306,7 @@ exports[`csr snapshot test > csr test src/image/_example/fill-position.jsx 1`] =
    - 图片加载中 -
    + /> @@ -101410,9 +101370,7 @@ exports[`csr snapshot test > csr test src/image/_example/fill-position.jsx 1`] =
    - 图片加载中 -
    + /> @@ -101485,9 +101443,7 @@ exports[`csr snapshot test > csr test src/image/_example/fill-position.jsx 1`] =
    - 图片加载中 -
    + /> @@ -101551,9 +101507,7 @@ exports[`csr snapshot test > csr test src/image/_example/fill-position.jsx 1`] =
    - 图片加载中 -
    + /> @@ -101617,9 +101571,7 @@ exports[`csr snapshot test > csr test src/image/_example/fill-position.jsx 1`] =
    - 图片加载中 -
    + /> @@ -101690,9 +101642,7 @@ exports[`csr snapshot test > csr test src/image/_example/fill-position.jsx 1`] =
    - 图片加载中 -
    + /> @@ -101756,9 +101706,7 @@ exports[`csr snapshot test > csr test src/image/_example/fill-position.jsx 1`] =
    - 图片加载中 -
    + /> @@ -101841,9 +101789,7 @@ exports[`csr snapshot test > csr test src/image/_example/fill-position.jsx 1`] =
    - 图片加载中 -
    + /> @@ -101907,9 +101853,7 @@ exports[`csr snapshot test > csr test src/image/_example/fill-position.jsx 1`] =
    - 图片加载中 -
    + /> @@ -101973,9 +101917,7 @@ exports[`csr snapshot test > csr test src/image/_example/fill-position.jsx 1`] =
    - 图片加载中 -
    + /> @@ -102048,9 +101990,7 @@ exports[`csr snapshot test > csr test src/image/_example/fill-position.jsx 1`] =
    - 图片加载中 -
    + /> @@ -102114,9 +102054,7 @@ exports[`csr snapshot test > csr test src/image/_example/fill-position.jsx 1`] =
    - 图片加载中 -
    + /> @@ -102189,9 +102127,7 @@ exports[`csr snapshot test > csr test src/image/_example/fill-position.jsx 1`] =
    - 图片加载中 -
    + /> @@ -102255,9 +102191,7 @@ exports[`csr snapshot test > csr test src/image/_example/fill-position.jsx 1`] =
    - 图片加载中 -
    + /> @@ -102321,9 +102255,7 @@ exports[`csr snapshot test > csr test src/image/_example/fill-position.jsx 1`] =
    - 图片加载中 -
    + /> @@ -102394,9 +102326,7 @@ exports[`csr snapshot test > csr test src/image/_example/fill-position.jsx 1`] =
    - 图片加载中 -
    + /> @@ -102460,9 +102390,7 @@ exports[`csr snapshot test > csr test src/image/_example/fill-position.jsx 1`] =
    - 图片加载中 -
    + /> @@ -102592,9 +102520,7 @@ exports[`csr snapshot test > csr test src/image/_example/gallery-cover.jsx 1`] =
    - 图片加载中 -
    + />
    csr test src/image/_example/gallery-cover.jsx 1`] =
    - 图片加载中 -
    + />
    csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -102856,9 +102778,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -102908,9 +102828,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -102960,9 +102878,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -103012,9 +102928,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -103064,9 +102978,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -103116,9 +103028,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -103168,9 +103078,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -103220,9 +103128,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -103272,9 +103178,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -103324,9 +103228,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -103376,9 +103278,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -103428,9 +103328,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -103480,9 +103378,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -103532,9 +103428,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -103584,9 +103478,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -103636,9 +103528,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -103688,9 +103578,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -103740,9 +103628,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -103792,9 +103678,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -103844,9 +103728,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -103896,9 +103778,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -103948,9 +103828,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -104000,9 +103878,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -104096,9 +103972,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -104148,9 +104022,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -104200,9 +104072,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -104252,9 +104122,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -104304,9 +104172,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -104356,9 +104222,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -104408,9 +104272,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -104460,9 +104322,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -104512,9 +104372,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -104564,9 +104422,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -104616,9 +104472,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -104668,9 +104522,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -104720,9 +104572,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -104772,9 +104622,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -104824,9 +104672,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -104876,9 +104722,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -104928,9 +104772,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -104980,9 +104822,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -105032,9 +104872,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -105084,9 +104922,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -105136,9 +104972,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -105188,9 +105022,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -105240,9 +105072,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -105292,9 +105122,7 @@ exports[`csr snapshot test > csr test src/image/_example/lazy-list.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -105712,9 +105540,7 @@ exports[`csr snapshot test > csr test src/image/_example/placeholder.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -105871,9 +105697,7 @@ exports[`csr snapshot test > csr test src/image/_example/placeholder.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -105964,9 +105788,7 @@ exports[`csr snapshot test > csr test src/image/_example/placeholder.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -106054,9 +105876,7 @@ exports[`csr snapshot test > csr test src/image/_example/placeholder.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -106213,9 +106033,7 @@ exports[`csr snapshot test > csr test src/image/_example/placeholder.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -106306,9 +106124,7 @@ exports[`csr snapshot test > csr test src/image/_example/placeholder.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -106443,9 +106259,7 @@ exports[`csr snapshot test > csr test src/image/_example/shape.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -106509,9 +106323,7 @@ exports[`csr snapshot test > csr test src/image/_example/shape.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -106575,9 +106387,7 @@ exports[`csr snapshot test > csr test src/image/_example/shape.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -106649,9 +106459,7 @@ exports[`csr snapshot test > csr test src/image/_example/shape.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -106715,9 +106523,7 @@ exports[`csr snapshot test > csr test src/image/_example/shape.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -106781,9 +106587,7 @@ exports[`csr snapshot test > csr test src/image/_example/shape.jsx 1`] = `
    - 图片加载中 -
    + /> @@ -106917,9 +106721,7 @@ exports[`csr snapshot test > csr test src/image-viewer/_example/album.jsx 1`] =
    - 图片加载中 -
    + />
    csr test src/image-viewer/_example/album.jsx 1`] =
    - 图片加载中 -
    + />
    csr test src/image-viewer/_example/albumIcons.jsx 1
    - 图片加载中 -
    + />
    csr test src/image-viewer/_example/albumIcons.jsx 1
    - 图片加载中 -
    + />
    csr test src/image-viewer/_example/base.jsx 1`] = `
    - 图片加载中 -
    + />
    csr test src/image-viewer/_example/base.jsx 1`] = `
    - 图片加载中 -
    + />
    csr test src/image-viewer/_example/block.jsx 1`] =
    - 图片加载中 -
    + />
    csr test src/image-viewer/_example/block.jsx 1`] =
    - 图片加载中 -
    + />
    csr test src/image-viewer/_example/block.jsx 1`] =
    - 图片加载中 -
    + />
    csr test src/image-viewer/_example/block.jsx 1`] =
    - 图片加载中 -
    + />
    csr test src/image-viewer/_example/error.jsx 1`] =
    - 图片加载中 -
    + />
    csr test src/image-viewer/_example/error.jsx 1`] =
    - 图片加载中 -
    + />
    csr test src/image-viewer/_example/error.jsx 1`] =
    - 图片加载中 -
    + />
    csr test src/image-viewer/_example/error.jsx 1`] =
    - 图片加载中 -
    + />
    csr test src/image-viewer/_example/error.jsx 1`] =
    - 图片加载中 -
    + />
    csr test src/image-viewer/_example/error.jsx 1`] =
    - 图片加载中 -
    + />
    csr test src/image-viewer/_example/error.jsx 1`] =
    - 图片加载中 -
    + />
    csr test src/image-viewer/_example/error.jsx 1`] =
    - 图片加载中 -
    + />
    csr test src/image-viewer/_example/modeless.jsx 1`]
    - 图片加载中 -
    + />
    csr test src/image-viewer/_example/modeless.jsx 1`]
    - 图片加载中 -
    + />
    csr test src/image-viewer/_example/multiple.jsx 1`]
    - 图片加载中 -
    + />
    csr test src/image-viewer/_example/multiple.jsx 1`]
    - 图片加载中 -
    + />
    csr test src/image-viewer/_example/multiple.jsx 1`]
    - 图片加载中 -
    + />
    csr test src/image-viewer/_example/multiple.jsx 1`]
    - 图片加载中 -
    + />
    csr test src/image-viewer/_example/multiple.jsx 1`]
    - 图片加载中 -
    + />
    csr test src/image-viewer/_example/multiple.jsx 1`]
    - 图片加载中 -
    + />
    csr test src/upload/_example/base.jsx 1`] = `
    要求文件大小在 1M 以内 @@ -283102,7 +282854,7 @@ exports[`csr snapshot test > csr test src/upload/_example/base.jsx 1`] = ` 文件上传失败示例 @@ -283295,7 +283047,7 @@ exports[`csr snapshot test > csr test src/upload/_example/base.jsx 1`] = ` 要求文件大小在 1M 以内 @@ -283422,7 +283174,7 @@ exports[`csr snapshot test > csr test src/upload/_example/base.jsx 1`] = ` 文件上传失败示例 @@ -283889,16 +283641,26 @@ exports[`csr snapshot test > csr test src/upload/_example/draggable.jsx 1`] = ` @@ -284091,16 +283853,26 @@ exports[`csr snapshot test > csr test src/upload/_example/draggable.jsx 1`] = ` @@ -284379,7 +284151,7 @@ exports[`csr snapshot test > csr test src/upload/_example/file-flow-list.jsx 1`] class="t-upload__flow-bottom" >
    @@ -284390,7 +284162,7 @@ exports[`csr snapshot test > csr test src/upload/_example/file-flow-list.jsx 1`]
    @@ -284609,7 +284381,7 @@ exports[`csr snapshot test > csr test src/upload/_example/file-flow-list.jsx 1`] class="t-upload__flow-bottom" >
    @@ -284620,7 +284392,7 @@ exports[`csr snapshot test > csr test src/upload/_example/file-flow-list.jsx 1`]
    @@ -284824,7 +284596,7 @@ exports[`csr snapshot test > csr test src/upload/_example/image.jsx 1`] = ` class="t-upload__card-item t-is-background" >
    csr test src/upload/_example/image.jsx 1`] = ` class="t-upload__card-item t-is-background" >
    csr test src/upload/_example/image.jsx 1`] = ` class="t-upload__card-item t-is-background" >
    csr test src/upload/_example/image.jsx 1`] = ` class="t-upload__card-item t-is-background" >
    csr test src/upload/_example/image.jsx 1`] = ` class="t-upload__card-item t-is-background" >
    csr test src/upload/_example/image.jsx 1`] = ` class="t-upload__card-item t-is-background" >
    csr test src/upload/_example/img-flow-list.jsx 1`] class="t-upload__flow-bottom" >
    @@ -285572,7 +285344,7 @@ exports[`csr snapshot test > csr test src/upload/_example/img-flow-list.jsx 1`]
    @@ -285687,7 +285459,7 @@ exports[`csr snapshot test > csr test src/upload/_example/img-flow-list.jsx 1`] class="t-upload__flow-bottom" >
    @@ -285698,7 +285470,7 @@ exports[`csr snapshot test > csr test src/upload/_example/img-flow-list.jsx 1`]
    @@ -285884,7 +285656,7 @@ exports[`csr snapshot test > csr test src/upload/_example/request-method.jsx 1`]
    自定义上传方法需要返回成功或失败信息 @@ -285998,7 +285770,7 @@ exports[`csr snapshot test > csr test src/upload/_example/request-method.jsx 1`]
    自定义上传方法需要返回成功或失败信息 @@ -286305,7 +286077,7 @@ exports[`csr snapshot test > csr test src/upload/_example/single-input.jsx 1`] = class="t-input__inner t-upload__placeholder" > 请选择文件 @@ -286431,7 +286203,7 @@ exports[`csr snapshot test > csr test src/upload/_example/single-input.jsx 1`] = class="t-input__inner t-upload__placeholder" > 请选择文件 diff --git a/test/utils/index.jsx b/test/utils/index.jsx index 86abc4079..bed0fa6bf 100644 --- a/test/utils/index.jsx +++ b/test/utils/index.jsx @@ -1,5 +1,5 @@ import '@testing-library/jest-dom'; -import { createEvent, fireEvent } from '@testing-library/react'; +import { createEvent, fireEvent, act } from '@testing-library/react'; import _userEvent from '@testing-library/user-event'; export * from '@testing-library/react'; @@ -14,18 +14,22 @@ export function mockTimeout(callback, timeout = 300) { }); } -export function mockDelay(timeout = 300) { - return new Promise((resolve) => { - setTimeout(() => resolve(true), timeout); - }); +export async function mockDelay(timeout) { + if (!timeout) { + await act(() => {}); + } else { + await act(async () => { + await mockTimeout(() => {}, timeout); + }); + } } -// dom 输入文本 text +// input text export function simulateInputChange(dom, text) { fireEvent.change(dom, { target: { value: text } }); } - +// input enter export function simulateInputEnter(dom) { fireEvent.keyDown(dom, { key: 'Enter', code: 'Enter', charCode: 13 }); } @@ -39,11 +43,59 @@ export function simulateClipboardPaste(dom, text) { fireEvent(dom, paste); } -// event 可选值:load/error +// image event 可选值:load/error export function simulateImageEvent(dom, event) { fireEvent(dom, createEvent(event, dom)); } +export function getFakeFileList(type = 'file', count = 1) { + if (type === 'image') { + return new Array(count).fill(null).map((_, index) => { + const letters = new Array(index).fill('A').join(''); + return new File([`image bits${letters}`], `image-name${index || ''}.png`, { type: 'text/plain', lastModified: 1674355700444 }) + }); + } + if (type === 'file') { + return new Array(count).fill(null).map((_, index) => { + const letters = new Array(index).fill('B').join(''); + return new File([`this is file text bits${letters}`], `file-name${index || ''}.txt`, { type: 'image/png', lastModified: 1674355700444 }); + }); + } + return []; +} + +/** + * 模拟文件变化 + * @param {String} dom 发生变化的元素 + * @param {String} type 类型,可选值: file/image。 + * @param {Number} count 文件数量 + * @returns File[] + */ +export function simulateFileChange(dom, type = 'file', count = 1) { + const fakeFileList = getFakeFileList(type, count); + fireEvent.change(dom, { + target: { files: fakeFileList }, + }); + return fakeFileList; +} + +/** + * 模拟拖拽上传文件 + * @param {String} dom 触发节点 + * @param {String} trigger 可选值:dragEnter/dragLeave/dragOver/drop + * @param {String} type 可选值:file/image + * @param {Number} count 数量 + * @returns File[] + */ +export function simulateDragFileChange(dom, trigger, type = 'file', count = 1) { + const fakeFileList = getFakeFileList(type, count); + fireEvent[trigger](dom, { + dataTransfer: { files: fakeFileList }, + }); + return fakeFileList; +} + +// document keydown export function simulateKeydownEvent(dom, type) { let event; switch (type) {