diff --git a/CHANGELOG.md b/CHANGELOG.md index ebf2dd6a4..51e7e7fbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ - 更新 TimePicker 滚动条在 IE10+ 下隐藏 - 新增 Dropdown 的 command api #432 - 修复 Slider 在 Form 中的显示问题 +- 修复 Upload 在 onSuccess、onError 钩子无法拿到服务端返回信息的问题 + +#### 非兼容性更新 + +- Upload on-error 钩子函数参数变更为 function(err, response, file), on-success 钩子函数参数变更为 function(response, file, fileList) ### 1.0.0-rc.8 diff --git a/examples/docs/zh-cn/upload.md b/examples/docs/zh-cn/upload.md index c4f8f51af..aea8427f0 100644 --- a/examples/docs/zh-cn/upload.md +++ b/examples/docs/zh-cn/upload.md @@ -145,9 +145,9 @@ | show-upload-list | 是否显示已上传文件列表 | boolean | — | true | | type | 上传控件类型 | string | select,drag | select | | accept | 可选参数, 接受上传的[文件类型](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-accept)(thumbnail-mode 模式下此参数无效)| string | — | — | -| on-preview | 可选参数, 点击已上传的文件链接时的钩子 | function(file) | — | — | +| on-preview | 可选参数, 点击已上传的文件链接时的钩子, 可以通过 file.response 拿到服务端返回数据 | function(file) | — | — | | on-remove | 可选参数, 文件列表移除文件时的钩子 | function(file, fileList) | — | — | -| on-success | 可选参数, 文件上传成功时的钩子 | function(file, fileList) | — | — | -| on-error | 可选参数, 文件上传失败时的钩子 | function(err, file, fileList) | — | — | +| on-success | 可选参数, 文件上传成功时的钩子 | function(response, file, fileList) | — | — | +| on-error | 可选参数, 文件上传失败时的钩子 | function(err, response, file) | — | — | | before-upload | 可选参数, 上传文件之前的钩子,参数为上传的文件,若返回 false 或者 Promise 则停止上传。 | function(file) | — | — | | thumbnail-mode | 是否设置为图片模式,该模式下会显示图片缩略图 | boolean | — | false | diff --git a/packages/upload/src/ajax.js b/packages/upload/src/ajax.js index ac07a14e9..7ab3e7474 100644 --- a/packages/upload/src/ajax.js +++ b/packages/upload/src/ajax.js @@ -1,5 +1,5 @@ function getError(action, option, xhr) { - const msg = `cannot post ${action} ${xhr.status}'`; + const msg = `fail to post ${action} ${xhr.status}'`; const err = new Error(msg); err.status = xhr.status; err.method = 'post'; @@ -20,12 +20,14 @@ function getBody(xhr) { } } -export default function upload(action, option) { +export default function upload(option) { if (typeof XMLHttpRequest === 'undefined') { return; } const xhr = new XMLHttpRequest(); + const action = option.action; + if (xhr.upload) { xhr.upload.onprogress = function progress(e) { if (e.total > 0) { @@ -65,6 +67,10 @@ export default function upload(action, option) { const headers = option.headers || {}; + if (headers['X-Requested-With'] !== null) { + xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + } + for (let item in headers) { if (headers.hasOwnProperty(item) && headers[item] !== null) { xhr.setRequestHeader(item, headers[item]); diff --git a/packages/upload/src/index.vue b/packages/upload/src/index.vue index fc3245ea3..db071671f 100644 --- a/packages/upload/src/index.vue +++ b/packages/upload/src/index.vue @@ -111,14 +111,14 @@ export default { _file.status = 'finished'; _file.response = res; - this.onSuccess(_file, this.fileList); + this.onSuccess(res, _file, this.fileList); setTimeout(() => { _file.showProgress = false; }, 1000); } }, - handleError(err, file) { + handleError(err, response, file) { var _file = this.getFile(file); var fileList = this.fileList; @@ -126,7 +126,7 @@ export default { fileList.splice(fileList.indexOf(_file), 1); - this.onError(err, _file, fileList); + this.onError(err, response, file); }, handleRemove(file) { var fileList = this.fileList; @@ -182,7 +182,8 @@ export default { 'on-error': this.handleError, 'on-preview': this.handlePreview, 'on-remove': this.handleRemove - } + }, + ref: 'upload-inner' }; var uploadComponent = typeof FormData !== 'undefined' diff --git a/packages/upload/src/upload.vue b/packages/upload/src/upload.vue index 363f494b6..01532dea2 100644 --- a/packages/upload/src/upload.vue +++ b/packages/upload/src/upload.vue @@ -130,20 +130,21 @@ export default { let formData = new FormData(); formData.append(this.name, file); - ajax(this.action, { + ajax({ headers: this.headers, withCredentials: this.withCredentials, file: file, data: this.data, filename: this.name, + action: this.action, onProgress: e => { this.onProgress(e, file); }, onSuccess: res => { this.onSuccess(res, file); }, - onError: err => { - this.onError(err, file); + onError: (err, response) => { + this.onError(err, response, file); } }); }, diff --git a/test/unit/specs/upload.spec.js b/test/unit/specs/upload.spec.js new file mode 100644 index 000000000..85d73f7d7 --- /dev/null +++ b/test/unit/specs/upload.spec.js @@ -0,0 +1,229 @@ +import { createVue, destroyVM } from '../util.js'; +import ajax from 'packages/upload/src/ajax'; +const noop = () => { +}; +const option = { + onSuccess: noop, + data: { a: 'abc', b: 'bcd' }, + filename: 'file.png', + file: 'foo', + action: '/upload', + headers: { region: 'shanghai' } +}; +let requests, xhr; +describe('ajax', () => { + beforeEach(() => { + xhr = sinon.useFakeXMLHttpRequest(); + requests = []; + xhr.onCreate = req => requests.push(req); + option.onError = noop; + option.onSuccess = noop; + }); + afterEach(() => { + xhr.restore(); + }); + it('request success', done => { + option.onError = done; + option.onSuccess = ret => { + expect(ret).to.eql({ success: true }); + done(); + }; + ajax(option); + requests[0].respond(200, {}, '{"success": true}'); + }); + it('request width header', done => { + ajax(option); + expect(requests[0].requestHeaders).to.eql({ + 'X-Requested-With': 'XMLHttpRequest', + region: 'shanghai' + }); + done(); + }); + it('40x code should be error', done => { + option.onError = e => { + expect(e.toString()).to.contain('404'); + done(); + }; + + option.onSuccess = () => done('404 should throw error'); + ajax(option); + requests[0].respond(404, {}, 'Not found'); + }); + it('2xx code should be success', done => { + option.onError = done; + option.onSuccess = ret => { + expect(ret).to.equal(''); + done(); + }; + ajax(option); + requests[0].respond(204, {}); + }); +}); +describe('Upload', () => { + let requests; + let xhr; + + beforeEach(() => { + xhr = sinon.useFakeXMLHttpRequest(); + requests = []; + xhr.onCreate = req => requests.push(req); + }); + + afterEach(() => { + xhr.restore(); + }); + + describe('ajax upload', () => { + if (typeof FormData === 'undefined') { + return; + } + + let uploader; + let handlers = {}; + + const props = { + props: { + action: '/upload', + onSuccess(res, file, fileList) { + console.log('onSuccess', res); + if (handlers.onSuccess) { + handlers.onSuccess(res, file, fileList); + } + }, + onError(err, response, file) { + console.log('onError', err, response); + if (handlers.onError) { + handlers.onError(err, response, file); + } + }, + onPreview(file) { + console.log('onPreview', file); + if (handlers.onPreview) { + handlers.onPreview(file); + } + } + } + }; + + beforeEach(() => { + uploader = createVue({ + render(h) { + return ( + + 点击上传 + + ); + } + }, true).$refs.upload; + }); + + afterEach(() => { + destroyVM(uploader); + handlers = {}; + }); + + it('upload success', done => { + const files = [{ + name: 'success.png', + type: 'xml' + }]; + + handlers.onSuccess = (res, file, fileList) => { + expect(file.name).to.equal('success.png'); + expect(fileList.length).to.equal(1); + expect(res).to.equal('success.png'); + done(); + }; + + uploader.$refs['upload-inner'].handleChange({ target: { files }}); + + setTimeout(() => { + requests[0].respond(200, {}, `${files[0].name}`); + }, 100); + }); + + it('upload fail', done => { + const files = [{ + name: 'fail.png', + type: 'xml' + }]; + + handlers.onError = (err, response, file) => { + expect(err instanceof Error).to.equal(true); + expect(response).to.equal('error 400'); + done(); + }; + + uploader.$refs['upload-inner'].handleChange({ target: { files }}); + + setTimeout(() => { + requests[0].respond(400, {}, 'error 400'); + }, 100); + }); + it('preview file', done => { + const files = [{ + name: 'success.png', + type: 'xml' + }]; + + handlers.onPreview = (file) => { + expect(file.response).to.equal('success.png'); + done(); + }; + + handlers.onSuccess = (res, file, fileList) => { + uploader.$nextTick(_ => { + uploader.$el.querySelector('.el-upload__files .is-finished a').click(); + }); + }; + + uploader.$refs['upload-inner'].handleChange({ target: { files }}); + + setTimeout(() => { + requests[0].respond(200, {}, `${files[0].name}`); + }, 100); + }); + it('file remove', done => { + const files = [{ + name: 'success.png', + type: 'xml' + }]; + + handlers.onSuccess = (res, file, fileList) => { + uploader.$el.querySelector('.el-upload__files .el-upload__btn-delete').click(); + uploader.$nextTick(_ => { + expect(uploader.fileList.length).to.equal(0); + expect(uploader.$el.querySelector('.el-upload__files')).to.not.exist; + done(); + }); + }; + + uploader.$refs['upload-inner'].handleChange({ target: { files }}); + + setTimeout(() => { + requests[0].respond(200, {}, `${files[0].name}`); + }, 100); + }); + it('clear files', done => { + const files = [{ + name: 'success.png', + type: 'xml' + }]; + + handlers.onSuccess = (res, file, fileList) => { + uploader.clearFiles(); + uploader.$nextTick(_ => { + expect(uploader.fileList.length).to.equal(0); + expect(uploader.$el.querySelector('.el-upload__files')).to.not.exist; + done(); + }); + }; + + uploader.$refs['upload-inner'].handleChange({ target: { files }}); + + setTimeout(() => { + requests[0].respond(200, {}, `${files[0].name}`); + }, 100); + }); + }); +});