flotwig / cypress-log-to-output

A Cypress plugin that sends all logs that occur in the browser to stdout in the terminal.
146 stars 19 forks source link

Update plugin to be compatible with Cypress 10 #22

Open admah opened 2 years ago

admah commented 2 years ago

Hello 👋 I'm writing on behalf of the Cypress DX team. We wanted to notify you that some changes may need to be made to this plugin to ensure that it works in Cypress 10.

For more information, here is our official plugin update guide.

vaclavGabriel commented 2 years ago

Hello @flotwig, may you please update your great plugin so it is compatible with the Cypress 10? 🙏 There is no alternative for debugging console logs. Many thanks!

kpturner commented 2 years ago

I have found that the plugin still works OK UNLESS you have a plugin after it that also listens to before:browser:launch

See https://github.com/cypress-io/cypress/discussions/23465

JohnnyDevNull commented 2 years ago

After trying to get this plugin to work with Cypress 10 i have to say, that it doesn't work in headless mode.

The Chrome debugger gets disconnected after the first suite and then is gone... no reconnect.

JohnnyDevNull commented 2 years ago

Maybe if someone is interested i've got it to work. I will just post it here because i don't think that this repo is getting some updates:

You can install the plugin that way:

    async setupNodeEvents(on, _config) {
      require('./src/plugins/cypress-log-to-output/index').install(on);
    },

The key change is to do the debugger connect in the before:spec hook:

function install(on, filter, options = {}) {
  eventFilter = filter;
  recordLogs = options.recordLogs;
  on('before:browser:launch', browserLaunchHandler)
  on('before:spec', tryConnect)
}

I also added some cleanup logic to the tryConnect function and some logging improvements, here is the full version:

const CDP = require('chrome-remote-interface')
const chalk = require('chalk')

let eventFilter
let recordLogs
let remoteDebugPort
let CDPInstance

let messageLog = [];

const severityColors = {
  'verbose': (a) => a,
  'info': chalk.blue,
  'warning': chalk.yellow,
  'error': chalk.red
}

const severityIcons = {
  'verbose': ' ',
  'info': '🛈',
  'warning': '⚠',
  'error': '⚠',
}

function debugLog(msg) {
  // suppress with DEBUG=-cypress-log-to-output
  if (process.env.DEBUG && process.env.DEBUG.includes('-cypress-log-to-output')) {
    return
  }

  log(`[cypress-log-to-output] ${msg}`)
}

function log(msg) {
  console.log(msg)
}

function logEntry(params) {
  if (eventFilter && !eventFilter('browser', params.entry)) {
    return
  }

  const { level, source, text, timestamp, url, lineNumber, stackTrace, args } = params.entry
  const color = severityColors[level]
  const icon = severityIcons[level]

  const prefix = `[${new Date(timestamp).toISOString()}] ${icon} `
  const prefixSpacer = ' '.repeat(prefix.length)

  let logMessage = `${prefix}${chalk.bold(level)} (${source}): ${text}`;
  log(color(logMessage));
  recordLogMessage(logMessage);

  const logAdditional = (msg) => {
    let additionalLogMessage = `${prefixSpacer}${msg}`;
    log(color(additionalLogMessage));
    recordLogMessage(additionalLogMessage);
  };

  if (url) {
    logAdditional(`${chalk.bold('URL')}: ${url}`)
  }

  if (stackTrace && lineNumber) {
    logAdditional(`Stack trace line number: ${lineNumber}`)
    logAdditional(`Stack trace description: ${stackTrace.description}`)
    logAdditional(`Stack call frames: ${stackTrace.callFrames.join(', ')}`)
  }

  if (args) {
    logAdditional(`Arguments:`)
    logAdditional('  ' + JSON.stringify(args, null, 2).split('\n').join(`\n${prefixSpacer}  `).trimRight())
  }
}

function logConsole(params) {
  if (eventFilter && !eventFilter('console', params)) {
    return
  }

  const { type, args, timestamp } = params
  const level = type === 'error' ? 'error' : 'verbose'
  const color = severityColors[level]
  const icon = severityIcons[level]

  const prefix = `[${new Date(timestamp).toISOString()}] ${icon} `
  const prefixSpacer = ' '.repeat(prefix.length)

  let logMessage = `${prefix}${chalk.bold(`console.${type}`)} called`;
  log(color(logMessage));
  recordLogMessage(logMessage);

  const logAdditional = (msg) => {
    let logMessage = `${prefixSpacer}${msg}`;
    log(color(logMessage));
    recordLogMessage(logMessage);
  };

  if (args) {
    logAdditional(`Arguments:`)
    logAdditional('  ' + JSON.stringify(args.map(i => i.value), null, 2).split('\n').join(`\n${prefixSpacer}  `).trimRight())
  }
}

function install(on, filter, options = {}) {
  eventFilter = filter;
  recordLogs = options.recordLogs;
  on('before:browser:launch', browserLaunchHandler)
  on('before:spec', tryConnect)
}

function recordLogMessage(logMessage) {
  if (recordLogs) {
    messageLog.push(logMessage);
  }
}

function getLogs() {
  return messageLog;
}

function clearLogs() {
  messageLog = [];
}

function isChrome(browser) {
  return browser.family === 'chrome' || ['chrome', 'chromium', 'canary'].includes(browser.name) || (browser.family === 'chromium' && browser.name !== 'electron')
}

function ensureRdpPort(args) {
  let port
  const existing = args.find(arg => arg.slice(0, 23) === '--remote-debugging-port')

  if (existing) {
    port = Number(existing.split('=')[1]);
    debugLog('Use existing port ' + port)
    return port
  }

  port = 40000 + Math.round(Math.random() * 25000)
  debugLog('Use custom port ' + port)
  args.push(`--remote-debugging-port=${port}`)

  return port
}

function tryConnect() {
  debugLog('Attempting to connect to Chrome Debugging Protocol on port ' + remoteDebugPort)

  if (typeof remoteDebugPort !== 'number') return
  if (CDPInstance != null) return

  CDPInstance = new CDP({ port: remoteDebugPort })
    .then((cdp) => {
      debugLog('Connected to Chrome Debugging Protocol')

      /** captures logs from the browser */
      cdp.Log.enable()
      cdp.Log.entryAdded(logEntry)

      /** captures logs from console.X calls */
      cdp.Runtime.enable()
      cdp.Runtime.consoleAPICalled(logConsole)

      cdp.on('disconnect', () => {
        debugLog('Chrome Debugging Protocol disconnected')
        CDPInstance = undefined;
      })
    })
    .catch(() => {
      CDPInstance = undefined;
      setTimeout(tryConnect, 100)
    })
}

function browserLaunchHandler(browser = {}, launchOptions) {
  const args = launchOptions.args || launchOptions

  if (!isChrome(browser)) {
    return debugLog(`Warning: An unsupported browser family was used, output will not be logged to console: ${browser.family}`)
  }

  remoteDebugPort = ensureRdpPort(args)

  tryConnect()
}

module.exports = {
  _ensureRdpPort: ensureRdpPort,
  install,
  browserLaunchHandler,
  getLogs,
  clearLogs
}
arminrosu commented 2 years ago

To those who landed here hoping to find a working plugin for cypress@10 - this is the plugin you're looking for: https://github.com/archfz/cypress-terminal-report