Module: Applitools::Selenium::DomCapture

Extended by:
DomCapture
Included in:
DomCapture
Defined in:
lib/applitools/selenium/dom_capture/dom_capture.rb,
lib/applitools/selenium/dom_capture/dom_capture_script.rb

Defined Under Namespace

Classes: DomParts

Constant Summary collapse

DOM_EXTRACTION_TIMEOUT =

seconds

300
CAPTURE_FRAME_SCRIPT =
<<'SCRIPT'
/* @applitools/[email protected] */

function __captureDomAndPoll() {
  var captureDomAndPoll = (function () {
  'use strict';

  const styleProps = [
    'background-repeat',
    'background-origin',
    'background-position',
    'background-color',
    'background-image',
    'background-size',
    'border-width',
    'border-color',
    'border-style',
    'color',
    'display',
    'font-size',
    'line-height',
    'margin',
    'opacity',
    'overflow',
    'padding',
    'visibility',
  ];

  const rectProps = ['width', 'height', 'top', 'left'];

  const ignoredTagNames = ['HEAD', 'SCRIPT'];

  var defaultDomProps = {
    styleProps,
    rectProps,
    ignoredTagNames,
  };

  const bgImageRe = /url\((?!['"]?:)['"]?([^'")]*)['"]?\)/;

  function getBackgroundImageUrl(cssText) {
    const match = cssText ? cssText.match(bgImageRe) : undefined;
    return match ? match[1] : match;
  }

  var getBackgroundImageUrl_1 = getBackgroundImageUrl;

  const psetTimeout = t =>
    new Promise(res => {
      setTimeout(res, t);
    });

  async function getImageSizes({bgImages, timeout = 5000, Image = window.Image}) {
    return (await Promise.all(
      Array.from(bgImages).map(url =>
        Promise.race([
          new Promise(resolve => {
            const img = new Image();
            img.onload = () => resolve({url, width: img.naturalWidth, height: img.naturalHeight});
            img.onerror = () => resolve();
            img.src = url;
          }),
          psetTimeout(timeout),
        ]),
      ),
    )).reduce((images, curr) => {
      if (curr) {
        images[curr.url] = {width: curr.width, height: curr.height};
      }
      return images;
    }, {});
  }

  var getImageSizes_1 = getImageSizes;

  function genXpath(el) {
    if (!el.ownerDocument) return ''; // this is the document node

    let xpath = '',
      currEl = el,
      doc = el.ownerDocument,
      frameElement = doc.defaultView.frameElement;
    while (currEl !== doc) {
      xpath = `${currEl.tagName}[${getIndex(currEl)}]/${xpath}`;
      currEl = currEl.parentNode;
    }
    if (frameElement) {
      xpath = `${genXpath(frameElement)},${xpath}`;
    }
    return xpath.replace(/\/$/, '');
  }

  function getIndex(el) {
    return (
      Array.prototype.filter
        .call(el.parentNode.childNodes, node => node.tagName === el.tagName)
        .indexOf(el) + 1
    );
  }

  var genXpath_1 = genXpath;

  function absolutizeUrl(url, absoluteUrl) {
    return new URL(url, absoluteUrl).href;
  }

  var absolutizeUrl_1 = absolutizeUrl;

  function makeGetBundledCssFromCssText({
    parseCss,
    CSSImportRule,
    absolutizeUrl,
    fetchCss,
    unfetchedToken,
  }) {
    return async function getBundledCssFromCssText(cssText, resourceUrl) {
      let unfetchedResources;
      let bundledCss = '';

      try {
        const styleSheet = parseCss(cssText);
        for (const rule of Array.from(styleSheet.cssRules)) {
          if (rule instanceof CSSImportRule) {
            const nestedUrl = absolutizeUrl(rule.href, resourceUrl);
            const nestedResource = await fetchCss(nestedUrl);
            if (nestedResource !== undefined) {
              const {
                bundledCss: nestedCssText,
                unfetchedResources: nestedUnfetchedResources,
              } = await getBundledCssFromCssText(nestedResource, nestedUrl);

              nestedUnfetchedResources && (unfetchedResources = new Set(nestedUnfetchedResources));
              bundledCss = `${nestedCssText}${bundledCss}`;
            } else {
              unfetchedResources = new Set([nestedUrl]);
              bundledCss = `\n${unfetchedToken}${nestedUrl}${unfetchedToken}`;
            }
          }
        }
      } catch (ex) {
        console.log(`error during getBundledCssFromCssText, resourceUrl=${resourceUrl}`, ex);
      }

      bundledCss = `${bundledCss}${getCss(cssText, resourceUrl)}`;

      return {
        bundledCss,
        unfetchedResources,
      };
    };
  }

  function getCss(newText, url) {
    return `\n/** ${url} **/\n${newText}`;
  }

  var getBundledCssFromCssText = makeGetBundledCssFromCssText;

  function parseCss(styleContent) {
    var doc = document.implementation.createHTMLDocument(''),
      styleElement = doc.createElement('style');
    styleElement.textContent = styleContent;
    // the style will only be parsed once it is added to a document
    doc.body.appendChild(styleElement);

    return styleElement.sheet;
  }

  var parseCss_1 = parseCss;

  function makeFetchCss(fetch) {
    return async function fetchCss(url) {
      try {
        const response = await fetch(url, {cache: 'force-cache'});
        if (response.ok) {
          return await response.text();
        }
        console.log('/failed to fetch (status ' + response.status + ') css from: ' + url + '/');
      } catch (err) {
        console.log('/failed to fetch (error ' + err.toString() + ') css from: ' + url + '/');
      }
    };
  }

  var fetchCss = makeFetchCss;

  function makeExtractCssFromNode({fetchCss, absolutizeUrl}) {
    return async function extractCssFromNode(node, baseUrl) {
      let cssText, resourceUrl, isUnfetched;
      if (isStyleElement(node)) {
        cssText = Array.from(node.childNodes)
          .map(node => node.nodeValue)
          .join('');
        resourceUrl = baseUrl;
      } else if (isLinkToStyleSheet(node)) {
        resourceUrl = absolutizeUrl(getHrefAttr(node), baseUrl);
        cssText = await fetchCss(resourceUrl);
        if (cssText === undefined) {
          isUnfetched = true;
        }
      }
      return {cssText, resourceUrl, isUnfetched};
    };
  }

  function isStyleElement(node) {
    return node.nodeName && node.nodeName.toUpperCase() === 'STYLE';
  }

  function getHrefAttr(node) {
    const attr = Array.from(node.attributes).find(attr => attr.name.toLowerCase() === 'href');
    return attr && attr.value;
  }

  function isLinkToStyleSheet(node) {
    return (
      node.nodeName &&
      node.nodeName.toUpperCase() === 'LINK' &&
      node.attributes &&
      Array.from(node.attributes).find(
        attr => attr.name.toLowerCase() === 'rel' && attr.value.toLowerCase() === 'stylesheet',
      )
    );
  }

  var extractCssFromNode = makeExtractCssFromNode;

  function makeCaptureNodeCss({extractCssFromNode, getBundledCssFromCssText, unfetchedToken}) {
    return async function captureNodeCss(node, baseUrl) {
      const {resourceUrl, cssText, isUnfetched} = await extractCssFromNode(node, baseUrl);

      let unfetchedResources;
      let bundledCss = '';
      if (cssText) {
        const {
          bundledCss: nestedCss,
          unfetchedResources: nestedUnfetched,
        } = await getBundledCssFromCssText(cssText, resourceUrl);

        bundledCss += nestedCss;
        unfetchedResources = new Set(nestedUnfetched);
      } else if (isUnfetched) {
        bundledCss += `${unfetchedToken}${resourceUrl}${unfetchedToken}`;
        unfetchedResources = new Set([resourceUrl]);
      }
      return {bundledCss, unfetchedResources};
    };
  }

  var captureNodeCss = makeCaptureNodeCss;

  const NODE_TYPES = {
    ELEMENT: 1,
    TEXT: 3,
  };
  const API_VERSION = '1.0.0';

  async function captureFrame(
    {styleProps, rectProps, ignoredTagNames} = defaultDomProps,
    doc = document,
  ) {
    const start = Date.now();
    const unfetchedResources = new Set();
    const iframeCors = [];
    const iframeToken = '@@@@@';
    const unfetchedToken = '#####';
    const separator = '-----';

    const fetchCss$$1 = fetchCss(fetch);
    const getBundledCssFromCssText$$1 = getBundledCssFromCssText({
      parseCss: parseCss_1,
      CSSImportRule,
      fetchCss: fetchCss$$1,
      absolutizeUrl: absolutizeUrl_1,
      unfetchedToken,
    });
    const extractCssFromNode$$1 = extractCssFromNode({fetchCss: fetchCss$$1, absolutizeUrl: absolutizeUrl_1});
    const captureNodeCss$$1 = captureNodeCss({
      extractCssFromNode: extractCssFromNode$$1,
      getBundledCssFromCssText: getBundledCssFromCssText$$1,
      unfetchedToken,
    });

    // Note: Change the API_VERSION when changing json structure.
    const capturedFrame = await doCaptureFrame(doc);
    capturedFrame.version = API_VERSION;

    const iframePrefix = iframeCors.length ? `${iframeCors.join('\n')}\n` : '';
    const unfetchedPrefix = unfetchedResources.size
      ? `${Array.from(unfetchedResources).join('\n')}\n`
      : '';
    const metaPrefix = JSON.stringify({
      separator,
      cssStartToken: unfetchedToken,
      cssEndToken: unfetchedToken,
      iframeStartToken: `"${iframeToken}`,
      iframeEndToken: `${iframeToken}"`,
    });
    const ret = `${metaPrefix}\n${unfetchedPrefix}${separator}\n${iframePrefix}${separator}\n${JSON.stringify(
    capturedFrame,
  )}`;
    console.log('[captureFrame]', Date.now() - start);
    return ret;

    function filter(x) {
      return !!x;
    }

    function notEmptyObj(obj) {
      return Object.keys(obj).length ? obj : undefined;
    }

    function captureTextNode(node) {
      return {
        tagName: '#text',
        text: node.textContent,
      };
    }

    async function doCaptureFrame(frameDoc) {
      const bgImages = new Set();
      let bundledCss = '';
      const ret = await captureNode(frameDoc.documentElement);
      ret.css = bundledCss;
      ret.images = await getImageSizes_1({bgImages});
      return ret;

      async function captureNode(node) {
        const {bundledCss: nodeCss, unfetchedResources: nodeUnfetched} = await captureNodeCss$$1(
          node,
          frameDoc.location.href,
        );
        bundledCss += nodeCss;
        if (nodeUnfetched) for (const elem of nodeUnfetched) unfetchedResources.add(elem);

        switch (node.nodeType) {
          case NODE_TYPES.TEXT: {
            return captureTextNode(node);
          }
          case NODE_TYPES.ELEMENT: {
            const tagName = node.tagName.toUpperCase();
            if (tagName === 'IFRAME') {
              return await iframeToJSON(node);
            } else {
              return await await elementToJSON(node);
            }
          }
          default: {
            return null;
          }
        }
      }

      async function elementToJSON(el) {
        const childNodes = (await Promise.all(
          Array.prototype.map.call(el.childNodes, captureNode),
        )).filter(filter);

        const tagName = el.tagName.toUpperCase();
        if (ignoredTagNames.indexOf(tagName) > -1) return null;

        const computedStyle = window.getComputedStyle(el);
        const boundingClientRect = el.getBoundingClientRect();

        const style = {};
        for (const p of styleProps) style[p] = computedStyle.getPropertyValue(p);

        const rect = {};
        for (const p of rectProps) rect[p] = boundingClientRect[p];

        const attributes = Array.from(el.attributes)
          .map(a => ({key: a.name, value: a.value}))
          .reduce((obj, attr) => {
            obj[attr.key] = attr.value;
            return obj;
          }, {});

        const bgImage = getBackgroundImageUrl_1(computedStyle.getPropertyValue('background-image'));
        if (bgImage) {
          bgImages.add(bgImage);
        }

        return {
          tagName,
          style: notEmptyObj(style),
          rect: notEmptyObj(rect),
          attributes: notEmptyObj(attributes),
          childNodes,
        };
      }

      async function iframeToJSON(el) {
        const obj = await elementToJSON(el);
        let doc;
        try {
          doc = el.contentDocument;
        } catch (ex) {
          markFrameAsCors();
          return obj;
        }
        try {
          if (doc) {
            obj.childNodes = [await doCaptureFrame(el.contentDocument)];
          } else {
            markFrameAsCors();
          }
        } catch (ex) {
          console.log('error in iframeToJSON', ex);
        }
        return obj;

        function markFrameAsCors() {
          const xpath = genXpath_1(el);
          iframeCors.push(xpath);
          obj.childNodes = [`${iframeToken}${xpath}${iframeToken}`];
        }
      }
    }
  }

  var captureFrame_1 = captureFrame;

  const EYES_NAME_SPACE = '__EYES__APPLITOOLS__';

  function captureFrameAndPoll(...args) {
    if (!window[EYES_NAME_SPACE]) {
      window[EYES_NAME_SPACE] = {};
    }
    if (!window[EYES_NAME_SPACE].captureDomResult) {
      window[EYES_NAME_SPACE].captureDomResult = {
        status: 'WIP',
        value: null,
        error: null,
      };
      captureFrame_1(...args)
        .then(r => ((resultObject.status = 'SUCCESS'), (resultObject.value = r)))
        .catch(e => ((resultObject.status = 'ERROR'), (resultObject.error = e.message)));
    }

    const resultObject = window[EYES_NAME_SPACE].captureDomResult;
    if (resultObject.status === 'SUCCESS') {
      window[EYES_NAME_SPACE].captureDomResult = null;
    }

    return JSON.stringify(resultObject);
  }

  var captureFrameAndPoll_1 = captureFrameAndPoll;

  return captureFrameAndPoll_1;

}());

  return captureDomAndPoll.apply(this, arguments);
}
SCRIPT

Instance Method Summary collapse

Instance Method Details

#base_url(url) ⇒ Object



140
141
142
143
144
145
# File 'lib/applitools/selenium/dom_capture/dom_capture.rb', line 140

def base_url(url)
  uri = URI.parse(url)
  uri.query = uri.fragment = nil;
  uri.path = ''
  uri
end

#fetch_css_files(missing_css_list, server_connector) ⇒ Object



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/applitools/selenium/dom_capture/dom_capture.rb', line 68

def fetch_css_files(missing_css_list, server_connector)
  result = {}
  missing_css_list.each do |url|
    next if url.empty?
    next if /^blob:/ =~ url

    begin
      missing_css_response = server_connector.download_resource(url)
      response_headers = missing_css_response.headers
      raise Applitools::EyesError, "Wrong response header: #{response_headers['content-type']}" unless
          %r{^text/css.*}i =~ response_headers['content-type']

      css = missing_css_response.body

      found_and_missing_css = Applitools::Selenium::CssParser::FindEmbeddedResources.new(css).imported_css.map do |found_url|
        base_url(url).merge(found_url).to_s
      end
      fetch_css_files(found_and_missing_css, server_connector).each do |_k, v|
        css += v
      end

      result[url] = Oj.dump("\n/** #{url} **/\n" + css).gsub(/^\"|\"$/, '')
    rescue StandardError
      result[url] = ''
    end
  end
  result
end

#full_window_dom(driver, server_connector, logger, position_provider = nil) ⇒ Object



6
7
8
9
10
11
# File 'lib/applitools/selenium/dom_capture/dom_capture.rb', line 6

def full_window_dom(driver, server_connector, logger, position_provider = nil)
  return get_dom(driver, server_connector, logger) unless position_provider
  scroll_top_and_return_back(position_provider) do
    get_dom(driver, server_connector, logger)
  end
end

#get_dom(driver, server_connector, logger) ⇒ Object



13
14
15
16
17
18
19
20
21
22
23
# File 'lib/applitools/selenium/dom_capture/dom_capture.rb', line 13

def get_dom(driver, server_connector, logger)
  original_frame_chain = driver.frame_chain
  dom = get_frame_dom(driver, server_connector, logger)
  unless original_frame_chain.empty?
    driver.switch_to.default_content
    driver.switch_to.frames(frame_chain: original_frame_chain)
  end
  # CSS processing

  dom
end

#get_frame_dom(driver, server_connector, logger) ⇒ Object



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/applitools/selenium/dom_capture/dom_capture.rb', line 25

def get_frame_dom(driver, server_connector, logger)
  logger.info 'Trying to get DOM from driver'
  start_time = Time.now
  script_response = nil
  loop do
    result_as_string = driver.execute_script(CAPTURE_FRAME_SCRIPT + ' return __captureDomAndPoll();')
    script_response = Oj.load(result_as_string)
    status = script_response['status']
    break if status == 'SUCCESS'
    raise Applitools::EyesError, 'DOM extraction timeout!' if Time.now - start_time > DOM_EXTRACTION_TIMEOUT
    raise Applitools::EyesError, "DOM extraction error: #{script_response['error']}" if script_response['error']
    sleep(0.2)
  end
  response_lines = script_response['value'].split /\r?\n/
  separators = Oj.load(response_lines.shift)
  missing_css_list = []
  missing_frame_list = []
  data = []

  blocks = DomParts.new(missing_css_list, missing_frame_list, data)
  collector = blocks.collectors.next
  response_lines.each do |line|
    if line == separators['separator']
      collector = blocks.collectors.next
    else
      collector << line
    end
  end
  logger.info "Missing CSS: #{missing_css_list.count}"
  logger.info "Missing frames: #{missing_frame_list.count}"
  #fetch_css_files(missing_css_list)

  frame_data = recurse_frames(driver, server_connector, logger, missing_frame_list)
  result = replace(separators['iframeStartToken'], separators['iframeEndToken'], data.first, frame_data)
  css_data = fetch_css_files(missing_css_list, server_connector)
  replace(separators['cssStartToken'], separators['cssEndToken'], result, css_data)
rescue StandardError
  logger.error(e.class)
  logger.error(e.message)
  logger.error(e)
  return ''
end

#recurse_frames(driver, server_connector, logger, missing_frame_list) ⇒ Object



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/applitools/selenium/dom_capture/dom_capture.rb', line 97

def recurse_frames(driver, server_connector, logger, missing_frame_list)
  return if missing_frame_list.empty?
  frame_data = {}
  frame_chain = driver.frame_chain
  origin_location = driver.execute_script('return document.location.href')
  missing_frame_list.each do |missing_frame_line|
    logger.info "Switching to frame line: #{missing_frame_line}"
    missing_frame_line.split(/,/).each do |xpath|
      logger.info "switching to specific frame: #{xpath}"
      frame_element = driver.find_element(:xpath, xpath)
      frame_src = frame_element.attribute('src')
      driver.switch_to.frame(frame_element)
      logger.info "Switched to frame ( #{xpath} ) with src( #{frame_src} )"
    end
    location_after_switch = driver.execute_script('return document.location.href')

    if origin_location == location_after_switch
      logger.info "Switch to frame (#{missing_frame_line}) failed"
      frame_data[missing_frame_line] = ''
    else
      result = get_frame_dom(driver, server_connector, logger)
      frame_data[missing_frame_line] = result
    end
  end
  driver.switch_to.default_content
  driver.switch_to.frames(frame_chain: frame_chain)
  frame_data
end

#replace(open_token, close_token, input, replacements) ⇒ Object



135
136
137
138
# File 'lib/applitools/selenium/dom_capture/dom_capture.rb', line 135

def replace(open_token, close_token, input, replacements)
  pattern = /#{open_token}(?<key>.+?)#{close_token}/
  input.gsub(pattern) { |_m| replacements[Regexp.last_match(1)] }
end

#scroll_top_and_return_back(position_provider) ⇒ Object



126
127
128
129
130
131
132
133
# File 'lib/applitools/selenium/dom_capture/dom_capture.rb', line 126

def scroll_top_and_return_back(position_provider)
  original_position = position_provider.current_position
  return yield if block_given? && original_position.nil?
  position_provider.scroll_to Applitools::Location.new(0, 0)
  result = yield if block_given?
  position_provider.scroll_to original_position
  result
end