Commit 700aad23 authored by craigdh@chromium.org's avatar craigdh@chromium.org

[I-Spy] Add support for rebaselining expectations from the web UI.

BUG=318865
TEST=unittests included
NOTRY=True

Committed: https://src.chromium.org/viewvc/chrome?view=rev&revision=240226

Review URL: https://codereview.chromium.org/106523003

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@243140 0039d316-1c4b-4281-b951-d872f2087c98
parent 5c521394
#!/usr/bin/env python
#
# Copyright 2013 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import json
import unittest
from PIL import Image
import chrome_utils
from ..common import mock_cloud_bucket
class ChromeUtilsTest(unittest.TestCase):
"""Unittest for ChromeUtils."""
def setUp(self):
self.cloud_bucket = mock_cloud_bucket.MockCloudBucket()
self.white_utils = chrome_utils.ChromeUtils(
self.cloud_bucket, 'versions.json',
lambda: Image.new('RGBA', (10, 10), (255, 255, 255, 255)))
self.black_utils = chrome_utils.ChromeUtils(
self.cloud_bucket, 'versions.json',
lambda: Image.new('RGBA', (10, 10), (0, 0, 0, 255)))
def testGenerateExpectationsRunComparison(self):
self.white_utils.GenerateExpectation('device', 'test', '1.1.1.1')
self.white_utils.UpdateExpectationVersion('1.1.1.1')
self.white_utils.PerformComparison('test1', 'device', 'test', '1.1.1.1')
expect_name = self.white_utils._CreateExpectationName(
'device', 'test', '1.1.1.1')
self.assertFalse(self.white_utils._ispy.FailureExists('test1', expect_name))
self.black_utils.PerformComparison('test2', 'device', 'test', '1.1.1.1')
self.assertTrue(self.white_utils._ispy.FailureExists('test2', expect_name))
def testUpdateExpectationVersion(self):
self.white_utils.UpdateExpectationVersion('1.0.0.0')
self.white_utils.UpdateExpectationVersion('1.0.4.0')
self.white_utils.UpdateExpectationVersion('2.1.5.0')
self.white_utils.UpdateExpectationVersion('1.1.5.0')
self.white_utils.UpdateExpectationVersion('0.0.0.0')
self.white_utils.UpdateExpectationVersion('1.1.5.0')
self.white_utils.UpdateExpectationVersion('0.0.0.1')
versions = json.loads(self.cloud_bucket.DownloadFile('versions.json'))
self.assertEqual(versions,
['2.1.5.0', '1.1.5.0', '1.0.4.0', '1.0.0.0', '0.0.0.1', '0.0.0.0'])
if __name__ == '__main__':
unittest.main()
# Copyright 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
def GetScriptToWaitForUnchangingDOM():
"""Gets Javascript that waits until the DOM is stable for 5 seconds.
Times out if the DOM is not stable within 30 seconds.
Returns:
Javascript as a string.
"""
return """
var target = document.body;
var callback = arguments[arguments.length - 1]
var timeout_id = setTimeout(function() {
callback()
}, 5000);
var observer = new MutationObserver(function(mutations) {
clearTimeout(timeout_id);
timeout_id = setTimeout(function() {
callback();
}, 5000);
}).observe(target, {attributes: true, childList: true,
characterData: true, subtree: true});
"""
# Copyright 2013 The Chromium Authors. All rights reserved. # Copyright 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be # Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file. # found in the LICENSE file.
import json import json
import logging import logging
import os import os
import time
from distutils.version import LooseVersion from distutils.version import LooseVersion
from PIL import Image from PIL import Image
from ..common import cloud_bucket import cloud_bucket
from ..common import ispy_utils import ispy_utils
class ChromeUtils(object): class ChromeUtils(object):
"""A utility for using ISpy with Chrome.""" """A utility for using ISpy with Chrome."""
def __init__(self, cloud_bucket, version_file, screenshot_func): def __init__(self, cloud_bucket):
"""Initializes the utility class. """Initializes the utility class.
Args: Args:
cloud_bucket: a BaseCloudBucket in which to the version file, cloud_bucket: a BaseCloudBucket in which to the version file,
expectations and results are to be stored. expectations and results are to be stored.
version_file: path to the version file in the cloud bucket. The version
file contains a json list of ordered Chrome versions for which
expectations exist.
screenshot_func: a function that returns a PIL.Image.
""" """
self._cloud_bucket = cloud_bucket self._cloud_bucket = cloud_bucket
self._version_file = version_file
self._screenshot_func = screenshot_func
self._ispy = ispy_utils.ISpyUtils(self._cloud_bucket) self._ispy = ispy_utils.ISpyUtils(self._cloud_bucket)
with open( self._rebaselineable_cache = {}
os.path.join(os.path.dirname(__file__), 'wait_on_ajax.js'), 'r') as f:
self._wait_for_unchanging_dom_script = f.read()
def UpdateExpectationVersion(self, chrome_version): def UpdateExpectationVersion(self, chrome_version, version_file):
"""Updates the most recent expectation version to the Chrome version. """Updates the most recent expectation version to the Chrome version.
Should be called after generating a new set of expectations. Should be called after generating a new set of expectations.
Args: Args:
chrome_version: the chrome version as a string of the form "31.0.123.4". chrome_version: the chrome version as a string of the form "31.0.123.4".
version_file: path to the version file in the cloud bucket. The version
file contains a json list of ordered Chrome versions for which
expectations exist.
""" """
insert_pos = 0 insert_pos = 0
expectation_versions = [] expectation_versions = []
try: try:
expectation_versions = self._GetExpectationVersionList() expectation_versions = self._GetExpectationVersionList(version_file)
if expectation_versions: if expectation_versions:
try: try:
version = self._GetExpectationVersion( version = self._GetExpectationVersion(
...@@ -61,7 +55,7 @@ class ChromeUtils(object): ...@@ -61,7 +55,7 @@ class ChromeUtils(object):
expectation_versions.insert(insert_pos, chrome_version) expectation_versions.insert(insert_pos, chrome_version)
logging.info('Updating expectation version...') logging.info('Updating expectation version...')
self._cloud_bucket.UploadFile( self._cloud_bucket.UploadFile(
self._version_file, json.dumps(expectation_versions), version_file, json.dumps(expectation_versions),
'application/json') 'application/json')
def _GetExpectationVersion(self, chrome_version, expectation_versions): def _GetExpectationVersion(self, chrome_version, expectation_versions):
...@@ -81,12 +75,21 @@ class ChromeUtils(object): ...@@ -81,12 +75,21 @@ class ChromeUtils(object):
return version return version
raise Exception('No expectation exists for Chrome %s' % chrome_version) raise Exception('No expectation exists for Chrome %s' % chrome_version)
def _GetExpectationVersionList(self): def _GetExpectationVersionList(self, version_file):
"""Gets the list of expectation versions from google storage.""" """Gets the list of expectation versions from google storage.
return json.loads(self._cloud_bucket.DownloadFile(self._version_file))
Args:
version_file: path to the version file in the cloud bucket. The version
file contains a json list of ordered Chrome versions for which
expectations exist.
Returns:
Ordered list of Chrome versions.
"""
return json.loads(self._cloud_bucket.DownloadFile(version_file))
def _GetExpectationNameWithVersion(self, device_type, expectation, def _GetExpectationNameWithVersion(self, device_type, expectation,
chrome_version): chrome_version, version_file):
"""Get the expectation to be used with the current Chrome version. """Get the expectation to be used with the current Chrome version.
Args: Args:
...@@ -98,7 +101,7 @@ class ChromeUtils(object): ...@@ -98,7 +101,7 @@ class ChromeUtils(object):
Version as an integer. Version as an integer.
""" """
version = self._GetExpectationVersion( version = self._GetExpectationVersion(
chrome_version, self._GetExpectationVersionList()) chrome_version, self._GetExpectationVersionList(version_file))
return self._CreateExpectationName(device_type, expectation, version) return self._CreateExpectationName(device_type, expectation, version)
def _CreateExpectationName(self, device_type, expectation, version): def _CreateExpectationName(self, device_type, expectation, version):
...@@ -114,47 +117,110 @@ class ChromeUtils(object): ...@@ -114,47 +117,110 @@ class ChromeUtils(object):
""" """
return '%s:%s(%s)' % (device_type, expectation, version) return '%s:%s(%s)' % (device_type, expectation, version)
def GenerateExpectation(self, device_type, expectation, chrome_version): def GenerateExpectation(self, device_type, expectation, chrome_version,
"""Take screenshots and store as an expectation in I-Spy. version_file, screenshots):
"""Create an expectation for I-Spy.
Args: Args:
device_type: string identifier for the device type. device_type: string identifier for the device type.
expectation: name for the expectation to generate. expectation: name for the expectation to generate.
chrome_version: the chrome version as a string of the form "31.0.123.4". chrome_version: the chrome version as a string of the form "31.0.123.4".
screenshots: a list of similar PIL.Images.
""" """
# https://code.google.com/p/chromedriver/issues/detail?id=463 # https://code.google.com/p/chromedriver/issues/detail?id=463
time.sleep(1)
expectation_with_version = self._CreateExpectationName( expectation_with_version = self._CreateExpectationName(
device_type, expectation, chrome_version) device_type, expectation, chrome_version)
if self._ispy.ExpectationExists(expectation_with_version): if self._ispy.ExpectationExists(expectation_with_version):
logging.warning( logging.warning(
'I-Spy expectation \'%s\' already exists, overwriting.', 'I-Spy expectation \'%s\' already exists, overwriting.',
expectation_with_version) expectation_with_version)
screenshots = [self._screenshot_func() for _ in range(8)]
logging.info('Generating I-Spy expectation...') logging.info('Generating I-Spy expectation...')
self._ispy.GenerateExpectation(expectation_with_version, screenshots) self._ispy.GenerateExpectation(expectation_with_version, screenshots)
def PerformComparison(self, test_run, device_type, expectation, def PerformComparison(self, test_run, device_type, expectation,
chrome_version): chrome_version, version_file, screenshot):
"""Take a screenshot and compare it with the given expectation in I-Spy. """Compare a screenshot with the given expectation in I-Spy.
Args: Args:
test_run: name for the test run. test_run: name for the test run.
device_type: string identifier for the device type. device_type: string identifier for the device type.
expectation: name for the expectation to compare against. expectation: name for the expectation to compare against.
chrome_version: the chrome version as a string of the form "31.0.123.4". chrome_version: the chrome version as a string of the form "31.0.123.4".
screenshot: a PIL.Image to compare.
""" """
# https://code.google.com/p/chromedriver/issues/detail?id=463 # https://code.google.com/p/chromedriver/issues/detail?id=463
time.sleep(1)
screenshot = self._screenshot_func()
logging.info('Performing I-Spy comparison...') logging.info('Performing I-Spy comparison...')
self._ispy.PerformComparison( self._ispy.PerformComparison(
test_run, test_run,
self._GetExpectationNameWithVersion( self._GetExpectationNameWithVersion(
device_type, expectation, chrome_version), device_type, expectation, chrome_version, version_file),
screenshot) screenshot)
def GetScriptToWaitForUnchangingDOM(self): def CanRebaselineToTestRun(self, test_run):
"""Returns a JavaScript script that waits for the DOM to stop changing.""" """Returns whether the test run has associated expectations.
return self._wait_for_unchanging_dom_script
Returns:
True if RebaselineToTestRun() can be called for this test run.
"""
if test_run in self._rebaselineable_cache:
return True
return self._cloud_bucket.FileExists(
ispy_utils.GetTestRunPath(test_run, 'rebaseline.txt'))
def RebaselineToTestRun(self, test_run):
"""Update the version file to use expectations associated with |test_run|.
Args:
test_run: The name of the test run to rebaseline.
"""
rebaseline_path = ispy_utils.GetTestRunPath(test_run, 'rebaseline.txt')
rebaseline_attrib = json.loads(
self._cloud_bucket.DownloadFile(rebaseline_path))
self.UpdateExpectationVersion(
rebaseline_attrib['version'], rebaseline_attrib['version_file'])
self._cloud_bucket.RemoveFile(rebaseline_path)
def _SetTestRunRebaselineable(self, test_run, chrome_version, version_file):
"""Writes a JSON file containing the data needed to rebaseline.
Args:
test_run: The name of the test run to add the rebaseline file to.
chrome_version: the chrome version that can be rebaselined to (must have
associated Expectations).
version_file: the path of the version file associated with the test run.
"""
self._rebaselineable_cache[test_run] = True
self._cloud_bucket.UploadFile(
ispy_utils.GetTestRunPath(test_run, 'rebaseline.txt'),
json.dumps({
'version': chrome_version,
'version_file': version_file}),
'application/json')
def PerformComparisonAndPrepareExpectation(self, test_run, device_type,
expectation, chrome_version,
version_file, screenshots):
"""Perform comparison and generate an expectation that can used later.
The test run web UI will have a button to set the Expectations generated for
this version as the expectation for comparison with later versions.
Args:
test_run: The name of the test run to add the rebaseline file to.
device_type: string identifier for the device type.
chrome_version: the chrome version that can be rebaselined to (must have
associated Expectations).
version_file: the path of the version file associated with the test run.
screenshot: a list of similar PIL.Images.
"""
if not self.CanRebaselineToTestRun(test_run):
self._SetTestRunRebaselineable(test_run, chrome_version, version_file)
expectation_with_version = self._CreateExpectationName(
device_type, expectation, chrome_version)
self._ispy.GenerateExpectation(expectation_with_version, screenshots)
self._ispy.PerformComparison(
test_run,
self._GetExpectationNameWithVersion(
device_type, expectation, chrome_version, version_file),
screenshots[-1])
#!/usr/bin/env python
#
# Copyright 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import json
import unittest
from PIL import Image
import chrome_utils
from ..common import cloud_bucket
from ..common import mock_cloud_bucket
class ChromeUtilsTest(unittest.TestCase):
"""Unittest for ChromeUtils."""
def setUp(self):
self.cloud_bucket = mock_cloud_bucket.MockCloudBucket()
self.chrome_util = chrome_utils.ChromeUtils(self.cloud_bucket)
self.white_img = Image.new('RGBA', (10, 10), (255, 255, 255, 255))
self.black_img = Image.new('RGBA', (10, 10), (0, 0, 0, 255))
def testGenerateExpectationsRunComparison(self):
self.chrome_util.GenerateExpectation(
'device', 'test', '1.1.1.1', 'versions.json',
[self.white_img, self.white_img])
self.chrome_util.UpdateExpectationVersion('1.1.1.1', 'versions.json')
self.chrome_util.PerformComparison(
'test1', 'device', 'test', '1.1.1.1', 'versions.json', self.white_img)
expect_name = self.chrome_util._CreateExpectationName(
'device', 'test', '1.1.1.1')
self.assertFalse(self.chrome_util._ispy.FailureExists('test1', expect_name))
self.chrome_util.PerformComparison(
'test2', 'device', 'test', '1.1.1.1','versions.json', self.black_img)
self.assertTrue(self.chrome_util._ispy.FailureExists('test2', expect_name))
def testUpdateExpectationVersion(self):
self.chrome_util.UpdateExpectationVersion('1.0.0.0', 'versions.json')
self.chrome_util.UpdateExpectationVersion('1.0.4.0', 'versions.json')
self.chrome_util.UpdateExpectationVersion('2.1.5.0', 'versions.json')
self.chrome_util.UpdateExpectationVersion('1.1.5.0', 'versions.json')
self.chrome_util.UpdateExpectationVersion('0.0.0.0', 'versions.json')
self.chrome_util.UpdateExpectationVersion('1.1.5.0', 'versions.json')
self.chrome_util.UpdateExpectationVersion('0.0.0.1', 'versions.json')
versions = json.loads(self.cloud_bucket.DownloadFile('versions.json'))
self.assertEqual(versions,
['2.1.5.0', '1.1.5.0', '1.0.4.0', '1.0.0.0', '0.0.0.1', '0.0.0.0'])
def testPerformComparisonAndPrepareExpectation(self):
self.assertFalse(self.chrome_util.CanRebaselineToTestRun('test'))
self.assertRaises(
cloud_bucket.FileNotFoundError,
self.chrome_util.PerformComparisonAndPrepareExpectation,
'test', 'device', 'expect', '1.0', 'versions.json',
[self.white_img, self.white_img])
self.assertTrue(self.chrome_util.CanRebaselineToTestRun('test'))
self.chrome_util.RebaselineToTestRun('test')
versions = json.loads(self.cloud_bucket.DownloadFile('versions.json'))
self.assertEqual(versions, ['1.0'])
self.chrome_util.PerformComparisonAndPrepareExpectation(
'test1', 'device', 'expect', '1.1', 'versions.json',
[self.white_img, self.white_img])
if __name__ == '__main__':
unittest.main()
...@@ -2,7 +2,10 @@ ...@@ -2,7 +2,10 @@
# Use of this source code is governed by a BSD-style license that can be # Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file. # found in the LICENSE file.
"""Utilities for managing I-Spy test results in Google Cloud Storage.""" """Internal utilities for managing I-Spy test results in Google Cloud Storage.
See the ispy.client.chrome_utils module for the external API.
"""
import collections import collections
import itertools import itertools
...@@ -44,7 +47,20 @@ def GetFailurePath(test_run, expectation, file_name=''): ...@@ -44,7 +47,20 @@ def GetFailurePath(test_run, expectation, file_name=''):
Returns: Returns:
the path as a string relative to the bucket. the path as a string relative to the bucket.
""" """
return 'failures/%s/%s/%s' % (test_run, expectation, file_name) return GetTestRunPath(test_run, '%s/%s' % (expectation, file_name))
def GetTestRunPath(test_run, file_name=''):
"""Get the path to a the given test run.
Args:
test_run: name of the test run.
file_name: name of the file.
Returns:
the path as a string relative to the bucket.
"""
return 'failures/%s/%s' % (test_run, file_name)
class ISpyUtils(object): class ISpyUtils(object):
...@@ -92,7 +108,6 @@ class ISpyUtils(object): ...@@ -92,7 +108,6 @@ class ISpyUtils(object):
""" """
self.cloud_bucket.UpdateFile(full_path, image_tools.EncodePNG(image)) self.cloud_bucket.UpdateFile(full_path, image_tools.EncodePNG(image))
def GenerateExpectation(self, expectation, images): def GenerateExpectation(self, expectation, images):
"""Creates and uploads an expectation to GS from a set of images and name. """Creates and uploads an expectation to GS from a set of images and name.
......
...@@ -2,16 +2,21 @@ ...@@ -2,16 +2,21 @@
# Use of this source code is governed by a BSD-style license that can be # Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file. # found in the LICENSE file.
import os
import sys
import webapp2 import webapp2
sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir))
import debug_view_handler import debug_view_handler
import image_handler import image_handler
import main_view_handler import main_view_handler
import rebaseline_handler
import update_mask_handler import update_mask_handler
application = webapp2.WSGIApplication( application = webapp2.WSGIApplication(
[('/update_mask', update_mask_handler.UpdateMaskHandler), [('/update_mask', update_mask_handler.UpdateMaskHandler),
('/rebaseline', rebaseline_handler.RebaselineHandler),
('/debug_view', debug_view_handler.DebugViewHandler), ('/debug_view', debug_view_handler.DebugViewHandler),
('/image', image_handler.ImageHandler), ('/image', image_handler.ImageHandler),
('/', main_view_handler.MainViewHandler)], debug=True) ('/', main_view_handler.MainViewHandler)], debug=True)
...@@ -9,7 +9,7 @@ import os ...@@ -9,7 +9,7 @@ import os
import sys import sys
import webapp2 import webapp2
from ..common import ispy_utils from common import ispy_utils
import views import views
......
...@@ -8,7 +8,7 @@ import sys ...@@ -8,7 +8,7 @@ import sys
import cloudstorage import cloudstorage
from ..common import cloud_bucket from common import cloud_bucket
class GoogleCloudStorageBucket(cloud_bucket.BaseCloudBucket): class GoogleCloudStorageBucket(cloud_bucket.BaseCloudBucket):
......
...@@ -9,8 +9,8 @@ import os ...@@ -9,8 +9,8 @@ import os
import sys import sys
import webapp2 import webapp2
from ..common import cloud_bucket from common import cloud_bucket
from ..common import constants from common import constants
import gs_bucket import gs_bucket
......
...@@ -11,8 +11,9 @@ import re ...@@ -11,8 +11,9 @@ import re
import sys import sys
import webapp2 import webapp2
from ..common import constants from common import chrome_utils
from ..common import ispy_utils from common import constants
from common import ispy_utils
import gs_bucket import gs_bucket
import views import views
...@@ -67,8 +68,10 @@ class MainViewHandler(webapp2.RequestHandler): ...@@ -67,8 +68,10 @@ class MainViewHandler(webapp2.RequestHandler):
""" """
paths = set([path for path in ispy.GetAllPaths('failures/' + test_run) paths = set([path for path in ispy.GetAllPaths('failures/' + test_run)
if path.endswith('actual.png')]) if path.endswith('actual.png')])
can_rebaseline = chrome_utils.ChromeUtils(
ispy.cloud_bucket).CanRebaselineToTestRun(test_run)
rows = [self._CreateRow(test_run, path, ispy) for path in paths] rows = [self._CreateRow(test_run, path, ispy) for path in paths]
if rows:
# Function that sorts by the different_pixels field in the failure-info. # Function that sorts by the different_pixels field in the failure-info.
def _Sorter(a, b): def _Sorter(a, b):
return cmp(b['percent_different'], return cmp(b['percent_different'],
...@@ -76,10 +79,8 @@ class MainViewHandler(webapp2.RequestHandler): ...@@ -76,10 +79,8 @@ class MainViewHandler(webapp2.RequestHandler):
template = JINJA.get_template('main_view.html') template = JINJA.get_template('main_view.html')
self.response.write( self.response.write(
template.render({'comparisons': sorted(rows, _Sorter), template.render({'comparisons': sorted(rows, _Sorter),
'test_run': test_run})) 'test_run': test_run,
else: 'can_rebaseline': can_rebaseline}))
template = JINJA.get_template('empty_view.html')
self.response.write(template.render())
def _CreateRow(self, test_run, path, ispy): def _CreateRow(self, test_run, path, ispy):
"""Creates one failure-row. """Creates one failure-row.
......
# Copyright 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Request Handler that updates the Expectation version."""
import webapp2
from common import constants
from common import chrome_utils
import gs_bucket
class RebaselineHandler(webapp2.RequestHandler):
"""Request handler to allow test mask updates."""
def post(self):
"""Accepts post requests.
Expects a test_run as a parameter and updates the associated version file to
use the expectations associated with that test run.
"""
test_run = self.request.get('test_run')
# Fail if test_run parameter is missing.
if not test_run:
self.response.header['Content-Type'] = 'json/application'
self.response.write(json.dumps(
{'error': '\'test_run\' must be supplied to rebaseline.'}))
return
# Otherwise, set up the utilities.
bucket = gs_bucket.GoogleCloudStorageBucket(constants.BUCKET)
chrome_util = chrome_utils.ChromeUtils(bucket)
# Update versions file.
try:
chrome_util.RebaselineToTestRun(test_run)
except:
self.response.header['Content-Type'] = 'json/application'
self.response.write(json.dumps(
{'error': 'Can not rebaseline to the given test run.'}))
return
# Redirect back to the sites list for the test run.
self.redirect('/?test_run=%s' % test_run)
...@@ -9,9 +9,9 @@ import re ...@@ -9,9 +9,9 @@ import re
import sys import sys
import os import os
from ..common import constants from common import constants
from ..common import image_tools from common import image_tools
from ..common import ispy_utils from common import ispy_utils
import gs_bucket import gs_bucket
......
<!DOCTYPE html>
<html>
<head>
<title>No Failures</title>
</head>
<body>
<h2>No Failures.</h2>
</body>
</html>
...@@ -24,8 +24,25 @@ ...@@ -24,8 +24,25 @@
height: 350px; height: 350px;
} }
</style> </style>
<script language="javascript">
var confirmSubmit = function() {
return confirm("The screenshots generated with this version of chrome will be used as the expected images for future comparisions. Are you sure?");
}
</script>
</head> </head>
<body> <body>
<h3>Test Run: {{ test_run }}</h3>
{% if can_rebaseline and comparisons %}
<form action="/rebaseline" method="post" onsubmit="return confirmSubmit();">
<input type="hidden" name="test_run" value="{{ test_run }}"/>
<input type="submit" value="This Chrome version looks good!"/>
</form>
<br>
{% endif %}
{% if not comparisons %}
<h2>No failures.</h2>
{% endif %}
{% for comp in comparisons %} {% for comp in comparisons %}
<div class="row"> <div class="row">
<div class="cell"> <div class="cell">
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment