Skip to content

Commit

Permalink
Add Selenium downloadFile command (#12923)
Browse files Browse the repository at this point in the history
  • Loading branch information
ccharnkij committed May 23, 2024
1 parent bb39c0b commit 247b729
Show file tree
Hide file tree
Showing 6 changed files with 282 additions and 108 deletions.
43 changes: 43 additions & 0 deletions packages/wdio-protocols/src/protocols/selenium.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,49 @@ export default {
},
},
},
'/session/:sessionId/se/files': {
GET: {
command: 'getDownloadableFiles',
description:
'List files from remote machine available for download.',
ref: 'https://www.seleniumhq.org/',
parameters: [],
returns: {
type: 'Object',
name: 'names',
description:
'Object containing a list of downloadable files on remote machine.',
},
},
POST: {
command: 'download',
description:
'Download a file from remote machine on which the browser is running.',
ref: 'https://www.seleniumhq.org/',
parameters: [
{
name: 'name',
type: 'string',
description:
'Name of the file to be downloaded',
required: true,
},
],
returns: {
type: 'Object',
name: 'data',
description:
'Object containing downloaded file name and its content',
},
},
DELETE: {
command: 'deleteDownloadableFiles',
description:
'Remove all downloadable files from remote machine on which the browser is running.',
ref: 'https://www.seleniumhq.org/',
parameters: [],
},
},
'/grid/api/hub/': {
GET: {
isHubCommand: true,
Expand Down
3 changes: 2 additions & 1 deletion packages/webdriverio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"htmlfy": "^0.2.1",
"import-meta-resolve": "^4.0.0",
"is-plain-obj": "^4.1.0",
"jszip": "^3.10.1",
"lodash.clonedeep": "^4.5.0",
"lodash.zip": "^4.2.0",
"minimatch": "^9.0.3",
Expand All @@ -101,4 +102,4 @@
"optional": true
}
}
}
}
1 change: 1 addition & 0 deletions packages/webdriverio/src/commands/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from './browser/custom$$.js'
export * from './browser/custom$.js'
export * from './browser/debug.js'
export * from './browser/deleteCookies.js'
export * from './browser/downloadFile.js'
export * from './browser/emulate.js'
export * from './browser/execute.js'
export * from './browser/executeAsync.js'
Expand Down
92 changes: 92 additions & 0 deletions packages/webdriverio/src/commands/browser/downloadFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import fs from 'node:fs'
import path from 'node:path'
import JSZip from 'jszip'
import logger from '@wdio/logger'

const log = logger('webdriverio')

/**
*
* Download a file from the remote computer running Selenium node to local file system
* by using the [`downloadFile`](https://webdriver.io/docs/api/selenium#downloadFile) command.
*
* :::info
* Note that this command is only supported if you use a
* [Selenium Grid](https://www.selenium.dev/documentation/en/grid/) with Chrome, Edge or Firefox
* and have the `se:downloadsEnabled` flag set in the capabilities.
* :::
*
* <example>
:downloadFile.js
it('should download a file', async () => {
await browser.url('https://www.selenium.dev/selenium/web/downloads/download.html')
await $('#file-1').click()
await browser.waitUntil(async function () {
return (await browser.getDownloadableFiles()).names.includes('file_1.txt')
}, {timeout: 5000})
const files = await browser.getDownloadableFiles()
const downloaded = await browser.downloadFile(files.names[0], process.cwd())
await browser.deleteDownloadableFiles()
})
* </example>
*
* @alias browser.downloadFile
* @param {string} fileName remote path to file
* @param {string} targetDirectory target location on local computer
* @type utility
* @uses protocol/download
*
*/
export async function downloadFile(
this: WebdriverIO.Browser,
fileName: string,
targetDirectory: string
): Promise<object> {
/**
* parameter check
*/
if (typeof fileName !== 'string' || typeof targetDirectory !== 'string') {
throw new Error('number or type of arguments don\'t agree with downloadFile command')
}

/**
* check if command is available
*/
if (typeof this.download !== 'function') {
throw new Error(`The downloadFile command is not available in ${(this.capabilities as WebdriverIO.Capabilities).browserName} and only available when using Selenium Grid`)
}

const response = await this.download(fileName)
const base64Content = response.contents

if (!targetDirectory.endsWith('/')) {
targetDirectory += '/'
}

fs.mkdirSync(targetDirectory, { recursive: true })
const zipFilePath = path.join(targetDirectory, `${fileName}.zip`)
fs.writeFileSync(zipFilePath, Buffer.from(base64Content, 'base64'))

const zipData = fs.readFileSync(zipFilePath)
const filesData: string[] = []

try {
const zip = await JSZip.loadAsync(zipData)
const keys = Object.keys(zip.files)

// Iterate through each file in the zip archive
for (let i = 0; i < keys.length; i++) {
const fileData = await zip.files[keys[i]].async('nodebuffer')
const dir = path.resolve(targetDirectory, keys[i])
fs.writeFileSync(dir, fileData)
log.info(`File extracted: ${keys[i]}`)
filesData.push(dir)
}
} catch (error) {
log.error('Error unzipping file:', error)
}

return Promise.resolve({
files: filesData
})
}
110 changes: 110 additions & 0 deletions packages/webdriverio/tests/commands/browser/downloadFile.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { expect, describe, it, afterEach, vi } from 'vitest'

import fs from 'node:fs'
import path from 'node:path'
import JSZip from 'jszip'
import { remote } from '../../../src/index.js'
import logger from '@wdio/logger'

vi.mock('node:fs', () => ({
default: {
mkdirSync: vi.fn(),
readFileSync: vi.fn(),
writeFileSync: vi.fn()
}
}))
vi.mock('got')
vi.mock('JSZip', () => ({
default: {
loadAsync: vi.fn()
}
}))
vi.mock('devtools')
vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger')))

describe('downloadFile', () => {
it('should throw if browser does not support it', async function () {
const browser = await remote({
baseUrl: 'http://webdriver.io',
capabilities: {
browserName: 'safari'
}
})

await expect(browser.downloadFile('bar.jpg', '/foo/bar')).rejects.toEqual(
new Error('The downloadFile command is not available in mockBrowser and only available when using Selenium Grid'))
})

it('should throw if path is not a string', async function () {
const browser = await remote({
baseUrl: 'http://webdriver.io',
capabilities: {
browserName: 'chrome'
}
})

// @ts-expect-error wrong parameter
await expect(browser.downloadFile(123, 456)).rejects.toEqual(
new Error('number or type of arguments don\'t agree with downloadFile command'))
})

it('should unzip the file and use downloadFile command', async () => {
vi.spyOn(JSZip, 'loadAsync').mockReturnValue(Promise.resolve(
{
files: {
'file_1': {
async: vi.fn()
}
}
})
)

const browser = await remote({
baseUrl: 'http://webdriver.io',
capabilities: {
browserName: 'chrome'
}
})
browser.download = vi.fn().mockReturnValue({
fileName: 'test',
contents: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=='
})

await browser.downloadFile('toDownload.jpg', __dirname)
const log = logger('test')

expect(log.info).toHaveBeenCalledWith(
expect.stringContaining('File extracted: file_1')
)
expect(fs.writeFileSync).toHaveBeenCalledTimes(2)
})

it('reject on error', async () => {
vi.spyOn(JSZip, 'loadAsync').mockRejectedValue('test'
)
const browser = await remote({
baseUrl: 'http://webdriver.io',
capabilities: {
browserName: 'chrome'
}
})
browser.download = vi.fn().mockReturnValue({
fileName: 'test',
contents: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=='
})

await browser.downloadFile('toDownload.jpg', __dirname)
const log = logger('test')

expect(log.error).toHaveBeenCalledWith('Error unzipping file:', 'test')
})

afterEach(() => {
const log = logger('test')
vi.mocked(log.info).mockClear()
vi.mocked(log.error).mockClear()
vi.mocked(fs.mkdirSync).mockClear()
vi.mocked(fs.readFileSync).mockClear()
vi.mocked(fs.writeFileSync).mockClear()
})
})
Loading

0 comments on commit 247b729

Please sign in to comment.