*
* This command helps you to debug your integration tests. It stops the running browser and gives
* you time to jump into it and check the state of your application (e.g. using dev tools).
* Your terminal transforms into a [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop)
* interface that will allow you to try out certain commands, find elements and test actions on
* them.
*
* [](https://webdriver.io/img/repl.gif)
*
* If you run the WDIO testrunner make sure you increase the timeout property of the test framework
* you are using (e.g. Mocha or Jasmine) in order to prevent test termination due to a test timeout.
* Also avoid executing the command with multiple capabilities running at the same time.
*
* <iframe width="560" height="315" src="https://www.youtube.com/embed/xWwP-3B_YyE" frameBorder="0" allowFullScreen></iframe>
*
* <example>
:debug.js
it('should demonstrate the debug command', () => {
$('#input').setValue('FOO')
browser.debug() // jumping into the browser and change value of #input to 'BAR'
const value = $('#input').getValue()
console.log(value) // outputs: "BAR"
})
* </example>
*
* @alias browser.debug
* @type utility
*
*/
import vm from 'node:vm'
import repl from 'node:repl'
import { STATIC_RETURNS, INTRO_MESSAGE, DEFAULT_CONFIG } from './constants.js'
export interface ReplConfig {
commandTimeout: number
eval: repl.REPLEval
prompt: string
useGlobal: boolean
useColor: boolean
}
export type ReplCallback = (err: Error | null, result: unknown) => void
export default class WDIORepl {
static introMessage = INTRO_MESSAGE
private _config: ReplConfig
private _isCommandRunning = false
private _replServer: repl.REPLServer | undefined
constructor(config?: ReplConfig) {
this._config = Object.assign(
DEFAULT_CONFIG,
{ eval: this.eval.bind(this) },
config
)
}
eval (cmd: string, context: vm.Context, filename: string | undefined, callback: ReplCallback) {
if (this._isCommandRunning) {
return
}
if (cmd && STATIC_RETURNS[cmd.trim()]) {
return callback(null, STATIC_RETURNS[cmd.trim()])
}
vm.createContext(context)
this._isCommandRunning = true
return this._runCmd(cmd, context, callback)
}
private _runCmd (cmd: string, context: vm.Context, callback: ReplCallback) {
try {
const result = vm.runInContext(cmd, context)
return this._handleResult(result, callback)
} catch (e: unknown) {
this._isCommandRunning = false
return callback(e as Error, undefined)
}
}
private _handleResult (result: Promise<unknown>, callback: ReplCallback) {
if (!result || typeof result.then !== 'function') {
this._isCommandRunning = false
return callback(null, result)
}
let timeoutCalled = false
const timeout = setTimeout(
() => {
callback(new Error('Command execution timed out'), undefined)
this._isCommandRunning = false
timeoutCalled = true
},
this._config.commandTimeout
)
result.then((res: unknown) => {
* don't do anything if timeout was called
*/
if (timeoutCalled) {
return
}
this._isCommandRunning = false
clearTimeout(timeout)
return callback(null, res)
}, (e: Error) => {
* don't do anything if timeout was called
*/
if (timeoutCalled) {
return
}
this._isCommandRunning = false
clearTimeout(timeout)
const errorMessage = e ? e.message : 'Command execution timed out'
const commandError = new Error(errorMessage)
delete commandError.stack
return callback(commandError, undefined)
})
}
start (context?: vm.Context) {
if (this._replServer) {
throw new Error('a repl was already initialized')
}
if (context) {
const evalFn = this._config.eval
this._config.eval = function (cmd, _, filename, callback) {
return evalFn.call(this, cmd, context, filename, callback)
}
}
this._replServer = repl.start(this._config)
return new Promise((resolve) => {
return (this._replServer as repl.REPLServer).on('exit', resolve)
})
}
}