Home Reference Source

lib/coverage/CSSCoverage.js

const { helper, debugError, assert } = require('../helper')
const { convertToDisjointRanges } = require('../__shared')

class CSSCoverage {
  /**
   * @param {!Chrome|CRIConnection|CDPSession|Object} client
   */
  constructor (client) {
    this._client = client
    this._enabled = false
    this._stylesheetURLs = new Map()
    this._stylesheetSources = new Map()
    this._eventListeners = []
    this._resetOnNavigation = false
  }

  /**
   * @param {{resetOnNavigation?: boolean}=} options
   */
  async start (options = {}) {
    assert(!this._enabled, 'CSSCoverage is already enabled')
    const { resetOnNavigation = true } = options
    this._resetOnNavigation = resetOnNavigation
    this._enabled = true
    this._stylesheetURLs.clear()
    this._stylesheetSources.clear()
    this._eventListeners = [
      helper.addEventListener(
        this._client,
        'CSS.styleSheetAdded',
        this._onStyleSheet.bind(this)
      ),
      helper.addEventListener(
        this._client,
        'Runtime.executionContextsCleared',
        this._onExecutionContextsCleared.bind(this)
      )
    ]
    await Promise.all([
      this._client.send('DOM.enable'),
      this._client.send('CSS.enable'),
      this._client.send('CSS.startRuleUsageTracking')
    ])
  }

  _onExecutionContextsCleared () {
    if (!this._resetOnNavigation) return
    this._stylesheetURLs.clear()
    this._stylesheetSources.clear()
  }

  /**
   * @param {!Object} event
   */
  async _onStyleSheet (event) {
    const header = event.header
    // Ignore anonymous scripts
    if (!header.sourceURL) return
    try {
      const response = await this._client.send('CSS.getStyleSheetText', {
        styleSheetId: header.styleSheetId
      })
      this._stylesheetURLs.set(header.styleSheetId, header.sourceURL)
      this._stylesheetSources.set(header.styleSheetId, response.text)
    } catch (e) {
      // This might happen if the page has already navigated away.
      debugError(e)
    }
  }

  /**
   * @return {Promise<Array<CoverageEntry>>}
   */
  async stop () {
    assert(this._enabled, 'CSSCoverage is not enabled')
    this._enabled = false
    const ruleTrackingResponse = await this._client.send(
      'CSS.stopRuleUsageTracking'
    )
    await Promise.all([
      this._client.send('CSS.disable'),
      this._client.send('DOM.disable')
    ])
    helper.removeEventListeners(this._eventListeners)

    // aggregate by styleSheetId
    const styleSheetIdToCoverage = new Map()
    for (const entry of ruleTrackingResponse.ruleUsage) {
      let ranges = styleSheetIdToCoverage.get(entry.styleSheetId)
      if (!ranges) {
        ranges = []
        styleSheetIdToCoverage.set(entry.styleSheetId, ranges)
      }
      ranges.push({
        startOffset: entry.startOffset,
        endOffset: entry.endOffset,
        count: entry.used ? 1 : 0
      })
    }

    const coverage = []
    for (const styleSheetId of this._stylesheetURLs.keys()) {
      const url = this._stylesheetURLs.get(styleSheetId)
      const text = this._stylesheetSources.get(styleSheetId)
      const ranges = convertToDisjointRanges(
        styleSheetIdToCoverage.get(styleSheetId) || []
      )
      coverage.push({ url, ranges, text })
    }

    return coverage
  }
}

/**
 *
 * @type {CSSCoverage}
 */
module.exports = CSSCoverage