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,
    getCssFromCache,
    unfetchedToken,
  }) {
    return function getBundledCssFromCssText(cssText, styleBaseUrl) {
      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, styleBaseUrl);
            const nestedResource = getCssFromCache(nestedUrl);
            if (nestedResource !== undefined) {
              const {
                bundledCss: nestedCssText,
                unfetchedResources: nestedUnfetchedResources,
              } = 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, styleBaseUrl=${styleBaseUrl}`, ex);
      }

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

      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;

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

  var isLinkToStyleSheet = 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',
      )
    );
  };

  function isDataUrl(url) {
    return url && url.startsWith('data:');
  }

  var isDataUrl_1 = isDataUrl;

  function makeExtractCssFromNode({getCssFromCache, absolutizeUrl}) {
    return function extractCssFromNode(node, baseUrl) {
      let cssText, styleBaseUrl, isUnfetched;
      if (isStyleElement(node)) {
        cssText = Array.from(node.childNodes)
          .map(node => node.nodeValue)
          .join('');
        styleBaseUrl = baseUrl;
      } else if (isLinkToStyleSheet(node)) {
        const href = getHrefAttr(node);
        if (!isDataUrl_1(href)) {
          styleBaseUrl = absolutizeUrl(href, baseUrl);
          cssText = getCssFromCache(styleBaseUrl);
        } else {
          styleBaseUrl = baseUrl;
          cssText = href.match(/,(.+)/)[1];
        }
        isUnfetched = cssText === undefined;
      }
      return {cssText, styleBaseUrl, isUnfetched};
    };
  }

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

  var extractCssFromNode = makeExtractCssFromNode;

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

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

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

  var captureNodeCss = makeCaptureNodeCss;

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

  var nodeTypes = {NODE_TYPES};

  const {NODE_TYPES: NODE_TYPES$1} = nodeTypes;





  function makePrefetchAllCss(fetchCss) {
    return async function prefetchAllCss(doc = document) {
      const cssMap = {};
      const start = Date.now();
      const promises = [];
      doFetchAllCssFromFrame(doc, cssMap, promises);
      await Promise.all(promises);
      console.log('[prefetchAllCss]', Date.now() - start);

      return function fetchCssSync(url) {
        return cssMap[url];
      };

      async function fetchNodeCss(node, baseUrl, cssMap) {
        let cssText, resourceUrl;
        if (isLinkToStyleSheet(node)) {
          resourceUrl = absolutizeUrl_1(getHrefAttr(node), baseUrl);
          cssText = await fetchCss(resourceUrl);
          if (cssText !== undefined) {
            cssMap[resourceUrl] = cssText;
          }
        }
        if (cssText) {
          await fetchBundledCss(cssText, resourceUrl, cssMap);
        }
      }

      async function fetchBundledCss(cssText, resourceUrl, cssMap) {
        try {
          const styleSheet = parseCss_1(cssText);
          const promises = [];
          for (const rule of Array.from(styleSheet.cssRules)) {
            if (rule instanceof CSSImportRule) {
              promises.push(
                (async () => {
                  const nestedUrl = absolutizeUrl_1(rule.href, resourceUrl);
                  const cssText = await fetchCss(nestedUrl);
                  cssMap[nestedUrl] = cssText;
                  if (cssText !== undefined) {
                    await fetchBundledCss(cssText, nestedUrl, cssMap);
                  }
                })(),
              );
            }
          }
          await Promise.all(promises);
        } catch (ex) {
          console.log(`error during fetchBundledCss, resourceUrl=${resourceUrl}`, ex);
        }
      }

      function doFetchAllCssFromFrame(frameDoc, cssMap, promises) {
        fetchAllCssFromNode(frameDoc.documentElement);

        function fetchAllCssFromNode(node) {
          promises.push(fetchNodeCss(node, frameDoc.location.href, cssMap));

          switch (node.nodeType) {
            case NODE_TYPES$1.ELEMENT: {
              const tagName = node.tagName.toUpperCase();
              if (tagName === 'IFRAME') {
                return fetchAllCssFromIframe(node);
              } else {
                return fetchAllCssFromElement(node);
              }
            }
          }
        }

        async function fetchAllCssFromElement(el) {
          Array.prototype.map.call(el.childNodes, fetchAllCssFromNode);
        }

        async function fetchAllCssFromIframe(el) {
          fetchAllCssFromElement(el);
          try {
            doFetchAllCssFromFrame(el.contentDocument, cssMap, promises);
          } catch (ex) {
            console.log(ex);
          }
        }
      }
    };
  }

  var prefetchAllCss = makePrefetchAllCss;

  const {NODE_TYPES: NODE_TYPES$2} = nodeTypes;

  const API_VERSION = '1.1.0';

  async function captureFrame(
    {styleProps, rectProps, ignoredTagNames} = defaultDomProps,
    doc = document,
    addStats = false,
  ) {
    const performance = {total: {}, prefetchCss: {}, doCaptureFrame: {}, waitForImages: {}};
    function startTime(obj) {
      obj.startTime = Date.now();
    }
    function endTime(obj) {
      obj.endTime = Date.now();
      obj.ellapsedTime = obj.endTime - obj.startTime;
    }
    const promises = [];
    startTime(performance.total);
    const unfetchedResources = new Set();
    const iframeCors = [];
    const iframeToken = '@@@@@';
    const unfetchedToken = '#####';
    const separator = '-----';

    startTime(performance.prefetchCss);
    const prefetchAllCss$$1 = prefetchAllCss(fetchCss(fetch));
    const getCssFromCache = await prefetchAllCss$$1(doc);
    endTime(performance.prefetchCss);

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

    startTime(performance.doCaptureFrame);
    const capturedFrame = doCaptureFrame(doc);
    endTime(performance.doCaptureFrame);

    startTime(performance.waitForImages);
    await Promise.all(promises);
    endTime(performance.waitForImages);

    // Note: Change the API_VERSION when changing json structure.
    capturedFrame.version = API_VERSION;
    capturedFrame.scriptVersion = '7.1.3';

    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}"`,
    });

    endTime(performance.total);

    function stats() {
      if (!addStats) {
        return '';
      }
      return `\n${separator}\n${JSON.stringify(performance)}`;
    }

    const ret = `${metaPrefix}\n${unfetchedPrefix}${separator}\n${iframePrefix}${separator}\n${JSON.stringify(
    capturedFrame,
  )}${stats()}`;
    console.log('[captureFrame]', JSON.stringify(performance));
    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,
      };
    }

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

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

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

      function elementToJSON(el) {
        const childNodes = 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);
        if (!style['border-width']) {
          style['border-width'] = `${computedStyle.getPropertyValue(
          'border-top-width',
        )} ${computedStyle.getPropertyValue('border-right-width')} ${computedStyle.getPropertyValue(
          'border-bottom-width',
        )} ${computedStyle.getPropertyValue('border-left-width')}`;
        }

        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,
        };
      }

      function iframeToJSON(el) {
        const obj = elementToJSON(el);
        let doc;
        try {
          doc = el.contentDocument;
        } catch (ex) {
          markFrameAsCors();
          return obj;
        }
        try {
          if (doc) {
            obj.childNodes = [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



147
148
149
150
151
152
# File 'lib/applitools/selenium/dom_capture/dom_capture.rb', line 147

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



67
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
96
97
98
# File 'lib/applitools/selenium/dom_capture/dom_capture.rb', line 67

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,
        Applitools::Utils::EyesSeleniumUtils.user_agent(driver)
      )
      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



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

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



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

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



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
# File 'lib/applitools/selenium/dom_capture/dom_capture.rb', line 26

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}"

  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 => e
  logger.error(e.class)
  logger.error(e.message)
  return ''
end

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



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
125
126
127
128
129
130
131
# File 'lib/applitools/selenium/dom_capture/dom_capture.rb', line 100

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}"
      begin
        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} )"
      ensure
        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

        driver.switch_to.default_content
        driver.switch_to.frames(frame_chain: frame_chain)
      end
    end
  end
  frame_data
end

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



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

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



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

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