wdio-screen-commands
Version:
WebdriverIO commands to capture and record browser screens.
258 lines (239 loc) • 8.09 kB
JavaScript
/*
* Webdriver.io commands to capture and record browser screens.
*
* Copyright 2019, Sebastian Tschan
* https://blueimp.net
*
* Licensed under the MIT license:
* https://opensource.org/licenses/MIT
*/
/* global browser */
/* eslint-disable jsdoc/valid-types */
/**
* @typedef {import('adb-record-screen').Options} ADBOptions
* @typedef {import('record-screen').Options} FFmpegOptions
* @typedef {import('record-screen').Recording} Recording
* @typedef {import('record-screen').Result} RecordingResult
* @typedef {import('ffmpeg-image-diff').Options} ImageDiffOptions
* @typedef {import('ffmpeg-image-diff').Result} ImageDiffResult
* @typedef {import('@wdio/types').Frameworks.Test} WebdriverIOTest
* @typedef {import('@wdio/types').Frameworks.TestResult} WebdriverIOTestResult
*/
/**
* @typedef {object} ScreenshotOptions Screenshot options
* @property {string} [dir=reports/screenshots] Screenshots directory
* @property {boolean} [saveOnFail] Automatically save screenshots on test fail
* @property {boolean} [saveOnPass] Automatically save screenshots on test pass
* @property {ImageDiffOptions} [imageDiff] Image diffing options
*/
/**
* @typedef {object} RecordingOptions Video recording options
* @property {string} [dir=reports/videos] Videos directory
* @property {boolean} [enabled] Set to true to enable video recordings
* @property {boolean} [deleteOnPass] Deletes screen recordings on pass if true
* @property {number} [startDelay] Delay in ms after starting the recording
* @property {number} [stopDelay] Delay in ms before stopping the recording
*/
/**
* @typedef {FFmpegOptions & ADBOptions & RecordingOptions} VideoOptions
*/
/* eslint-enable jsdoc/valid-types */
const fs = require('fs')
const path = require('path')
const { promisify } = require('util')
const mkdir = promisify(fs.mkdir)
const rename = promisify(fs.rename)
const unlink = promisify(fs.unlink)
const logger = require('@wdio/logger').default
const imageDiffLogger = logger('image-diff')
const screenRecordingLogger = logger('screen-recording')
const imageDiff = require('ffmpeg-image-diff')
const ffmpegRecordScreen = require('record-screen')
const adbRecordScreen = require('adb-record-screen')
const screenRecordings = new Map()
/**
* Starts a screen recording via ffmpeg or adb shell screenrecord.
*
* @param {string} fileName Output file name
* @param {VideoOptions} [options] Screen recording options
* @returns {Recording} Recording object
*/
function recordScreen(fileName, options) {
// The ffmpeg version requires either inputFormat or resolution to be set,
// neither of which are options for the adb version:
if (options.inputFormat || options.resolution) {
return ffmpegRecordScreen(fileName, options)
}
return adbRecordScreen(fileName, options)
}
/**
* Sanitizes the basename of a file path.
*
* @param {string} str Input string
* @returns {string} Sanitized string
*/
function sanitizeBaseName(str) {
// Remove non-word characters from the start and end of the string.
// Replace everything but word characters, dots and spaces with a dash.
return str.replace(/^\W+|\W+$/g, '').replace(/[^\w. -]+/g, '-')
}
/**
* Creates a sanitized file path with browser info as directory.
*
* @param {string} name Base filename
* @param {string} ext File extension
* @param {string} [baseDir=reports] Base directory
* @returns {Promise<string>} Resolves with the file path
*/
async function createFileName(name, ext, baseDir = 'reports') {
const caps = browser.capabilities
const dir = path.join(
baseDir,
sanitizeBaseName(
[
caps.browserName,
String(caps.browserVersion || caps.version || '')
.split('.')
.slice(0, 2)
.join('.'),
caps.platformName || caps.platform,
String(caps.platformVersion || caps['safari:platformVersion'] || '')
.split('.')
.slice(0, 2)
.join('.'),
caps.deviceName || caps['safari:deviceName'],
caps.orientation
]
.filter(s => s) // Remove empty ('', undefined, null, 0) values
.join(' ')
)
)
await mkdir(dir, { recursive: true })
return path.format({
dir,
name: sanitizeBaseName(name),
ext
})
}
/**
* Saves a screenshot for the given name.
*
* @param {string} name Screenshot name
*/
async function saveScreenshotByName(name) {
const options = Object.assign(
{ dir: 'reports/screenshots' },
browser.config.screenshots
)
const fileName = await createFileName(name, '.png', options.dir)
await this.saveScreenshot(fileName)
}
/**
* Saves a screenshot for the given test.
*
* @param {WebdriverIOTest} test WebdriverIO Test
* @param {WebdriverIOTestResult} result WebdriverIO Test result
*/
async function saveScreenshotByTest(test, result) {
const options = browser.config.screenshots || {}
if (result.passed) {
if (options.saveOnPass) {
await saveScreenshotByName.call(
browser,
`${test.parent} ${test.title} PASSED`
)
}
} else {
if (options.saveOnFail) {
await saveScreenshotByName.call(
browser,
`${test.parent} ${test.title} FAILED`
)
}
}
}
/**
* Saves and diffs a screenshot for the given name.
*
* @param {string} name Screenshot name
* @returns {Promise<ImageDiffResult>} Resolves with the image diff results
*/
async function saveAndDiffScreenshot(name) {
const options = Object.assign(
{ dir: 'reports/screenshots' },
browser.config.screenshots
)
const fileName = await createFileName(name, '.png', options.dir)
if (fs.existsSync(fileName)) {
const [fileNameOriginal, fileNameDifference] = await Promise.all([
createFileName(name + ' original', '.png', options.dir),
createFileName(name + ' diff', '.png', options.dir)
])
await rename(fileName, fileNameOriginal)
await this.saveScreenshot(fileName)
const ssim = await imageDiff(
fileNameOriginal,
fileName,
fileNameDifference,
options.imageDiff
).catch(err => imageDiffLogger.error(err))
if (!ssim) return
if (ssim.All < 1) {
imageDiffLogger.warn(name, ssim)
} else {
await Promise.all([unlink(fileNameOriginal), unlink(fileNameDifference)])
}
return ssim
}
await this.saveScreenshot(fileName)
}
/**
* Starts a streen recording for the given test.
*
* @param {WebdriverIOTest} test WebdriverIO Test
*/
async function startScreenRecording(test) {
const options = Object.assign(
{ dir: 'reports/videos', hostname: browser.config.hostname },
browser.config.videos
)
if (!options.enabled) return
const name = `${test.parent} ${test.title}`
const videoKey = `${browser.sessionId} ${name}`
const fileName = await createFileName(name, '.mp4', options.dir)
const recording = recordScreen(fileName, options)
screenRecordings.set(videoKey, { options, recording, fileName })
recording.promise.catch(err => screenRecordingLogger.error(err))
if (options.startDelay) await browser.pause(options.startDelay)
}
/**
* Stops the screen recording for the given test.
*
* @param {WebdriverIOTest} test WebdriverIO Test
* @param {WebdriverIOTestResult} result WebdriverIO Test result
* @returns {Promise<RecordingResult>} Resolves with the recording result
*/
async function stopScreenRecording(test, result) {
const name = `${test.parent} ${test.title}`
const videoKey = `${browser.sessionId} ${name}`
const currentRecording = screenRecordings.get(videoKey)
if (currentRecording) {
const { options, recording, fileName } = currentRecording
screenRecordings.delete(videoKey)
if (options.stopDelay) await browser.pause(options.stopDelay)
recording.stop()
await recording.promise.catch(() => {}) // Handled by start function
if (result.passed && options.deleteOnPass) {
await unlink(fileName)
}
return recording.promise
}
}
module.exports = {
saveScreenshotByName,
saveScreenshotByTest,
saveAndDiffScreenshot,
startScreenRecording,
stopScreenRecording
}