Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/codecept.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ class Codecept {
if (!this.config.noGlobals) {
global.Helper = global.codecept_helper = require('@codeceptjs/helper')
global.actor = global.codecept_actor = require('./actor')
global.pause = require('./pause')
global.pause = require('./debug').pause
global.within = require('./within')
global.session = require('./session')
global.DataTable = require('./data/table')
Expand Down
2 changes: 1 addition & 1 deletion lib/command/interactive.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ module.exports = async function (path, options) {
break
}
}
require('../pause')()
require('../debug').pause()
// recorder.catchWithoutStop((err) => console.log(err.stack));
recorder.add(() => event.emit(event.test.after, {}))
recorder.add(() => event.emit(event.suite.after, {}))
Expand Down
238 changes: 238 additions & 0 deletions lib/debug.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
const colors = require('chalk')
const readline = require('readline')
const ora = require('ora-classic')
const debug = require('debug')('codeceptjs:pause')

const container = require('./container')
const history = require('./history')
const store = require('./store')
const aiAssistant = require('./ai')
const recorder = require('./recorder')
const event = require('./event')
const output = require('./output')
const { methodsOfObject, searchWithFusejs } = require('./utils')

// npm install colors
let rl
let nextStep
let finish
let next
let registeredVariables = {}
/**
* Pauses test execution and starts interactive shell
* @param {Object<string, *>} [passedObject]
*/
const pause = function (passedObject = {}) {
if (store.dryRun) return

next = false
// add listener to all next steps to provide next() functionality
event.dispatcher.on(event.step.after, () => {
recorder.add('Start next pause session', () => {
// test already finished, nothing to pause
if (!store.currentTest) return
if (!next) return
return pauseSession()
})
})

event.dispatcher.on(event.test.finished, () => {
finish()
recorder.session.restore('pause')
rl.close()
history.save()
})

recorder.add('Start new session', () => pauseSession(passedObject))
}

function pauseSession(passedObject = {}) {
registeredVariables = passedObject
recorder.session.start('pause')
if (!next) {
let vars = Object.keys(registeredVariables).join(', ')
if (vars) vars = `(vars: ${vars})`

output.print(colors.yellow(' Interactive shell started'))
output.print(colors.yellow(' Use JavaScript syntax to try steps in action'))
output.print(colors.yellow(` - Press ${colors.bold('ENTER')} to run the next step`))
output.print(colors.yellow(` - Press ${colors.bold('TAB')} twice to see all available commands`))
output.print(colors.yellow(` - Type ${colors.bold('exit')} + Enter to exit the interactive shell`))
output.print(colors.yellow(` - Prefix ${colors.bold('=>')} to run js commands ${colors.bold(vars)}`))

if (aiAssistant.isEnabled) {
output.print(colors.blue(` ${colors.bold('AI is enabled! (experimental)')} Write what you want and make AI run it`))
output.print(colors.blue(' Please note, only HTML fragments with interactive elements are sent to AI provider'))
output.print(colors.blue(' Ideas: ask it to fill forms for you or to click'))
}
}

rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: true,
completer,
history: history.load(),
historySize: 50, // Adjust the history size as needed
})

store.onPause = true
rl.on('line', parseInput)
rl.on('close', () => {
if (!next) console.log('Exiting interactive shell....')
store.onPause = false
})
return new Promise(resolve => {
finish = resolve
// eslint-disable-next-line
return askForStep()
})
}

/* eslint-disable */
async function parseInput(cmd) {
rl.pause()
next = false
recorder.session.start('pause')
if (cmd === '') next = true
if (!cmd || cmd === 'resume' || cmd === 'exit') {
finish()
recorder.session.restore('pause')
rl.close()
history.save()
return nextStep()
}
for (const k of Object.keys(registeredVariables)) {
eval(`var ${k} = registeredVariables['${k}'];`)
}

let executeCommand = Promise.resolve()

const getCmd = () => {
debug('Command:', cmd)
return cmd
}

let isCustomCommand = false
let lastError = null
let isAiCommand = false
let $res
try {
const locate = global.locate // enable locate in this context

const I = container.support('I')
if (cmd.trim().startsWith('=>')) {
isCustomCommand = true
cmd = cmd.trim().substring(2, cmd.length)
} else if (aiAssistant.isEnabled && cmd.trim() && !cmd.match(/^\w+\(/) && cmd.includes(' ')) {
const currentOutputLevel = output.level()
output.level(0)
const res = I.grabSource()
isAiCommand = true
executeCommand = executeCommand.then(async () => {
try {
const html = await res
await aiAssistant.setHtmlContext(html)
} catch (err) {
output.print(output.styles.error(' ERROR '), "Can't get HTML context", err.stack)
return
} finally {
output.level(currentOutputLevel)
}

const spinner = ora('Processing AI request...').start()
cmd = await aiAssistant.writeSteps(cmd)
spinner.stop()
output.print('')
output.print(colors.blue(aiAssistant.getResponse()))
output.print('')
return cmd
})
} else {
cmd = `I.${cmd}`
}
executeCommand = executeCommand
.then(async () => {
const cmd = getCmd()
if (!cmd) return
return eval(cmd)
})
.catch(err => {
debug(err)
if (isAiCommand) return
if (!lastError) output.print(output.styles.error(' ERROR '), err.message)
debug(err.stack)

lastError = err.message
})

const val = await executeCommand

if (isCustomCommand) {
if (val !== undefined) console.log('Result', '$res=', val)
$res = val
}

if (cmd?.startsWith('I.see') || cmd?.startsWith('I.dontSee')) {
output.print(output.styles.success(' OK '), cmd)
}
if (cmd?.startsWith('I.grab')) {
output.print(output.styles.debug(val))
}

history.push(cmd) // add command to history when successful
} catch (err) {
if (!lastError) output.print(output.styles.error(' ERROR '), err.message)
lastError = err.message
}
recorder.session.catch(err => {
const msg = err.cliMessage ? err.cliMessage() : err.message

// pop latest command from history because it failed
history.pop()

if (isAiCommand) return
if (!lastError) output.print(output.styles.error(' FAIL '), msg)
lastError = err.message
})
recorder.add('ask for next step', askForStep)
nextStep()
}
/* eslint-enable */

function askForStep() {
return new Promise(resolve => {
nextStep = resolve
rl.setPrompt(' I.', 3)
rl.resume()
rl.prompt([false])
})
}

function completer(line) {
const I = container.support('I')
const completions = methodsOfObject(I)
// If no input, return all completions
if (!line) {
return [completions, line]
}

// Search using Fuse.js
const searchResults = searchWithFusejs(completions, line, {
threshold: 0.3,
distance: 100,
minMatchCharLength: 1,
})
const hits = searchResults.map(result => result.item)

return [hits, line]
}

function registerVariable(name, value) {
registeredVariables[name] = value
}

module.exports = {
pause,
registerVariable,
}
2 changes: 1 addition & 1 deletion lib/helper/AI.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const Container = require('../container')
const { splitByChunks, minifyHtml } = require('../html')
const { beautify } = require('../utils')
const output = require('../output')
const { registerVariable } = require('../pause')
const { registerVariable } = require('../debug')

const gtpRole = {
user: 'user',
Expand Down
4 changes: 2 additions & 2 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ module.exports = {
/** @type {typeof CodeceptJS.Helper} */
Helper: require('./helper'),
/** @type {typeof CodeceptJS.pause} */
pause: require('./pause'),
pause: require('./debug').pause,
/** @type {typeof CodeceptJS.within} */
within: require('./within'),
/** @type {typeof CodeceptJS.DataTable} */
Expand All @@ -43,4 +43,4 @@ module.exports = {
ai: require('./ai'),

Workers: require('./workers'),
};
}
Loading
Loading