import fs from 'node:fs/promises'
import path from 'node:path'
import { getBrowserObject } from '@wdio/utils'
import type { remote } from 'webdriver'
import { getAbsoluteFilepath, assertDirectoryExists } from './utils.js'
import { getContextManager } from '../session/context.js'
import type { SaveScreenshotOptions } from '../types.js'
/**
*
* Save a screenshot of the current browsing context to a PNG file on your OS. Be aware that
* some browser drivers take screenshots of the whole document (e.g. Geckodriver with Firefox)
* and others only of the current viewport (e.g. Chromedriver with Chrome).
*
*
:saveScreenshot.js
it('should save a screenshot of the browser view', async () => {
await browser.saveScreenshot('./some/path/screenshot.png');
});
*
*
* When running from a hook, make sure to explicitly define the hook as async:
*
:wdio.conf.js
afterTest: async function(test) {
await browser.saveScreenshot('./some/path/screenshot.png');
}
*
* @alias browser.saveScreenshot
* @param {String} filepath path to the generated image (`.png` suffix is required) relative to the execution directory
* @return {Buffer} screenshot buffer
* @type utility
*
*/
export async function saveScreenshot (
this: WebdriverIO.Browser,
filepath: string,
options?: SaveScreenshotOptions
) {
/**
* type check
*/
if (typeof filepath !== 'string') {
throw new Error('saveScreenshot expects a filepath of type string and ".png" file ending')
}
const absoluteFilepath = getAbsoluteFilepath(filepath)
await assertDirectoryExists(absoluteFilepath)
const screenBuffer = this.isBidi
? await takeScreenshotBidi.call(this, filepath, options)
: await takeScreenshotClassic.call(this, filepath, options)
const screenshot = Buffer.from(screenBuffer, 'base64')
await fs.writeFile(absoluteFilepath, screenshot)
return screenshot
}
/**
* take screenshot using legacy WebDriver command
* @returns {string} a base64 encoded screenshot
*/
export function takeScreenshotClassic (this: WebdriverIO.Browser, filepath: string, options?: SaveScreenshotOptions): Promise {
if (options) {
throw new Error('saveScreenshot does not support options in WebDriver Classic mode')
}
const fileExtension = path.extname(filepath).slice(1)
if (fileExtension !== 'png') {
throw new Error('Invalid file extension, use ".png" for PNG format')
}
return this.takeScreenshot()
}
/**
* takeScreenshotBidi
* @returns {string} a base64 encoded screenshot
*/
export async function takeScreenshotBidi (this: WebdriverIO.Browser, filepath: string, options?: SaveScreenshotOptions): Promise {
const browser = getBrowserObject(this)
const contextManager = getContextManager(browser)
const context = await contextManager.getCurrentContext()
const tree = await this.browsingContextGetTree({})
const origin: remote.BrowsingContextCaptureScreenshotParameters['origin'] = options?.fullPage ? 'document' : 'viewport'
const givenFormat = options?.format || path.extname(filepath).slice(1)
const imageFormat = givenFormat === 'png'
? 'image/png'
: givenFormat === 'jpeg' || givenFormat === 'jpg'
? 'image/jpeg'
: undefined
if (!imageFormat) {
throw new Error(`Invalid image format, use 'png', 'jpg' or 'jpeg', got '${options?.format}'`)
}
if (imageFormat === 'image/jpeg' && path.extname(filepath) !== '.jpeg' && path.extname(filepath) !== '.jpg') {
throw new Error('Invalid file extension, use ".jpeg" or ".jpg" for JPEG format')
} else if (imageFormat === 'image/png' && path.extname(filepath) !== '.png') {
throw new Error('Invalid file extension, use ".png" for PNG format')
}
const quality = typeof options?.quality === 'number' ? (options.quality / 100) : undefined
if (typeof options?.quality === 'number' && (options?.quality < 0 || options?.quality > 100)) {
throw new Error(`Invalid quality, use a number between 0 and 100, got '${options?.quality}'`)
}
if (typeof options?.quality === 'number' && imageFormat !== 'image/jpeg') {
throw new Error('Invalid option "quality" for PNG format')
}
const format: remote.BrowsingContextImageFormat = {
type: imageFormat,
quality
}
const clip: remote.BrowsingContextBoxClipRectangle | undefined = options?.clip
? {
type: 'box',
x: options.clip.x,
y: options.clip.y,
width: options.clip.width,
height: options.clip.height
}
: undefined
if (clip) {
if (typeof clip.x !== 'number' || typeof clip.y !== 'number' || typeof clip.width !== 'number' || typeof clip.height !== 'number') {
throw new Error('Invalid clip, use an object with x, y, width and height properties')
}
}
/**
* WebDriver Bidi doesn't allow to take a screenshot of an iframe, it fails with:
* "unsupported operation - Non-top-level 'context'". Therefor we need to check if
* we are within an iframe and if so, take a screenshot of the document element
* instead.
*/
const { data } = contextManager.findParentContext(context, tree.contexts)
? await browser.$('html').getElement().then(
(el) => this.takeElementScreenshot(el.elementId).then((data) => ({ data })))
: await this.browsingContextCaptureScreenshot({ context, origin, format, clip })
return data
}