js粘貼板為什么獲取不到圖片信息_【第1829期】復制黏貼上傳圖片和跨瀏覽器自動化測試...

前言

這個操作體驗倒是不錯。今日早讀文章由丁香園@蔣璇投稿分享。

@蔣璇, 前端開發攻城獅, 現任職于丁香園. 英語愛好者, 測試驅動開發(TDD)&行為驅動開發(BDD)推崇者. 先專注于 https://github.com/Jiang-Xuan/tuchuang.space 項目的測試驅動開發探索

正文從這開始~~

在網頁中上傳圖片有多重選擇.

  • 點擊文件上傳控件, 選擇文件進行上傳

  • 從文件瀏覽器中拖拽文件進行上傳

  • 從系統粘貼板中粘貼上傳

本篇文章著重介紹最后一種, 也是最方便的上傳的方法, Control/Command + v 進行上傳, 以及如何使用 selenium 來跨瀏覽器的自動化測試這個功能.

一般的截圖程序, 比如 QQ, 微信, PrintScreen 按鈕, 都會將截圖以 png 格式放入系統粘貼板, 所以這里討論 png 格式的粘貼, 而不是其他格式的, 更多的還是給截圖程序使用.

瀏覽器如何獲取 Control/Command + v 粘貼的圖片數據??

Note: 支持 IE 11, 以及現代瀏覽器Chrome, Firefox, Safari

現代瀏覽器在 paste 事件中提供 clipboardData 屬性來訪問粘貼板中的數據

獲取粘貼板中的圖片數據可以通過監聽 paste 事件來實現:

document.addEventListener('paste', (event) => {

const { items } = event.clipboardData

if (items) {

;[...items].forEach((item) => {

if (item.type.indexOf('image') !== -1) {

// item 的 mime 類型是圖片, 說明想要粘貼的是圖片數據

// https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem

}

})

}

})

上面的代碼中 item 提供 getAsFile 方法來獲取粘貼的圖片的數據的二進制數據:

file = item.getAsFile()

這里獲取到的 file 為 File 的實例, 繼承自 Blob, js 中的二進制數據, 你可以直接將 file 上傳給后端服務器就可以完成圖片的上傳:

const formData = newFormData()

formData.append('images', file)

const xhr = newXMLHTTPRequest()

xhr.open('POST', 'https://tuchuang.space/api/v1/images')

xhr.send(formData)

上訴討論的是現代瀏覽器的處理, 麻煩的是 IE 11 的處理(IE11 以下的瀏覽器無法獲取粘貼板中的圖片數據, 就不用嘗試了?), IE 11 支持粘貼板中的圖片以 img 標簽, src 為 圖片的 base64 編碼放入設置了 contenteditable 屬性的元素之中, 官方來源 Enhanced Rich Editing Experiences in IE11

IE 11 中需要使用 hack 的方法來獲取粘貼板中的圖片數據

hack 的實例可以去 這里 看下, 要使用 IE 11 瀏覽器哦, 目前能找到的在線編輯器支持 IE 的也就是 jsfiddle 了. 大部分代碼都是 copy 來自 這個 Stack Overflow 問題 中, 思想就是在用戶 paste 的時候 focus 一個 設置 contenteditable 屬性的 div, 然后從這個 div 中獲取數據. 接下來 庖丁解牛, 這里解釋的代碼的原理和 jsfiddle 中的例子一致, 但是做了一定的優化, 實際的使用可以去 這里 看下, 全部代碼如下:

class PasteImage {

/**

* 在獲取到用戶 paste 的圖片數據時的回調函數

* @param {(imageBlobData: Blob) => void} callback

*/

constructor (callback) {

this._callBack = callback

/** @private {boolean} 用戶是否正在按下 ctrl 鍵 */

this._ctrlPressed = false

/** @private {boolean} 用戶是否正在按下 command 鍵, MacOS 系統下 */

this._commandPressed = false

/** @private {HTMLDivElement} 捕獲用戶粘貼的圖片的容器 */

this._pasteCatcher = document.createElement('div')

/** @private {boolean} 是否支持 Native paste 事件 */

this._pasteEventSupport = false

/** @private {HTMLDivElement} 捕獲用戶粘貼的圖片的容器的 ID */

this._pasteCatcherId = `paste-image-${Math.random()}`

this._pasteCatcher.setAttribute('id', this._pasteCatcherId)

this._pasteCatcher.setAttribute('contenteditable', '')

this._pasteCatcher.style.cssText = 'opacity:0;position:fixed;top:0px;left:0px;width:10px;margin-left:-20px;'

/**

* 處理頁面按鍵按下

* @private

* @type {(event: KeyboardEvent) => void}

*/

this._handleOnKeyDown = this._handleOnKeyDown.bind(this)

/**

* 處理頁面按鍵釋放

* @private

* @type {(event: KeyboardEvent) => void}

*/

this._handleOnKeyUp = this._handleOnKeyUp.bind(this)

/**

* 處理頁面的 paste 事件

* @private

* @type {(event: ClipboardEvent) => void}

*/

this._handleOnPaste = this._handleOnPaste.bind(this)

const observer = new MutationObserver((mutations) => {

mutations.forEach((mutation) => {

if (this._pasteEventSupport || this._ctrlPressed === false || mutation.type !== 'childList') {

return

}

if (mutation.addedNodes.length === 1) {

if (mutation.addedNodes[0].src !== undefined) {

this._pasteCreateImage(mutation.addedNodes[0].src)

}

}

})

})

observer.observe(this._pasteCatcher, {

childList: true,

attributes: true,

characterData: true

})

}

/**

* 處理非標準的 paste 事件, 從 image 標簽中獲取數據

* 目前支持的瀏覽器中只有 IE 11 不支持標準的 paste 事件

* IE 11 中粘貼的圖片的格式為 [data url](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs)

*

* atob('MTIz') // 123

*

* Example: data:image/png;base64,MTIz

*

* @private

* @param {string} source image 標簽的 src 屬性

*/

_pasteCreateImage (source) {

const base64String = source.split(',')[1]

const buffer = base64js.toByteArray(base64String)

const uint8 = new Uint8Array(buffer)

const pngBlob = new Blob([uint8], { type: 'image/png' })

this._callBack(pngBlob)

}

/**

* 處理頁面按鍵按下

* @private

* @param {KeyboardEvent} event

*/

_handleOnKeyDown (event) {

const { keyCode } = event

console.log(event)

if (keyCode === 17 || event.metaKey || event.ctrlKey) {

if (this._ctrlPressed === false) {

this._ctrlPressed = true

}

}

if (keyCode === 86) {

if (document.activeElement !== null && document.activeElement.type === 'text') {

// 允許用戶拷貝文字進入輸入框

return false

}

if (this._ctrlPressed === true) {

this._pasteCatcher.focus()

}

}

}

/**

* 處理頁面按下釋放

* @private

* @param {KeyboardEvent} event

*/

_handleOnKeyUp (event) {

// ctrl

if (event.ctrlKey && this._ctrlPressed === true) {

this._ctrlPressed = false

}

// command

if (event.metaKey && this._commandPressed === true) {

this._commandPressed = false

this._ctrlPressed = false

}

}

/**

* 處理頁面的 paste 事件

* @private

* @param {ClipboardEvent} event

*/

_handleOnPaste (event) {

this._pasteCatcher.innerHTML = ''

if (event.clipboardData) {

const { items } = event.clipboardData

if (items) {

this._pasteEventSupport = true

;[...items].forEach((item) => {

if (item.type.indexOf('image') !== -1) {

console.log(item)

const blob = item.getAsFile()

this._callBack(blob)

}

})

}

}

}

/**

* 監聽事件, 將 pasteCatcher 放入 body 中

* @public

*/

install () {

document.body.appendChild(this._pasteCatcher)

document.addEventListener('keydown', this._handleOnKeyDown)

document.addEventListener('keyup', this._handleOnKeyUp)

document.addEventListener('paste', this._handleOnPaste)

}

uninstall () {

document.body.removeChild(this._pasteCatcher)

document.removeEventListener('keydown', this._handleOnKeyDown)

document.removeEventListener('keyup', this._handleOnKeyUp)

document.removeEventListener('paste', this._handleOnPaste)

}

}

使用方法:

const pasteImage = new PasteImage((blob) => {

// blob 就是獲取到的圖片的二進制數據

})

pasteImage.install()

// 如果想要停止監聽 paste, 調用 pasteImage.uninstall()

constructor 構造函數

構造函數接受一個回調函數作為在接收到數據的時候的回調.

this._callBack = callback

_ctrlPressed 判斷用戶是否按下 control 按鍵(Windows 下粘貼組合鍵為 Control + v), _commandPressed 判斷用戶是否按下 command 按鍵(Macos 下粘貼組合鍵為 command + v, Macos 沒有 IE 11, 其實 Firefox 22 以下也不支持標準的 paste 方法獲取圖片數據?, 不過也可以忽略了) .

/** @private {boolean} 用戶是否正在按下 ctrl 鍵 */

this._ctrlPressed = false

/** @private {boolean} 用戶是否正在按下 command 鍵, MacOS 系統下 */

this._commandPressed = false

_pasteEventSupprt 判斷瀏覽器是否支持通過標準的 paste 事件獲取數據.

/** @private {boolean} 是否支持 Native paste 事件 */

this._pasteEventSupport = false

接下來創建一個 div, 用來在不支持標準的 paste 事件獲取數據的瀏覽器中捕獲用戶粘貼操作(其實就是 IE 11), 給這個 div 設置 id 屬性, 然后設置其的 contenteditable 屬性, 給這個 div 設置 css, 讓其不會顯示在用戶的屏幕上.

/** @private {HTMLDivElement} 捕獲用戶粘貼的圖片的容器 */

this._pasteCatcher = document.createElement('div')

/** @private {HTMLDivElement} 捕獲用戶粘貼的圖片的容器的 ID */

this._pasteCatcherId = `paste-image-${Math.random()}`

this._pasteCatcher.setAttribute('id', this._pasteCatcherId)

this._pasteCatcher.setAttribute('contenteditable', '')

this._pasteCatcher.style.cssText = 'opacity:0;position:fixed;top:0px;left:0px;width:10px;margin-left:-20px;'

接下來是綁定頁面上的幾個事件監聽器的 this 指向, 包括監聽用戶按下按鍵, 釋放按鍵, 和 paste 事件的監聽器.

/**

* 處理頁面按鍵按下

* @private

* @type {(event: KeyboardEvent) => void}

*/

this._handleOnKeyDown = this._handleOnKeyDown.bind(this)

/**

* 處理頁面按鍵釋放

* @private

* @type {(event: KeyboardEvent) => void}

*/

this._handleOnKeyUp = this._handleOnKeyUp.bind(this)

/**

* 處理頁面的 paste 事件

* @private

* @type {(event: ClipboardEvent) => void}

*/

this._handleOnPaste = this._handleOnPaste.bind(this)

為了在 IE 11 上獲取到用戶粘貼到上面的 _pasteCatcher 容器之中的內容, 需要監聽這個 DOM 的子元素的變動, 通過 MutationObserver 來實現, 如果支持標準的 paste 事件獲取數據, 或者是 control 沒有被按下, 或者是不是子元素的變化, 則不處理. 否則找到被添加的元素, 如果是圖片的粘貼, 在 IE11 中將是通過 img 標簽以 data url 為 src, data url 為 image base64 編碼, 將這個 data url 取出來傳遞給 _pasteCreateImage 函數.

const observer = new MutationObserver((mutations) => {

mutations.forEach((mutation) => {

if (this._pasteEventSupport || this._ctrlPressed === false || mutation.type !== 'childList') {

return

}

if (mutation.addedNodes.length === 1) {

if (mutation.addedNodes[0].src !== undefined) {

this._pasteCreateImage(mutation.addedNodes[0].src)

}

}

})

})

observer.observe(this._pasteCatcher, {

childList: true,

attributes: true,

characterData: true

})

_pasteCreateImage 方法

接收一個參數, 就是 圖片的 data url, 比如 data:image/png;base64,MTIz(MTIz 是 123 的 base64 編碼)

將圖片的 base64 編碼數據從 data url 找出并提取出來.

const base64String = source.split(',')[1]

將 base64 轉成二進制數據, 這里用到的是 base64-js, 可以將 base64 編碼轉換成二進制數據, 在 nodejs 中, 這種轉換是內置的.

const buffer = base64js.toByteArray(base64String).buffer

然后用這個 buffer 創建 mimetype 是 image/png 的 Blob 對象

const pngBlob = newBlob([buffer], { type: 'image/png'})

成功的拿到了需要的數據, 調用回調將數據傳遞出去

this._callBack(pngBlob)

_handleOnKeyDown 方法

這是一個按鍵按下監聽器, 在鍵盤被按下的時候觸發該函數.

從 event 參數中獲取 keycode

const{ keyCode } = event

如果 keycode 是 17 或者是 event.metaKey, event.ctrlKey 成立, 則是用戶按下了 control 修飾鍵.

if (keyCode === 17 || event.metaKey || event.ctrlKey) {

if (this._ctrlPressed === false) {

this._ctrlPressed = true

}

}

如果 keycode 是 86, 86 是 v 的 keycode. document.activeElement 獲取當前被聚焦的元素 , 如果被聚焦的是一個 type 是 text 的 input 輸入框, 用戶是想將文字拷貝進輸入框, 而不是粘貼圖片.

if (keyCode === 86) {

if (document.activeElement !== null && document.activeElement.type === 'text') {

// 允許用戶拷貝文字進入輸入框

return false

}

if (this._ctrlPressed === true) {

this._pasteCatcher.focus()

}

}

在 _pasteCatcher 元素被 focus 之后, 用戶 ctrl+v 的數據就會粘貼進 _pasteCatcher 元素內部中:

e484fd9aa7b3ad84ee10a36dd4803d89.png

這會觸發 _pasteCacher 的 MutationObserver 的回調

const observer = new MutationObserver((mutations) => {

mutations.forEach((mutation) => {

if (this._pasteEventSupport || this._ctrlPressed === false || mutation.type !== 'childList') {

return

}

if (mutation.addedNodes.length === 1) {

if (mutation.addedNodes[0].src !== undefined) {

this._pasteCreateImage(mutation.addedNodes[0].src)

}

}

})

})

如果瀏覽器原生支持標準的 paste 事件, 或者是 control 按鍵沒有被按下, 或者這不是一個 childList 類型的 mutation, 不處理. 否則判斷 mutation 的對否有添加的節點, 然后判斷第一個被添加的節點的 src 屬性是否存在, 因為圖片的粘貼必定是 img 標簽, 并且有 src 屬性, 這個時候就可以判斷出用戶粘貼的是一張圖片, 將獲得到的圖片的 data url 傳遞給處理函數 pasteCreateImage.

監聽 _pasteCacher 的變化的調用, 其實可以只監聽 childList.

observer.observe(this._pasteCatcher, {

??childList: true,

??attributes: true,

??characterData: true

})

跨瀏覽器自動化測試

CI 服務為 Github 提供的 Github Actions

測試的瀏覽器為: IE 11, Chrome latest(Github Actions 提供的 Chrome), Firefox latest(Github Actions 提供的 Firefox)

e2e 測試的工具為 selenium

為什么是 selenium?

puppeteer, cypress 只支持 chromium 系列瀏覽器, 無法達成跨瀏覽器測試需求 passed

karma 只能在瀏覽器內部執行代碼, 無法操作操作系統的剪切板, passed

測試的步驟如下:

準備一張測試的 png 圖片, 計算這張圖片的 bitmap, 這里是用 jimp 來計算出測試圖片的 bitmap

image.bitmap.data; // a Buffer of the raw bitmap data

向操作系統的剪切板寫入第一步準備的圖片

訪問 tuchuang.space ctrl + v 快捷鍵粘貼圖片

應該頁面發起請求, 并且傳入的是一張 png 圖片, 并且圖片的 bitmap 和第一步準備的圖片的 bitmap 一致

第一步, 準備測試圖片, 計算圖片的 bitmap

第一步和第二步被封裝到了一個單獨的 npm 包中 copy-logo-to-clipboard

測試圖片為 tuchuang.space 的 壓縮版 logo, 為了測試方便, 壓縮到了只有4個像素, 像素的 rgba 16進制的值為:

5373fc47dbed4bb2dd2c311bcfa4e5b8.png

第一個像素的 rgba 值: rgba(124, 158, 181, 253) 第二個像素的 rgba 值: rgba(139, 137, 165, 253) 第三個像素的 rgba 值: rgba(243, 188, 110, 253) 第四個像素的 rgba 值: rgba(219, 89, 89, 253)

第二步, 將圖片寫入操作系統

處理起來最麻煩的一步

支持 Windows, Macos

nodejs 中沒有一個很好的辦法操作操作系統的剪切板, Windows 操作系統下可以使用 C# 加上 .net 框架和操作系統的剪切板交互, 可以看下我的嘗試 github.com/Jiang-Xuan/… github.com/Jiang-Xuan/… 編寫代碼使用的平臺是 Macos, 所以還要處理 Mac 平臺的剪切板的交互, Swift 太難了. 最后我放棄了, 轉而使用 electron 提供的 api 來處理 github.com/Jiang-Xuan/… electron 提供了 方法 來將圖片寫入操作系統的剪切板.

// Modules to control application life and create native browser window

const{ app, clipboard, nativeImage } = require('electron')

const path = require('path')

console.log('main.js')

const fooImage = nativeImage.createFromPath(path.resolve(__dirname, './logo.png'))

// This method will be called when Electron has finished

// initialization and is ready to create browser windows.

// Some APIs can only be used after this event occurs.

app.on('ready', () => {

console.log('ready')

clipboard.writeImage(fooImage)

console.log(clipboard.readImage())

app.quit()

})

打包一個 electron 應用來實現, electron 的應用打包出來都比較大, 但是在沒有更好的辦法的情況下只能這樣, 分發一個 electron 應用來實現跨平臺的操作系統的剪切板操作. 暴露出去一個 copyLogoToClip 方法, 使用方法為:

const{ copyLogoToClip } = require('copy-logo-to-clipboard')

// 寫入操作系統

await copyLogoToClip()

這里說一個小故事 在剛開始的時候我并沒有給這個模塊寫測試用例, 在我實際在 tuchuang.space 項目中寫測試用例的時候我發現在讀取出來的圖片和寫入的圖片的 bitmap 并不一致, 這個時候我不確定是哪一部分出的問題了, 到底是 copy-logo-to-clipboard 在向系統剪切板寫入圖片的時候修改了圖片的 bitmap, 還是瀏覽器在讀取操作系統的剪切板的 bitmap 的時候改變了圖片的 bitmap? 太相信瀏覽器導致我一度懷疑是 electron 修改了圖片的 bitmap, 可是最后卻發現了是某些瀏覽器修改了圖片的 bitmap, 如果我在剛開始的時候對 copy-logo-to-clipboard 寫了測試用例, 我就有理由相信是瀏覽器出了問題, 所以后續我對 copy-logo-to-clipboard 寫了 測試用例 來保證這個模塊是正確的

第三步, 按下 ctrl+v

在按下 ctrl+v 這一步也有坑, 在 Macos chrome 上, 你會發現無論是 control+v 還是 command+v 都無法執行粘貼操作, 輾轉多處, 在 Stack Overflow 上面發現了 解決辦法, 就是按下 Shift + Insert

c027e9884f83bb627abe3c6883c71b82.png

另一個需要注意的點是在 IE 11 下, 我們做了特殊的粘貼圖片的處理, 如果我們用程序按下 ctrl+v 你會發現無法粘貼圖片, 是因為程序的操作太快了, 沒有給我們聚焦 _pasteCatcher 的機會, 但是實際的用戶操作的時候并沒有這么快, 所以特殊處理一下 IE 11 下的 ctrl+v 的按下的時機, 以更符合實際的用戶操作

const controlKeyDown = driver.actions().keyDown(Key.CONTROL)

const vKeyDown = driver.actions().keyDown('v')

const vKeyUp = driver.actions().keyUp('v')

const controlKeyUp = driver.actions().keyUp(Key.CONTROL)

awaitnewPromise((resolve) => setTimeout(resolve, 2000))

await body.click()

await controlKeyDown.perform()

awaitnewPromise((resolve) => setTimeout(resolve, 500))

await vKeyDown.perform()

awaitnewPromise((resolve) => setTimeout(resolve, 500))

await vKeyUp.perform()

awaitnewPromise((resolve) => setTimeout(resolve, 500))

await controlKeyUp.perform()

頁面發起請求, 并且傳入的是一張 png 圖數據, 并且圖片的 bitmap 和第一步準備的圖片的 bitmap 一致

不想讓頁面真正的向后端發起請求, 但是卻沒有找到一種可以攔截 selenium 操作的瀏覽器的請求, 在 puppeteer 中可以通過監聽 page.on('request') 事件來攔截和 mock 請求

// 來自: https://pptr.dev/#?product=Puppeteer&version=v2.0.0&show=api-pagesetrequestinterceptionvalue

const puppeteer = require('puppeteer');

puppeteer.launch().then(async browser => {

const page = await browser.newPage();

await page.setRequestInterception(true);

page.on('request', interceptedRequest => {

if(interceptedRequest.url().endsWith('.png') || interceptedRequest.url().endsWith('.jpg'))

interceptedRequest.abort();

else

interceptedRequest.continue();

});

await page.goto('https://example.com');

await browser.close();

});

為了達到類似 puppeteer 的這種功能, 可以 mock 一個服務器, 然后在 selenium 環境中請求 mock 的服務器, 我手動實現了一個 mock-server, 提供的功能僅僅滿足該測試的需求, 詳情可以去項目倉庫看細節(建議看測試用例來了解, 沒有文檔的情況下測試用例就是最好的文檔). 配置想要返回的請求, 啟動 mock 服務器

mockServer.config.configResponse({

body: {

images: {

'image_from_clipboard.png': {

mimetype: 'image/png',

md5: '637e2ee416a2de90cf6e76b6f4cc8c89',

filename: 'test-test.png',

ossPath: 'http://example.com/test-test.png',

cdnPath: 'https://i.tuchuang.space/test.png',

deleteKey: '2436b48115486de952296f2b5295aeb90d284761278661102e7dda990c3f67022133080fb1bcd99d7f94678a991c57f1'

}

}

}

})

await mockServer.start()

在請求執行完畢之后, 在 mock 服務器接收到的請求中進行搜索, 找到需要的請求, 然后進行判斷請求服務接收到的參數是否正常

// assert https://github.com/Jiang-Xuan/tuchuang.space/issues/36#issuecomment-566868929

awaitnewPromise((resolve) => setTimeout(resolve, 2000))

const requests = mockServer.search({ path: '/api/v1/images'})

awaitnewPromise((resolve) => setTimeout(resolve, 2000))

const outputLogoJimp = await jimp.read(requests[0].files[0].buffer)

const logoBitmap = await getLogoBitmap()

if(forBrowser === 'chrome') {

expect(md5(outputLogoJimp.bitmap.data)).toEqual(md5(logoBitmap))

} else{

expect(outputLogoJimp.getMIME()).toEqual('image/png')

}

你可能注意到了, 對于 bitmap 的一致判定, 我只判斷了 chrome 瀏覽器, 這是一個我目前也都沒有找到具體原因的地方, 接下來用一個段落詳解原因

為什么只對 chrome 瀏覽器判斷 bitmap

本段只針對 Windows 平臺, 在 macos 平臺下, Firefox 是可以正常的讀取出在粘貼板中的圖片的 bitmap

在剛開始寫測試的時候, 我篤定瀏覽器可以正常的讀取出在粘貼板中的圖片的 bitmap, 但是經過后續的測試發現只有 chrome 能正確的讀取圖片的 bitmap, IE 11(hack 方式處理), Firefox(標準的方法) 均無法保證讀取出來的圖片的 bitmap 和最初的圖片的 bitmap 完全一致, 雖然有時肉眼并無法分辨出圖片的細節. 最明顯的一個問題是透明通道丟失了, 初以為是 IE 11 在讀取的時候做了處理, 后來發現 Firefox 也是如此, 并且同一張圖片, 在 IE11和 Firefox 中的結果一致, 所以做出了以下猜測:

IE 11 和 Firefox 都是調用 Windows 提供的某一個接口, 是這個接口讀取操作系統粘貼板的時候做了一些操作, 這也能解釋為什么 Firefox 在 macos 平臺上面是正常的

Chrome 為什么是正常的? chrome 調用了不同的接口, 或者是自己實現了接口

所以最后只針對 chrome 做了 bitmap 的對比, 而在 IE11 和 Firefox 上則只判斷接收到了一張 png 圖片 expect(outputLogoJimp.getMIME()).toEqual('image/png')

你可以使用官方的測試用例來測試一下不同的瀏覽器 w3c-test.org/clipboard-a…

總結

IE 11 獲取粘貼板中的圖片需要 hack 的方式

用 TDD 的方式來測試這個功能其實非常復雜, 因為涉及到了操作系統, 而操作系統又是一個很難以 mock 的對象, 所以只能操作真正的操作系統, Macos 平臺和 Windows 平臺提供的接口不一致, 使用 electron 可以幫助抹平平臺差異.

只有 chrome 保證了讀取出來的圖片的 bitmap 是和原始的圖片的 bitmap 完全一致, 其他瀏覽器均不能保證(在 Windows 下, Macos 下 chrome, Firefox 均可以保證, Safari 沒有測試). 所以盡量不要測試圖片的 bitmap 是否一致, 測試是一張圖片就夠了.

測試步驟:

?準備測試圖片, 計算圖片的 bitmap.

?將圖片寫入操作系統. 注意這里必須對這個操作做測試, 以保證寫入的和讀取出來的圖片數據一致

??按下 ctrl+v. Macos 的 chrome 下按下的是 shift+insert

?頁面發起請求, 并且傳入的是一張 png 圖數據, 并且圖片的 bitmap 和第一步準備的圖片的 bitmap 一致. selenium 測試沒有找到監聽請求的方法, 可以 mock 一個 server. 只有 chrome 需要測試 bitmap, 其余瀏覽器測試接收到的是一張 png 圖片就可

?留下的問題

到底是什么原因導致的 Firefox 和 IE11 在 Windows 下無法讀取出一致的圖片的 bitmap ?

關于本文 作者:Jiang-Xuan 原文:https://juejin.im/post/5e0d965ef265da5d597e0fd5

fb19bbc469f4aa30dd52a4a3eb0e3712.png

你可能還喜歡看

【譯】WebAssembly 1.0成為W3C推薦標準,也是在瀏覽器中運行的第四種語言

【第1537期】Fusion Next 之 Upload 上傳組件設計思路

【第1262期】Jenkins打造強大的前端自動化工作流

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/258995.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/258995.shtml
英文地址,請注明出處:http://en.pswp.cn/news/258995.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

在C++中調用DLL中的函數

轉載自:http://blog.sina.com.cn/s/blog_53004b4901009h3b.html   應用程序使用DLL可以采用兩種方式:一種是隱式鏈接,另一種是顯式鏈接。在使用DLL之前首先要知道DLL中函數的結構信息。Visual C6.0(或者更先進的版…

CentOS LVS安裝配置

目錄(?)[] 一般2.6.10以上內核版本都已經自帶了ipvsadm,故不需要安裝。 Ipvs 1.25編譯 ipvsadm-1.25編譯不過 去掉netlink庫的依賴:去掉libipvs/Makefile的CFLAGS -DLIBIPVS_USE_NL,去掉Makefile的LIBS -lnl。需要popt庫解析命令行,在這里…

《淘寶網開店 拍攝 修圖 設計 裝修 實戰150招》一一2.7 橫式構圖和豎式構圖...

本節書摘來自異步社區出版社《淘寶網開店 拍攝 修圖 設計 裝修 實戰150招》一書中的第2章,第2.7節,作者: 葛存山,更多章節內容可以訪問云棲社區“異步社區”公眾號查看。 2.7 橫式構圖和豎式構圖 橫幅畫面,即畫面底邊…

Node.js初接觸(一)

本來還在糾結著到底要學哪一種后臺語言呢,突然發現node.js很火,既然能被這么多人推崇,自然是有他的優勢的。去百度百科看了一眼,或許是我理解能力太差,并沒有了解到很多關于node.js的東西,大概就是知道了No…

python request file upload_Python基于requests實現模擬上傳文件

方法1: 1.安裝requests_toolbelt依賴庫 #代碼實現 def upload(self): login_token self.token.loadTokenList() for token in login_token: tempPassword_url self.config[crm_test_api]/document/upload tempPassword_data self.data_to_str.strToDict(title:1.…

MATLAB中的randi函數

randi Pseudorandom integers from a uniform discrete distribution.來自一個均勻離散分布的偽隨機整數 R randi(IMAX,N) returns an N-by-N matrix containing pseudorandom integer values drawn from the discrete uniform distribution on 1:IMAX.返回一個NN的包含偽隨機…

C++ dll的隱式與顯式調用

轉載自:http://blog.sina.com.cn/s/blog_53004b4901009h3b.html   應用程序使用DLL可以采用兩種方式:一種是隱式鏈接,另一種是顯式鏈接。在使用DLL之前首先要知道DLL中函數的結構信息。Visual C6.0&…

《OpenGL ES 2.0游戲開發(上卷):基礎技術和典型案例》——6.5節光照的每頂點計算與每片元計算...

本節書摘來自異步社區《OpenGL ES 2.0游戲開發(上卷):基礎技術和典型案例》一書中的第6章,第6.5節光照的每頂點計算與每片元計算,作者 吳亞峰,更多章節內容可以訪問云棲社區“異步社區”公眾號查看 6.5 光照…

毛筆筆鋒算法IOS版

http://www.merowing.info/2012/04/drawing-smooth-lines-with-cocos2d-ios-inspired-by-paper/#.VUln2_mqpBe轉載于:https://www.cnblogs.com/wangjinming/p/4481145.html

USE PDFCREATE TO CREATE A PDF FILE

來源:http://www.pdfforge.org/files/old_forum/1002.html a working sample with C & COM 2007-08-15 22:56:18 by eckart hi, here is a working sample of how to use PDFCreator in Visual C (after searching on internet for something similar I guess t…

python將一行作為字段_關于python:Django admin在同一行顯示多個字段

我已經創建了一個模型,它將自動顯示模型中的所有字段,并將其顯示在管理頁面上。 現在,我有一個問題,我希望在同一行中有兩個字段,為此,我必須在modeladmin中指定字段集: 1 2 3 4 5fieldsets ( …

c++顯式加載dll并使用DLL的類

轉載自: http://blog.163.com/tianjunqiang666126/blog/static/8725911920121064573594/ 首先需要強調,當使用某個類時一般目的有二:實例化成對象或者繼承它產生新類。對于前者,我們可以構造一個抽象類&a…

如何在Debian上安裝配置ownCloud

如何在Debian上安裝配置ownCloud 據其官方網站,ownCloud可以讓你通過一個Web界面或者WebDAV訪問你的文件。它還提供了一個平臺,可以輕松地查看、編輯和同步您所有設備的通訊錄、日歷和書簽。盡管ownCloud與廣泛使用Dropbox非常相似,但主要區別…

jQuery相當于對 javascript二次開發,所以基于 jQuery實現的各種插件直接調用即可...

jQuery相當于對 javascript二次開發,所以基于 jQuery實現的各種插件直接調用即可轉載于:https://www.cnblogs.com/npk19195global/p/4482363.html

[轉]js判斷url是否有效

本文轉自:http://www.cnblogs.com/fumj/p/3490121.html 方法一:(僅適用于ie) function CheckStatus(url){XMLHTTP new ActiveXObject("Microsoft.XMLHTTP")XMLHTTP.open("HEAD",url,false)XMLHTTP.send()return XMLHTTP.status200}function Ne…

VS中lib和dll

轉載: http://www.cnblogs.com/Yogurshine/archive/2013/06/14/3136025.html Lib文件 先來說一說lib文件,C中lib文件主要有兩類,一種是靜態的編譯連接,叫做靜態鏈接庫,另一種是動態的編譯鏈…

32位md5解密_冰蝎特征檢測及報文解密

點擊“藍字”關注我們,不迷路~??前言19年駐場于某金融單位。參加19年9月、11月兩次攻防演練,負責攻防演練組織、技術支持和復盤。期間,多個攻擊隊伍使用冰蝎 webshell ,防守方監測時確實各 IDS 確實報出 webshell 連接&#xff…

使用extern C改善顯式調用dll

extern "C"的簡單解析 我們前面介紹了顯式調用dll的方法,例如 http://www.cnblogs.com/laogao/archive/2012/12/07/2806528.html ,其中在GetProcAddress第二個參數的填寫煞費苦心,我們需要比較麻煩…

《移動App測試的22條軍規》—App測試綜合案例分析23.13節測試微信App的流量和電量消耗...

本節書摘來自異步社區《移動App測試的22條軍規》一書中的App測試綜合案例分析,第23.13節測試微信App的流量和電量消耗,作者黃勇,更多章節內容可以訪問云棲社區“異步社區”公眾號查看。 23.13 測試微信App的流量和電量消耗關于微信App消耗流…

UVA 10269 Super Mario,最短路+動態規劃

這個題目我昨晚看到的,沒什么思路,因為馬里奧有boot加速器,只要中間沒有城堡,即可不耗時間和腳力,瞬間移動不超過L距離,遇見城堡就要停下來,當然不能該使用超過K次。。。我糾結了很久&#xff0…