Source code
Revision control
Copy as Markdown
Other Tools
Test Info:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
import base64
import hashlib
import imghdr
import struct
import tempfile
import unittest
import six
from six.moves.urllib.parse import quote
import mozinfo
from marionette_driver import By
from marionette_driver.errors import NoSuchWindowException
from marionette_harness import (
MarionetteTestCase,
skip,
WindowManagerMixin,
)
def decodebytes(s):
if six.PY3:
return base64.decodebytes(six.ensure_binary(s))
return base64.decodestring(s)
def inline(doc, mime="text/html;charset=utf-8"):
return "data:{0},{1}".format(mime, quote(doc))
box = inline(
"<body><div id='box'><p id='green' style='width: 50px; height: 50px; "
"background: silver;'></p></div></body>"
)
input = inline("<body><input id='text-input'></input></body>")
long = inline("<body style='height: 300vh'><p style='margin-top: 100vh'>foo</p></body>")
short = inline("<body style='height: 10vh'></body>")
svg = inline(
"""
<rect height="20" width="20"/>
</svg>""",
mime="image/svg+xml",
)
class ScreenCaptureTestCase(MarionetteTestCase):
def setUp(self):
super(ScreenCaptureTestCase, self).setUp()
self.maxDiff = None
self._device_pixel_ratio = None
# Ensure that each screenshot test runs on a blank page to avoid left
# over elements or focus which could interfer with taking screenshots
self.marionette.navigate("about:blank")
@property
def device_pixel_ratio(self):
if self._device_pixel_ratio is None:
self._device_pixel_ratio = self.marionette.execute_script(
"""
return window.devicePixelRatio
"""
)
return self._device_pixel_ratio
@property
def document_element(self):
return self.marionette.find_element(By.CSS_SELECTOR, ":root")
@property
def page_y_offset(self):
return self.marionette.execute_script("return window.pageYOffset")
@property
def viewport_dimensions(self):
return self.marionette.execute_script(
"return [window.innerWidth, window.innerHeight];"
)
def assert_png(self, screenshot):
"""Test that screenshot is a Base64 encoded PNG file."""
if six.PY3 and not isinstance(screenshot, bytes):
screenshot = bytes(screenshot, encoding="utf-8")
image = decodebytes(screenshot)
self.assertEqual(imghdr.what("", image), "png")
def assert_formats(self, element=None):
if element is None:
element = self.document_element
screenshot_default = self.marionette.screenshot(element=element)
if six.PY3 and not isinstance(screenshot_default, bytes):
screenshot_default = bytes(screenshot_default, encoding="utf-8")
screenshot_image = self.marionette.screenshot(element=element, format="base64")
if six.PY3 and not isinstance(screenshot_image, bytes):
screenshot_image = bytes(screenshot_image, encoding="utf-8")
binary1 = self.marionette.screenshot(element=element, format="binary")
binary2 = self.marionette.screenshot(element=element, format="binary")
hash1 = self.marionette.screenshot(element=element, format="hash")
hash2 = self.marionette.screenshot(element=element, format="hash")
# Valid data should have been returned
self.assert_png(screenshot_image)
self.assertEqual(imghdr.what("", binary1), "png")
self.assertEqual(screenshot_image, base64.b64encode(binary1))
self.assertEqual(hash1, hashlib.sha256(screenshot_image).hexdigest())
# Different formats produce different data
self.assertNotEqual(screenshot_image, binary1)
self.assertNotEqual(screenshot_image, hash1)
self.assertNotEqual(binary1, hash1)
# A second capture should be identical
self.assertEqual(screenshot_image, screenshot_default)
self.assertEqual(binary1, binary2)
self.assertEqual(hash1, hash2)
def get_element_dimensions(self, element):
rect = element.rect
return rect["width"], rect["height"]
def get_image_dimensions(self, screenshot):
if six.PY3 and not isinstance(screenshot, bytes):
screenshot = bytes(screenshot, encoding="utf-8")
self.assert_png(screenshot)
image = decodebytes(screenshot)
width, height = struct.unpack(">LL", image[16:24])
return int(width), int(height)
def scale(self, rect):
return (
int(rect[0] * self.device_pixel_ratio),
int(rect[1] * self.device_pixel_ratio),
)
class TestScreenCaptureChrome(WindowManagerMixin, ScreenCaptureTestCase):
def setUp(self):
super(TestScreenCaptureChrome, self).setUp()
self.marionette.set_context("chrome")
def tearDown(self):
self.close_all_windows()
super(TestScreenCaptureChrome, self).tearDown()
@property
def window_dimensions(self):
return tuple(
self.marionette.execute_script(
"""
let el = document.documentElement;
let rect = el.getBoundingClientRect();
return [rect.width, rect.height];
"""
)
)
def open_dialog(self):
return self.open_chrome_window(
"chrome://remote/content/marionette/test_dialog.xhtml"
)
def test_capture_different_context(self):
"""Check that screenshots in content and chrome are different."""
with self.marionette.using_context("content"):
screenshot_content = self.marionette.screenshot()
screenshot_chrome = self.marionette.screenshot()
self.assertNotEqual(screenshot_content, screenshot_chrome)
def test_capture_element(self):
dialog = self.open_dialog()
self.marionette.switch_to_window(dialog)
# Ensure we only capture the element
el = self.marionette.find_element(By.ID, "test-list")
screenshot_element = self.marionette.screenshot(element=el)
self.assertEqual(
self.scale(self.get_element_dimensions(el)),
self.get_image_dimensions(screenshot_element),
)
# Ensure we do not capture the full window
screenshot_dialog = self.marionette.screenshot()
self.assertNotEqual(screenshot_dialog, screenshot_element)
self.marionette.close_chrome_window()
self.marionette.switch_to_window(self.start_window)
def test_capture_full_area(self):
dialog = self.open_dialog()
self.marionette.switch_to_window(dialog)
root_dimensions = self.scale(self.get_element_dimensions(self.document_element))
# self.marionette.set_window_rect(width=100, height=100)
# A full capture is not the outer dimensions of the window,
# but instead the bounding box of the window's root node (documentElement).
screenshot_full = self.marionette.screenshot()
screenshot_root = self.marionette.screenshot(element=self.document_element)
self.marionette.close_chrome_window()
self.marionette.switch_to_window(self.start_window)
self.assert_png(screenshot_full)
self.assert_png(screenshot_root)
self.assertEqual(root_dimensions, self.get_image_dimensions(screenshot_full))
self.assertEqual(screenshot_root, screenshot_full)
def test_capture_window_already_closed(self):
dialog = self.open_dialog()
self.marionette.switch_to_window(dialog)
self.marionette.close_chrome_window()
self.assertRaises(NoSuchWindowException, self.marionette.screenshot)
self.marionette.switch_to_window(self.start_window)
def test_formats(self):
dialog = self.open_dialog()
self.marionette.switch_to_window(dialog)
self.assert_formats()
self.marionette.close_chrome_window()
self.marionette.switch_to_window(self.start_window)
def test_format_unknown(self):
with self.assertRaises(ValueError):
self.marionette.screenshot(format="cheese")
class TestScreenCaptureContent(WindowManagerMixin, ScreenCaptureTestCase):
def setUp(self):
super(TestScreenCaptureContent, self).setUp()
self.marionette.set_context("content")
def tearDown(self):
self.close_all_tabs()
super(TestScreenCaptureContent, self).tearDown()
@property
def scroll_dimensions(self):
return tuple(
self.marionette.execute_script(
"""
return [
document.documentElement.scrollWidth,
document.documentElement.scrollHeight
];
"""
)
)
def test_capture_tab_already_closed(self):
new_tab = self.open_tab()
self.marionette.switch_to_window(new_tab)
self.marionette.close()
self.assertRaises(NoSuchWindowException, self.marionette.screenshot)
self.marionette.switch_to_window(self.start_tab)
def test_capture_vertical_bounds(self):
self.marionette.navigate(inline("<body style='margin-top: 32768px'>foo"))
screenshot = self.marionette.screenshot()
self.assert_png(screenshot)
def test_capture_horizontal_bounds(self):
self.marionette.navigate(inline("<body style='margin-left: 32768px'>foo"))
screenshot = self.marionette.screenshot()
self.assert_png(screenshot)
def test_capture_area_bounds(self):
self.marionette.navigate(
inline("<body style='margin-right: 21747px; margin-top: 21747px'>foo")
)
screenshot = self.marionette.screenshot()
self.assert_png(screenshot)
def test_capture_element(self):
self.marionette.navigate(box)
el = self.marionette.find_element(By.TAG_NAME, "div")
screenshot = self.marionette.screenshot(element=el)
self.assert_png(screenshot)
self.assertEqual(
self.scale(self.get_element_dimensions(el)),
self.get_image_dimensions(screenshot),
)
def test_capture_element_scrolled_into_view(self):
self.marionette.navigate(long)
el = self.marionette.find_element(By.TAG_NAME, "p")
screenshot = self.marionette.screenshot(element=el)
self.assert_png(screenshot)
self.assertEqual(
self.scale(self.get_element_dimensions(el)),
self.get_image_dimensions(screenshot),
)
self.assertGreater(self.page_y_offset, 0)
def test_capture_full_html_document_element(self):
self.marionette.navigate(long)
screenshot = self.marionette.screenshot()
self.assert_png(screenshot)
self.assertEqual(
self.scale(self.scroll_dimensions), self.get_image_dimensions(screenshot)
)
def test_capture_full_svg_document_element(self):
self.marionette.navigate(svg)
screenshot = self.marionette.screenshot()
self.assert_png(screenshot)
self.assertEqual(
self.scale(self.scroll_dimensions), self.get_image_dimensions(screenshot)
)
def test_capture_viewport(self):
url = self.marionette.absolute_url("clicks.html")
self.marionette.navigate(short)
self.marionette.navigate(url)
screenshot = self.marionette.screenshot(full=False)
self.assert_png(screenshot)
self.assertEqual(
self.scale(self.viewport_dimensions), self.get_image_dimensions(screenshot)
)
def test_capture_viewport_after_scroll(self):
self.marionette.navigate(long)
before = self.marionette.screenshot()
el = self.marionette.find_element(By.TAG_NAME, "p")
self.marionette.execute_script(
"arguments[0].scrollIntoView()", script_args=[el]
)
after = self.marionette.screenshot(full=False)
self.assertNotEqual(before, after)
self.assertGreater(self.page_y_offset, 0)
def test_formats(self):
self.marionette.navigate(box)
# Use a smaller region to speed up the test
element = self.marionette.find_element(By.TAG_NAME, "div")
self.assert_formats(element=element)
def test_format_unknown(self):
with self.assertRaises(ValueError):
self.marionette.screenshot(format="cheese")
def test_save_screenshot(self):
expected = self.marionette.screenshot(format="binary")
with tempfile.TemporaryFile("w+b") as fh:
self.marionette.save_screenshot(fh)
fh.flush()
fh.seek(0)
content = fh.read()
self.assertEqual(expected, content)
def test_scroll_default(self):
self.marionette.navigate(long)
before = self.page_y_offset
el = self.marionette.find_element(By.TAG_NAME, "p")
self.marionette.screenshot(element=el, format="hash")
self.assertNotEqual(before, self.page_y_offset)
def test_scroll(self):
self.marionette.navigate(long)
before = self.page_y_offset
el = self.marionette.find_element(By.TAG_NAME, "p")
self.marionette.screenshot(element=el, format="hash", scroll=True)
self.assertNotEqual(before, self.page_y_offset)
def test_scroll_off(self):
self.marionette.navigate(long)
el = self.marionette.find_element(By.TAG_NAME, "p")
before = self.page_y_offset
self.marionette.screenshot(element=el, format="hash", scroll=False)
self.assertEqual(before, self.page_y_offset)
def test_scroll_no_element(self):
self.marionette.navigate(long)
before = self.page_y_offset
self.marionette.screenshot(format="hash", scroll=True)
self.assertEqual(before, self.page_y_offset)