diff --git a/docs/webapi/attachFile.mustache b/docs/webapi/attachFile.mustache index 9cc952927..3519f594e 100644 --- a/docs/webapi/attachFile.mustache +++ b/docs/webapi/attachFile.mustache @@ -7,6 +7,13 @@ I.attachFile('Avatar', 'data/avatar.jpg'); I.attachFile('form input[name=avatar]', 'data/avatar.jpg'); ``` +If the locator points to a non-file-input element (e.g., a dropzone area), +the file will be dropped onto that element using drag-and-drop events. + +```js +I.attachFile('#dropzone', 'data/avatar.jpg'); +``` + @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator. @param {string} pathToFile local file path relative to codecept.conf.ts or codecept.conf.js config file. @returns {void} automatically synchronized promise through #recorder diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index de2925bda..d6c95f133 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -26,6 +26,8 @@ import { normalizePath, resolveUrl, relativeDir, + getMimeType, + base64EncodeFile, } from '../utils.js' import { isColorProperty, convertColorToRGBA } from '../colorUtils.js' import ElementNotFound from './errors/ElementNotFound.js' @@ -2348,8 +2350,31 @@ class Playwright extends Helper { throw new Error(`File at ${file} can not be found on local system`) } const els = await findFields.call(this, locator) - assertElementExists(els, locator, 'Field') - await els[0].setInputFiles(file) + if (els.length) { + const tag = await els[0].evaluate(el => el.tagName) + const type = await els[0].evaluate(el => el.type) + if (tag === 'INPUT' && type === 'file') { + await els[0].setInputFiles(file) + return this._waitForAction() + } + } + + const targetEls = els.length ? els : await this._locate(locator) + assertElementExists(targetEls, locator, 'Element') + const base64Content = base64EncodeFile(file) + const fileName = path.basename(file) + const mimeType = getMimeType(fileName) + await targetEls[0].evaluate((el, { base64Content, fileName, mimeType }) => { + const binaryStr = atob(base64Content) + const bytes = new Uint8Array(binaryStr.length) + for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i) + const fileObj = new File([bytes], fileName, { type: mimeType }) + const dataTransfer = new DataTransfer() + dataTransfer.items.add(fileObj) + el.dispatchEvent(new DragEvent('dragenter', { dataTransfer, bubbles: true })) + el.dispatchEvent(new DragEvent('dragover', { dataTransfer, bubbles: true })) + el.dispatchEvent(new DragEvent('drop', { dataTransfer, bubbles: true })) + }, { base64Content, fileName, mimeType }) return this._waitForAction() } diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js index ba6b324bd..9d51a81b0 100644 --- a/lib/helper/Puppeteer.js +++ b/lib/helper/Puppeteer.js @@ -28,6 +28,8 @@ import { normalizeSpacesInString, normalizePath, resolveUrl, + getMimeType, + base64EncodeFile, } from '../utils.js' import { isColorProperty, convertColorToRGBA } from '../colorUtils.js' import ElementNotFound from './errors/ElementNotFound.js' @@ -1626,8 +1628,31 @@ class Puppeteer extends Helper { throw new Error(`File at ${file} can not be found on local system`) } const els = await findFields.call(this, locator) - assertElementExists(els, locator, 'Field') - await els[0].uploadFile(file) + if (els.length) { + const tag = await els[0].evaluate(el => el.tagName) + const type = await els[0].evaluate(el => el.type) + if (tag === 'INPUT' && type === 'file') { + await els[0].uploadFile(file) + return this._waitForAction() + } + } + + const targetEls = els.length ? els : await this._locate(locator) + assertElementExists(targetEls, locator, 'Element') + const base64Content = base64EncodeFile(file) + const fileName = path.basename(file) + const mimeType = getMimeType(fileName) + await targetEls[0].evaluate((el, { base64Content, fileName, mimeType }) => { + const binaryStr = atob(base64Content) + const bytes = new Uint8Array(binaryStr.length) + for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i) + const fileObj = new File([bytes], fileName, { type: mimeType }) + const dataTransfer = new DataTransfer() + dataTransfer.items.add(fileObj) + el.dispatchEvent(new DragEvent('dragenter', { dataTransfer, bubbles: true })) + el.dispatchEvent(new DragEvent('dragover', { dataTransfer, bubbles: true })) + el.dispatchEvent(new DragEvent('drop', { dataTransfer, bubbles: true })) + }, { base64Content, fileName, mimeType }) return this._waitForAction() } diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js index 667b47af0..c0a583a2f 100644 --- a/lib/helper/WebDriver.js +++ b/lib/helper/WebDriver.js @@ -1,5 +1,6 @@ let webdriverio +import fs from 'fs' import assert from 'assert' import path from 'path' import crypto from 'crypto' @@ -24,6 +25,8 @@ import { modifierKeys, normalizePath, resolveUrl, + getMimeType, + base64EncodeFile, } from '../utils.js' import { isColorProperty, convertColorToRGBA } from '../colorUtils.js' import ElementNotFound from './errors/ElementNotFound.js' @@ -1352,20 +1355,41 @@ class WebDriver extends Helper { const res = await findFields.call(this, locator) this.debug(`Uploading ${file}`) - assertElementExists(res, locator, 'File field') - const el = usingFirstElement(res) - // Remote Upload (when running Selenium Server) - if (this.options.remoteFileUpload) { - try { - this.debugSection('File', 'Uploading file to remote server') - file = await this.browser.uploadFile(file) - } catch (err) { - throw new Error(`File can't be transferred to remote server. Set \`remoteFileUpload: false\` in config to upload file locally.\n${err.message}`) + if (res.length) { + const el = usingFirstElement(res) + const tag = await this.browser.execute(function (elem) { return elem.tagName }, el) + const type = await this.browser.execute(function (elem) { return elem.type }, el) + if (tag === 'INPUT' && type === 'file') { + if (this.options.remoteFileUpload) { + try { + this.debugSection('File', 'Uploading file to remote server') + file = await this.browser.uploadFile(file) + } catch (err) { + throw new Error(`File can't be transferred to remote server. Set \`remoteFileUpload: false\` in config to upload file locally.\n${err.message}`) + } + } + return el.addValue(file) } } - return el.addValue(file) + const targetRes = res.length ? res : await this._locate(locator) + assertElementExists(targetRes, locator, 'Element') + const targetEl = usingFirstElement(targetRes) + const base64Content = base64EncodeFile(file) + const fileName = path.basename(file) + const mimeType = getMimeType(fileName) + return this.browser.execute(function (el, base64Content, fileName, mimeType) { + var binaryStr = atob(base64Content) + var bytes = new Uint8Array(binaryStr.length) + for (var i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i) + var fileObj = new File([bytes], fileName, { type: mimeType }) + var dataTransfer = new DataTransfer() + dataTransfer.items.add(fileObj) + el.dispatchEvent(new DragEvent('dragenter', { dataTransfer: dataTransfer, bubbles: true })) + el.dispatchEvent(new DragEvent('dragover', { dataTransfer: dataTransfer, bubbles: true })) + el.dispatchEvent(new DragEvent('drop', { dataTransfer: dataTransfer, bubbles: true })) + }, targetEl, base64Content, fileName, mimeType) } /** diff --git a/lib/utils.js b/lib/utils.js index f3b2a2319..ac0d5d563 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -658,6 +658,36 @@ export const base64EncodeFile = function (filePath) { return Buffer.from(fs.readFileSync(filePath)).toString('base64') } +export const getMimeType = function (fileName) { + const ext = path.extname(fileName).toLowerCase() + const mimeTypes = { + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.gif': 'image/gif', + '.bmp': 'image/bmp', + '.svg': 'image/svg+xml', + '.webp': 'image/webp', + '.pdf': 'application/pdf', + '.txt': 'text/plain', + '.html': 'text/html', + '.css': 'text/css', + '.js': 'application/javascript', + '.json': 'application/json', + '.xml': 'application/xml', + '.zip': 'application/zip', + '.csv': 'text/csv', + '.doc': 'application/msword', + '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + '.xls': 'application/vnd.ms-excel', + '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + '.mp3': 'audio/mpeg', + '.mp4': 'video/mp4', + '.wav': 'audio/wav', + } + return mimeTypes[ext] || 'application/octet-stream' +} + export const markdownToAnsi = function (markdown) { return ( markdown diff --git a/test/data/app/view/form/dropzone.php b/test/data/app/view/form/dropzone.php new file mode 100644 index 000000000..cb79f0350 --- /dev/null +++ b/test/data/app/view/form/dropzone.php @@ -0,0 +1,24 @@ + +
+

Drop files here

+
+
+ + diff --git a/test/helper/webapi.js b/test/helper/webapi.js index 26220f973..4fb20e67a 100644 --- a/test/helper/webapi.js +++ b/test/helper/webapi.js @@ -1054,6 +1054,19 @@ export function tests() { expect(formContents().files.avatar.name).to.eql('avatar.jpg') expect(formContents().files.avatar.type).to.eql('image/jpeg') }) + + it('should drop file to dropzone', async () => { + await I.amOnPage('/form/dropzone') + await I.attachFile('#droparea', 'app/avatar.jpg') + await I.see('Dropped 1 file(s)') + await I.see('avatar.jpg') + }) + + it('should see correct file type after drop', async () => { + await I.amOnPage('/form/dropzone') + await I.attachFile('#droparea', 'app/avatar.jpg') + await I.see('image/jpeg') + }) }) describe('#saveScreenshot', () => {