Title: [272174] trunk
Revision
272174
Author
[email protected]
Date
2021-02-01 13:40:55 -0800 (Mon, 01 Feb 2021)

Log Message

Lazily create m_windowCloseWatchpoints so we don't mistakenly think we have a frame when re-associating a document to a given cached frame
https://bugs.webkit.org/show_bug.cgi?id=221098
<rdar://72894454>

Reviewed by Ryosuke Niwa and Mark Lam.

.:

* ManualTests/dont-create-invalid-watchpoint-when-going-back.html: Added.
* ManualTests/resources/empty-text.txt: Added.
* ManualTests/resources/full_results.json: Added.
* ManualTests/resources/test-results-page.html: Added.

Source/_javascript_Core:

* bytecode/AccessCase.cpp:
(JSC::AccessCase::commit):
* bytecode/Watchpoint.h:
(JSC::WatchpointSet::isStillValidOnJSThread const):
* runtime/PropertySlot.h:
(JSC::PropertySlot::setWatchpointSet):

Source/WebCore:

There's a scenario when we go back while using the back forward cache, and
we re-associate a cached frame with a document, that we were creating an
already invalidated "frame cleared" watchpoint.
There were a few things we were doing wrong:
1. In JSDOMWindowBase's constructor, we thought that we didn't have a frame,
even though we did. It was because we hadn't finished the bookkeeping in
`FrameLoader::open(CachedFrameBase& cachedFrame)` that associates a document with a
frame. And DOMWindow relies on its document to get its frame.
2. When the watchpoint was invalidated, we were still telling the PropertySlot
about it. This was breaking JSC's invariant that these had to be valid
watchpoints.

This patch resolves:
1. We now lazily create the watchpoint when we first need it. By that time, we
would've already been associated with a frame in the above example.
2. We check if the watchpoint is still valid before telling the PropertySlot
about it, instead of always assuming it's valid.

I wasn't able to get this test to fail in WKTR/DRT, because it seems to be
we're crashing when running some JS code from Safari's injected bundle. I've
added a manual test instead.

* bindings/js/JSDOMWindowBase.cpp:
(WebCore::JSDOMWindowBase::JSDOMWindowBase):
* bindings/js/JSDOMWindowBase.h:
* bindings/js/JSDOMWindowCustom.cpp:
(WebCore::JSDOMWindow::getOwnPropertySlot):

Modified Paths

Added Paths

Diff

Modified: trunk/ChangeLog (272173 => 272174)


--- trunk/ChangeLog	2021-02-01 21:27:01 UTC (rev 272173)
+++ trunk/ChangeLog	2021-02-01 21:40:55 UTC (rev 272174)
@@ -1,3 +1,16 @@
+2021-02-01  Saam Barati  <[email protected]>
+
+        Lazily create m_windowCloseWatchpoints so we don't mistakenly think we have a frame when re-associating a document to a given cached frame
+        https://bugs.webkit.org/show_bug.cgi?id=221098
+        <rdar://72894454>
+
+        Reviewed by Ryosuke Niwa and Mark Lam.
+
+        * ManualTests/dont-create-invalid-watchpoint-when-going-back.html: Added.
+        * ManualTests/resources/empty-text.txt: Added.
+        * ManualTests/resources/full_results.json: Added.
+        * ManualTests/resources/test-results-page.html: Added.
+
 2021-02-01  Don Olmstead  <[email protected]>
 
         [Curl] Enable FTP protocol only when ENABLE_FTPDIR is on

Added: trunk/ManualTests/dont-create-invalid-watchpoint-when-going-back.html (0 => 272174)


--- trunk/ManualTests/dont-create-invalid-watchpoint-when-going-back.html	                        (rev 0)
+++ trunk/ManualTests/dont-create-invalid-watchpoint-when-going-back.html	2021-02-01 21:40:55 UTC (rev 272174)
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html>
+
+<body>
+
+<script>
+
+function run() {
+    popupWindow = window.open("resources/test-results-page.html");
+    console.log("created popup window");
+    popupWindow._onload_ = () => {
+        setTimeout(() => {
+            let a = popupWindow.document.querySelectorAll("a[href='']")[0];
+            console.log(popupWindow.location.href);
+            a.href = ""
+            let priorHistoryLength = popupWindow.history.length;
+            a.click();
+            console.log("called click()");
+            function checkNavigated() {
+                if (popupWindow.history.length !== (priorHistoryLength + 1)) {
+                    setTimeout(checkNavigated, 100);
+                    return;
+                }
+
+                setTimeout(() => {
+                    console.log(popupWindow.location.href);
+                    popupWindow.history.back();
+                    console.log("setting timer to notifyDone");
+                    setTimeout(() => { 
+                        console.log(popupWindow.location.href);
+                        console.log("calling notifydone");
+                    }, 2500);
+                }, 2500);
+            }
+            setTimeout(checkNavigated, 100);
+        }, 2000);
+    };
+}
+
+</script>
+
+<a _onclick_="run()"> click me </a>
+
+</body>
+</html>

Added: trunk/ManualTests/resources/empty-text.txt ( => )


Added: trunk/ManualTests/resources/full_results.json
===================================================================
--- trunk/ManualTests/resources/full_results.json	                        (rev 0)
+++ trunk/ManualTests/resources/full_results.json	2021-02-01 21:40:55 UTC (rev 272174)
@@ -0,0 +1 @@
+ADD_RESULTS({"tests":{"platform":{"mac":{"fast":{"text":{"combining-character-sequence-fallback.html":{"expected":"PASS FAIL","actual":"TEXT","has_stderr":true},"ligature-subdivision.html":{"expected":"PASS FAIL","actual":"PASS","has_stderr":true},"vertical-no-sideways.html":{"expected":"PASS FAIL","actual":"TEXT","has_stderr":true},"systemFont.html":{"expected":"PASS FAIL","actual":"TEXT","has_stderr":true}},"overflow":{"overflow-scrollbar-hit-test.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true}},"objc":{"longlongTest.html":{"report":"REGRESSION","expected"
 ;:"PASS","actual":"TEXT","has_stderr":true},"webScriptObject-hasWebScriptKey.html":{"report":"REGRESSION","expected":"PASS","actual":"TEXT","has_stderr":true}},"loader":{"file-url-mimetypes-2.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true}}}}},"webgl":{"2.0.0":{"conformance":{"textures":{"misc":{"texture-copying-feedback-loops.html":{"report":"REGRESSION","expected":"PASS","actual":"CRASH","has_stderr":true}}}}},"1.0.3":{"conformance":{"textures":{"texture-copying-feedback-loops.html":{"report":"REGRESSION","expected":"PASS","actual":"CRASH","has_stderr&
 quot;:true}}}}},"fast":{"css-generated-content":{"initial-letter-first-line-wrapping.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.93}},"text":{"arabic-zwj-and-zwnj.html":{"expected":"PASS IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["=="]},"font-variations-feature-detection.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":12.16},"emphasis-vertical.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true},"emoji-gender-6.html":{"expected":"IMAGE","actual":"IMAGE",
 "has_stderr":true,"reftest_type":["=="],"image_diff_percent":8.52},"multiglyph-characters.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true},"emoji-gender-9.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.37},"emoji-gender-3.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":4.17},"text-security-disc-bullet-pua.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.37},"letter-spacing-shaping.html":{"expected":"PASS IMAGE"
 ,"actual":"PASS","has_stderr":true,"reftest_type":["=="]},"arabic-blocked-fonts.html":{"expected":"PASS IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["=="]},"system-font-japanese-synthetic-italic.html":{"expected":"PASS IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["!="]},"user-installed-fonts":{"extended-character.html":{"expected":"PASS IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["=="]},"shadow-family.html":{"expected":"PASS IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.69},"shadow-postscript-family.html":{"e
 xpected":"PASS IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.69},"shadow-disable.html":{"expected":"PASS IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["!="]},"shadow-family-disable.html":{"expected":"PASS IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["!="]},"local.html":{"expected":"PASS IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["=="]},"shadow-postscript.html":{"expected":"PASS IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.69},"extended-character-with-user-font.ht
 ml":{"expected":"PASS IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["!="]},"shadow-postscript-disable.html":{"expected":"PASS IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["!="]},"disable.html":{"expected":"PASS IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["!="]},"shadow-postscript-family-disable.html":{"expected":"PASS IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["!="]},"system-ui.html":{"expected":"PASS IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["!="]},"shadow.html":{"expected":"PASS IMAGE","actual
 ":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.69}},"locale-shaping.html":{"expected":"PASS IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["!="]},"woff2-totalsfntsize.html":{"expected":"PASS IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["=="]},"canvas-color-fonts":{"stroke-color-shadow-COLR.html":{"expected":"PASS","actual":"IMAGE","report":"REGRESSION","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.31},"stroke-color-COLR.html":{"expected":"PASS","actual":"IMAGE","report":"REGRESSION","has_stderr":true,"refte
 st_type":["=="],"image_diff_percent":0.31}},"hyphen-min-preferred-width.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.01},"emoji-gender-4.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":3.6},"mac":{"select-character-before-zero-width-joiner.html":{"expected":"PASS IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["=="]}},"international":{"bidi-AN-after-empty-run.html":{"expected":"PASS FAIL","actual":"TEXT","has_stderr":true},"bidi-CS-after-AN.html":{"expected":"PASS FAIL",&
 quot;actual":"TEXT","has_stderr":true},"system-language":{"composition.html":{"expected":"PASS FAIL IMAGE","actual":"PASS","has_stderr":true},"system-font-punctuation.html":{"expected":"PASS FAIL IMAGE","actual":"PASS","has_stderr":true},"navigator-language":{"navigator-language-fr-CA.html":{"expected":"PASS FAIL IMAGE","actual":"TEXT","has_stderr":true},"navigator-language-es-MX.html":{"expected":"PASS FAIL IMAGE","actual":"TEXT","has_stderr":true},"navigator-language-zh-Hant-HK.html":{"expected":"PASS FAIL IMAGE","actual":"TEXT","has_stderr":true},"navigator-language-hi.html":{"expected":"PASS FAIL IMAGE","a
 ctual":"TEXT","has_stderr":true},"navigator-language-fr.html":{"expected":"PASS FAIL IMAGE","actual":"TEXT","has_stderr":true},"navigator-language-zh-TW.html":{"expected":"PASS FAIL IMAGE","actual":"TEXT","has_stderr":true},"navigator-language-zh-Hans.html":{"expected":"PASS FAIL IMAGE","actual":"TEXT","has_stderr":true},"navigator-language-es-ES.html":{"expected":"PASS FAIL IMAGE","actual":"TEXT","has_stderr":true},"navigator-language-zh-HK.html":{"expected":"PASS FAIL IMAGE","actual":"TEXT","has_stderr":true},"navigator-language-pt-PT.html":{"expected":"PASS FAIL IMAGE","actual":"TEXT","has_stderr":true
 },"navigator-language-zh-Hant.html":{"expected":"PASS FAIL IMAGE","actual":"TEXT","has_stderr":true},"navigator-language-en-GB.html":{"expected":"PASS FAIL IMAGE","actual":"TEXT","has_stderr":true},"navigator-language-ja.html":{"expected":"PASS FAIL IMAGE","actual":"TEXT","has_stderr":true},"navigator-language-en.html":{"expected":"PASS FAIL IMAGE","actual":"TEXT","has_stderr":true},"navigator-language-en-US.html":{"expected":"PASS FAIL IMAGE","actual":"TEXT","has_stderr":true},"navigator-language-ru.html":{"expected":"PASS FAIL IMAGE","actual":"TEXT","has_stderr":true},"navigator-language-pt-BR.html":{"expected&qu
 ot;:"PASS FAIL IMAGE","actual":"TEXT","has_stderr":true},"navigator-language-es-419.html":{"expected":"PASS FAIL IMAGE","actual":"TEXT","has_stderr":true},"navigator-language-es.html":{"expected":"PASS FAIL IMAGE","actual":"TEXT","has_stderr":true}},"declarative-language.html":{"expected":"PASS FAIL IMAGE","actual":"TEXT","has_stderr":true},"arabic-glyph-cache-fill-combine.html":{"expected":"PASS FAIL IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["!="]},"han-quotes.html":{"expected":"PASS FAIL IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["!="]},"hindi-system-font-punctuation.html&q
 uot;:{"expected":"PASS FAIL IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["=="]}},"thai-line-breaks.html":{"expected":"PASS FAIL","actual":"TEXT","has_stderr":true}},"emoji-gender-5.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":5.0},"letterpress-different.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["!="]},"crash-complex-text-surrogate.html":{"expected":"PASS FAIL","actual":"TEXT","has_stderr":true},"emoji-gender-8.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":t
 rue,"reftest_type":["=="],"image_diff_percent":2.71},"chinese-font-name-aliases-2.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":1.86},"woff2.html":{"expected":"PASS IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["=="]},"control-characters":{"visible-control-characters-3.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["!="]},"visible-control-characters-3.xhtml":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["!="]},"visible-control-characters-2.xhtml":{"expected":"IMAGE",&q
 uot;actual":"IMAGE","has_stderr":true,"reftest_type":["!="]},"visible-control-characters-2.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["!="]},"visible-control-characters-1.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["!="]}}},"parser":{"xml-error-adopted.xml":{"expected":"PASS TIMEOUT","actual":"PASS","has_stderr":true},"adoption-agency-unload-iframe-4.html":{"expected":"PASS TIMEOUT","actual":"PASS","has_stderr":true}},"dynamic":{"text-combine.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true}},"loader"
 ;:{"_javascript_-url-in-embed.html":{"expected":"PASS FAIL","actual":"PASS","has_stderr":true},"redirect-to-invalid-url-using-meta-refresh-disallowed-async-delegates.html":{"expected":"PASS FAIL","actual":"PASS","has_stderr":true},"document-with-fragment-url-4.html":{"expected":"PASS TIMEOUT","actual":"PASS","has_stderr":true},"redirect-to-invalid-url-using-meta-refresh-calls-policy-delegate.html":{"expected":"PASS FAIL","actual":"PASS","has_stderr":true},"document-with-fragment-url-3.html":{"expected":"PASS TIMEOUT","actual":"PASS","has_stderr":true}},"frames":{"sandboxed-iframe-plugins.html":{"report":"REGRESSION","expected":"PASS&quo
 t;,"actual":"TEXT","has_stderr":true},"crash-during-iframe-load-stop.html":{"expected":"PASS TIMEOUT","actual":"PASS","has_stderr":true},"flattening":{"frameset-flattening-subframesets.html":{"expected":"PASS FAIL","actual":"TEXT","has_stderr":true},"frameset-flattening-grid.html":{"expected":"PASS FAIL","actual":"TEXT","has_stderr":true},"iframe-flattening-fixed-height.html":{"expected":"PASS FAIL","actual":"TEXT","has_stderr":true},"iframe-flattening-out-of-view.html":{"expected":"PASS FAIL","actual":"TEXT","has_stderr":true},"iframe-flattening-offscreen.html":{"expected":"PASS FAIL","actual":"TEXT"
 ;,"has_stderr":true},"frameset-flattening-advanced.html":{"expected":"PASS FAIL","actual":"TEXT","has_stderr":true}},"sandboxed-iframe-about-blank.html":{"report":"REGRESSION","expected":"PASS","actual":"TEXT","has_stderr":true},"sandboxed-iframe-navigation-allowed.html":{"report":"REGRESSION","expected":"PASS","actual":"TEXT","has_stderr":true}},"harness":{"sample-fail-mismatch-reftest.html":{"expected":"IMAGE","wontfix":true,"actual":"IMAGE","has_stderr":true,"reftest_type":["!="]}},"css-intrinsic-dimensions":{"height-tables.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,&qu
 ot;reftest_type":["=="],"image_diff_percent":0.02},"height-css-tables.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.04}},"images":{"async-image-body-background-image.html":{"expected":"PASS TIMEOUT","actual":"TIMEOUT","has_stderr":true,"reftest_type":["=="]},"animated-png-loop-count.html":{"expected":"PASS IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["=="]},"animated-gif-loop-count.html":{"expected":"PASS IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["=="]},"webp-as-image.html":{"expected":"PASS IMAGE","actua
 l":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":24.99},"gif-loop-count.html":{"expected":"IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["=="]},"image-copy-memory-usage.html":{"expected":"FAIL","actual":"PASS","has_stderr":true},"animated-gif-no-layout.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":1.14},"slower-animation-than-decoding-image.html":{"expected":"PASS FAIL","actual":"PASS","has_stderr":true},"animated-image-different-dest-size.html":{"expected":"PASS IMAGE","actual":"PASS","has_stderr&qu
 ot;:true,"reftest_type":["=="]},"image-controls-basic.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true},"animated-gif-scrolling-crash.html":{"expected":"PASS TIMEOUT","actual":"TIMEOUT","has_stderr":true},"reset-image-animation.html":{"expected":"PASS FAIL","actual":"PASS","has_stderr":true}},"table":{"hittest-tablecell-right-edge.html":{"expected":"PASS FAIL","actual":"PASS","has_stderr":true},"border-collapsing":{"collapsed-borders-adjoining-sections.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.07}},"hittest-tablecell-with-borders-right-edge.h
 tml":{"expected":"PASS FAIL","actual":"PASS","has_stderr":true}},"layers":{"no-clipping-overflow-hidden-added-after-transform.html":{"expected":"PASS IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["=="]}},"canvas":{"canvas-ellipse-zero-lineto.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true},"canvas-fillRect-gradient-shadow.html":{"expected":"PASS FAIL","actual":"PASS","has_stderr":true},"canvas-composite-alpha.html":{"expected":"PASS FAIL","actual":"PASS","has_stderr":true},"canvas-scale-strokePath-shadow.html":{"expected":"PASS FAIL","actual":"TEXT","has_stderr":true},"
 ;2d.text.draw.fill.maxWidth.gradient.html":{"expected":"PASS FAIL","actual":"PASS","has_stderr":true},"canvas-quadratic-same-endpoint.html":{"expected":"FAIL","actual":"PASS","has_stderr":true},"canvas-draw-canvas-on-canvas-shadow.html":{"expected":"PASS FAIL","actual":"PASS","has_stderr":true},"set-colors.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true},"canvas-currentColor.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true},"canvas-imageSmoothingEnabled-repaint.html":{"expected":"PASS FAIL","actual":"PASS","has_stderr":true},"webgl":{"webgl-compressed-texture-size-limit.html":{"expected&qu
 ot;:"PASS FAIL TIMEOUT","actual":"TEXT","has_stderr":true},"bufferData-offset-length.html":{"expected":"PASS FAIL","actual":"TEXT","has_stderr":true},"context-creation-attributes.html":{"report":"REGRESSION","expected":"PASS","actual":"TEXT","has_stderr":true},"tex-image-and-sub-image-2d-with-image-data-rgb565.html":{"expected":"PASS FAIL","actual":"PASS","has_stderr":true},"tex-image-and-sub-image-2d-with-array-buffer-view.html":{"expected":"PASS FAIL","actual":"PASS","has_stderr":true},"tex-image-and-sub-image-2d-with-image-data-rgba4444.html":{"expected":"PASS FAIL","actual":"PASS","has_stderr":true},"webgl-draw-buffers.h
 tml":{"expected":"PASS FAIL","actual":"TEXT","has_stderr":true},"read-pixels-test.html":{"expected":"PASS FAIL","actual":"PASS","has_stderr":true},"tex-image-and-sub-image-2d-with-potentially-subsampled-image.html":{"expected":"CRASH","actual":"PASS","has_stderr":true},"program-test.html":{"expected":"FAIL","actual":"PASS","has_stderr":true},"tex-image-and-sub-image-2d-with-image-data-rgba5551.html":{"expected":"PASS FAIL","actual":"PASS","has_stderr":true},"oes-element-index-uint.html":{"expected":"PASS FAIL","actual":"TEXT","has_stderr":true},"tex-image-and-sub-image-2d-with-image-data.html":{"expected":"PAS
 S FAIL","actual":"PASS","has_stderr":true},"texImage2D-mse-flipY-false.html":{"expected":"PASS TIMEOUT","actual":"PASS","has_stderr":true},"oes-texture-float-linear.html":{"expected":"PASS FAIL","actual":"TEXT","has_stderr":true}}},"multicol":{"pagination":{"RightToLeft-tb.html":{"expected":"FAIL","actual":"PASS","has_stderr":true},"RightToLeft-rl.html":{"expected":"FAIL","actual":"PASS","has_stderr":true}},"multicol-with-child-renderLayer-for-input.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.01},"crash-when-spanner-candidate-is-out-of-flo
 w.html":{"report":"REGRESSION","expected":"PASS","actual":"CRASH","has_stderr":true},"fixed-stack.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.01},"mixed-opacity-fixed-test.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":8.54}},"xmlhttprequest":{"xmlhttprequest-recursive-sync-event.html":{"expected":"PASS FAIL","actual":"PASS","has_stderr":true}},"text-autosizing":{"ios":{"text-autosizing-after-back.html":{"expected":"PASS TIMEOUT","actual":"PASS","has_stderr":true,"
 ;reftest_type":["=="]}}},"ruby":{"base-shorter-than-text.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true}},"workers":{"worker-terminate.html":{"expected":"PASS TIMEOUT","actual":"PASS","has_stderr":true}},"preloader":{"image-srcset.html":{"expected":"PASS FAIL","actual":"PASS","has_stderr":true}},"forms":{"searchfield-heights.html":{"expected":"PASS FAIL","actual":"TEXT","has_stderr":true},"textarea":{"textarea-state-restore.html":{"expected":"PASS FAIL TIMEOUT","actual":"PASS","has_stderr":true}},"search-event-delay.html":{"expected":"PASS FAIL","actual":"PASS",&q
 uot;has_stderr":true},"input-set-composition-scroll.html":{"expected":"PASS FAIL","actual":"TEXT","has_stderr":true},"focus-selection-textarea.html":{"expected":"PASS FAIL","actual":"PASS","has_stderr":true},"state-restore-to-non-edited-controls.html":{"expected":"PASS FAIL","actual":"PASS","has_stderr":true},"listbox-scrollbar-hit-test.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true}},"layoutformattingcontext":{"run-expansion-extends-line-content-width.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.44},"quirk-margin-not-collapse-when-floating.html":{"expected&
 quot;:"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.27},"inlin-box-content-fits-but-the-box-does-not.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.62},"br-and-wbr-simple.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.34},"intrusive-floats-takes-entire-horizontal-space.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.72},"horizontal-sizing-with-trailing-letter-spacing.html":{"expected":"IMAGE","actual&
 quot;:"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.93},"wbr-simple.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.02}},"performance":{"performance-now-timestamps.html":{"expected":"PASS FAIL","actual":"PASS","has_stderr":true}},"borders":{"border-painting-dotted.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":1.08},"border-painting-dashed.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.71},"
 ;border-painting-double.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.47},"border-fit.html":{"expected":"PASS FAIL","actual":"TEXT","has_stderr":true},"border-fit-2.html":{"expected":"PASS FAIL","actual":"PASS","has_stderr":true}},"sub-pixel":{"block-with-margin-overflow.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.1},"client-width-height-snapping.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true},"file-upload-control-at-fractional-offset.html":{"expected":"IMAGE",&qu
 ot;actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.02},"input-caret-on-subpixel-bound.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.01}},"events":{"scroll-div-with-prevent-default-in-subframe.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true},"context-nodrag.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true},"attempt-scroll-with-no-scrollbars.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true},"tabindex-focus-blur-all.html":{"expected":"PASS TIMEOUT","actual":"PASS","has_stderr":t
 rue},"mouse-cursor-change.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true},"mouseout-on-window.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true},"constructors":{"close-event-constructor.html":{"report":"REGRESSION","expected":"PASS","actual":"TEXT","has_stderr":true},"overflow-event-constructor.html":{"report":"REGRESSION","expected":"PASS","actual":"TEXT","has_stderr":true}},"before-unload-adopt-subframe-to-outside.html":{"expected":"PASS TIMEOUT","actual":"PASS","has_stderr":true},"overflow-scroll-fake-mouse-move.html":{"expected":"PASS FAIL","actual":"PASS","has
 _stderr":true}},"css":{"parsing-css-allowed-string-characters.html":{"expected":"PASS FAIL","actual":"PASS","has_stderr":true},"object-fit":{"object-fit-canvas.html":{"expected":"PASS IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.04}},"vertical-align-baseline-rowspan-010.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.48},"sticky":{"inline-sticky-abspos-child.html":{"expected":"PASS IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["=="]},"inline-sticky.html":{"expected":"PASS IMAGE","actual"
 ;:"PASS","has_stderr":true,"reftest_type":["=="]},"sticky-top-zoomed.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":13.56},"sticky-both-sides.html":{"expected":"PASS IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["=="]}},"read-only-read-write-input-basics.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.35},"resize-corner-tracking.html":{"expected":"PASS FAIL","actual":"TEXT","has_stderr":true},"input-search-padding.html":{"expected":"FAIL","actual":"TEXT",&q
 uot;has_stderr":true},"pseudo-visited-background-color-on-input.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.36},"counters":{"element-removal-crash.xhtml":{"expected":"PASS FAIL","actual":"PASS","has_stderr":true}}},"hidpi":{"filters-and-image-buffer-resolution.html":{"expected":"PASS IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["=="]}},"css-grid-layout":{"grid-shorthands-style-format.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true},"positioned-grid-container-percentage-tracks.html":{"expected":"FAIL","actual":"TEXT","has_stder
 r":true},"grid-align-baseline-vertical.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true}},"mediacapturefromelement":{"CanvasCaptureMediaStream-2d-events.html":{"expected":"PASS TIMEOUT","actual":"PASS","has_stderr":true}},"overflow":{"scrollbar-click-retains-focus.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true},"horizontal-scroll-after-back.html":{"expected":"PASS TIMEOUT","actual":"PASS","has_stderr":true}},"scrolling":{"rtl-scrollbars-simple.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["!="]},"rtl-scrollbars-positioning.html":{"expected":"IMAGE",&quo
 t;actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.28},"rtl-scrollbars-animation-property.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true},"rtl-scrollbars-iframe-offset.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.18},"rtl-scrollbars.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.78},"rtl-scrollbars-text-selection.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.01},"scrollable-area-fra
 me-zero-size-and-border.html":{"expected":"PASS FAIL","actual":"PASS","has_stderr":true},"rtl-scrollbars-position-absolute.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.01},"rtl-scrollbars-elementFromPoint-static.html":{"expected":"FAIL","actual":"PASS","has_stderr":true},"rtl-scrollbars-positioned-intersect-scrollbars.html":{"expected":"IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["=="]},"rtl-scrollbars-overflow-elementFromPoint.html":{"expected":"FAIL","actual":"PASS","has_stderr":true},"rtl-scrollbars-iframe-scrolled.html":{"expected":"IMAGE"
 ;,"actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.01},"rtl-scrollbars-listbox-simple.html":{"expected":"IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["!="]},"rtl-scrollbars-listbox-scroll.html":{"expected":"IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["=="]},"rtl-scrollbars-text-selection-scrolled.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.01},"rtl-scrollbars-listbox-select-right.html":{"expected":"IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["=="]},"rtl-scrollbars-ov
 erflow-contents.html":{"expected":"IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["=="]},"rtl-scrollbars-overflow-position-absolute.html":{"expected":"IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["=="]},"rtl-scrollbars-overflow-text-selection-scrolled.html":{"expected":"IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["=="]},"rtl-scrollbars-overflow.html":{"expected":"IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["=="]},"rtl-scrollbars-listbox-select-left.html":{"expected":"IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["=="]},"rtl-scrollba
 rs-overflow-simple.html":{"expected":"IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["!="]},"rtl-scrollbars-overflow-padding.html":{"expected":"IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["=="]},"rtl-scrollbars-alternate-body-dir-attr-does-not-update-scrollbar-placement-2.html":{"expected":"IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["=="]},"rtl-scrollbars-position-fixed.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.01},"rtl-scrollbars-overflow-dir-rtl.html":{"expected":"IMAGE","actual":"PASS","has_stderr":true
 ,"reftest_type":["=="]},"rtl-scrollbars-listbox.html":{"expected":"IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["=="]},"rtl-scrollbars-alternate-iframe-body-dir-attr-does-not-update-scrollbar-placement.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.58}},"animation":{"request-animation-frame-time-unit.html":{"expected":"PASS FAIL","actual":"PASS","has_stderr":true}},"repaint":{"border-fit-lines.html":{"expected":"PASS FAIL","actual":"PASS","has_stderr":true,"reftest_type":["=="]},"hidpi-wrong-repaint-rect-when-parent-has-noncompositing-transform.html":{"ex
 pected":"FAIL","actual":"TEXT","has_stderr":true},"add-table-overpaint.html":{"expected":"PASS FAIL","actual":"TEXT","has_stderr":true},"list-item-equal-style-change-no-repaint.html":{"expected":"PASS FAIL","actual":"PASS","has_stderr":true},"japanese-rl-selection-repaint.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true},"vertical-text-repaint.html":{"expected":"PASS FAIL","actual":"PASS","has_stderr":true},"obscured-background-no-repaint.html":{"expected":"PASS FAIL","actual":"TEXT","has_stderr":true},"japanese-rl-selection-clear.html":{"expected":"FAIL","actual":"TEXT","has_stde
 rr":true},"repaint-across-writing-mode-boundary.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true}},"writing-mode":{"broken-ideograph-small-caps.html":{"expected":"FAIL IMAGE","actual":"TEXT","has_stderr":true},"japanese-lr-selection.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true},"japanese-rl-text.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true},"vertical-subst-font-vert-no-dflt.html":{"expected":"PASS IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["=="]},"japanese-lr-text.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true},"japanese-rl-sel
 ection.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true},"border-vertical-lr.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true},"fallback-orientation.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true}},"dom":{"Geolocation":{"page-reload-cancel-permission-requests.html":{"expected":"FAIL TIMEOUT","actual":"TEXT","has_stderr":true}},"rtl-scroll-to-leftmost-and-resize.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true},"linkify-phone-numbers.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.11},&qu
 ot;Window":{"window-properties-performance-resource-timing.html":{"expected":"PASS FAIL","actual":"TEXT","has_stderr":true},"window-properties-performance.html":{"expected":"PASS FAIL","actual":"TEXT","has_stderr":true},"window-open-self-disallow-close.html":{"expected":"PASS TIMEOUT CRASH","actual":"PASS","has_stderr":true}},"Range":{"compareBoundaryPoints-compareHow-exception.html":{"report":"REGRESSION","expected":"PASS","actual":"TEXT","has_stderr":true}},"MutationObserver":{"end-of-task-delivery.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true}},"HTMLAnchorElement":{"anchor-nodownload-set.html":{"expected
 ":"FAIL","actual":"TEXT","has_stderr":true},"anchor-download.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true}},"navigator-detached-no-crash.html":{"report":"REGRESSION","expected":"PASS","actual":"TEXT","has_stderr":true},"horizontal-scrollbar-in-rtl.html":{"expected":"FAIL","actual":"PASS","has_stderr":true},"HTMLMeterElement":{"meter-optimums.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true},"meter-element.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true},"meter-appearances-capacity.html":{"expected":"FAIL","actual":"TEXT","has_
 stderr":true},"meter-styles.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true},"meter-element-repaint-on-update-value.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true},"meter-boundary-values.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true},"meter-appearances-rating-relevancy.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true}},"gc-attribute-node.html":{"expected":"PASS FAIL","actual":"TEXT","has_stderr":true}},"backgrounds":{"background-opaque-images-over-color.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_dif
 f_percent":0.01},"hidpi-bitmap-background-on-subpixel-position.html":{"expected":"PASS IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["=="]}},"replaced":{"preferred-widths.html":{"expected":"PASS FAIL","actual":"PASS","has_stderr":true},"replaced-breaking.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true},"border-radius-clip.html":{"expected":"PASS FAIL","actual":"TEXT","has_stderr":true},"no-focus-ring-embed.html":{"report":"REGRESSION","expected":"PASS","actual":"TIMEOUT","has_stderr":true},"frame-removed-during-resize-smaller.html":{"expected":"PASS CRASH","actual":"PA
 SS","has_stderr":true},"invalid-object-with-fallback.html":{"report":"REGRESSION","expected":"PASS","actual":"TEXT","has_stderr":true},"no-focus-ring-object.html":{"report":"REGRESSION","expected":"PASS","actual":"TIMEOUT","has_stderr":true}},"css3-text":{"css3-text-decoration":{"text-decoration-skip":{"text-decoration-skip-ink-inherit.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.01}},"no-gap-between-two-rounded-textboxes.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":0.01}}},"block&q
 uot;:{"float":{"016.html":{"expected":"FAIL","actual":"TEXT","has_stderr":true}},"positioning":{"fixed-container-with-relative-parent.html":{"expected":"PASS IMAGE","actual":"PASS","has_stderr":true,"reftest_type":["=="]}},"inside-inlines":{"opacity-on-inline.html":{"expected":"IMAGE","actual":"IMAGE","has_stderr":true,"reftest_type":["=="],"image_diff_percent":2.14}}},"history":{"page-cache-webdatabase-pending-transaction.html":{"expected":"PASS TIMEOUT","actual":"TIMEOUT","has_stderr":true},"history-back-while-pdf-in-pagecache.html":{"expected":"PASS IMAGE TIMEOUT CRASH","actual":"IMAGE","has_stderr&quo
 t;:true,"reftest_type":["=="],"image_diff_percent":0.84}}}},"skipped":1372,"num_regressions":18,"other_crashes":{"DumpRenderTree-62625":{},"DumpRenderTree-63234":{},"DumpRenderTree-63209":{},"DumpRenderTree-63135":{}},"interrupted":false,"num_missing":0,"layout_tests_dir":"/Users/mlam/src/webkit/OpenSource/LayoutTests","version":4,"num_passes":12480,"pixel_tests_enabled":false,"date":"04:13PM on January 27, 2021","has_pretty_patch":true,"fixable":1556,"num_flaky":0,"uses_expectations_file":true});
\ No newline at end of file

Added: trunk/ManualTests/resources/test-results-page.html (0 => 272174)


--- trunk/ManualTests/resources/test-results-page.html	                        (rev 0)
+++ trunk/ManualTests/resources/test-results-page.html	2021-02-01 21:40:55 UTC (rev 272174)
@@ -0,0 +1,2063 @@
+<!DOCTYPE html>
+
+<style>
+body {
+    margin: 0;
+    font-family: Helvetica, sans-serif;
+    font-size: 11pt;
+}
+
+body > * {
+    margin-left: 4px;
+    margin-top: 4px;
+}
+
+h1 {
+    font-size: 14pt;
+    margin-top: 1.5em;
+}
+
+p {
+    margin-bottom: 0.3em;
+}
+
+a.clickable {
+    color: blue;
+    cursor: pointer;
+    margin-left: 0.2em;
+}
+
+tr:not(.results-row) td {
+    white-space: nowrap;
+}
+
+tr:not(.results-row) td:first-of-type {
+    white-space: normal;
+}
+
+td:not(:first-of-type) {
+    text-transform: lowercase;
+}
+
+th, td {
+    padding: 1px 4px;
+    vertical-align: top;
+}
+
+td:nth-child(1) {  
+    min-width: 35em;
+}
+
+th:empty, td:empty {
+    padding: 0;
+}
+
+th {
+    -webkit-user-select: none;
+    -moz-user-select: none;
+}
+
+dt > sup {
+    vertical-align:text-top;
+    font-size:75%;
+}
+
+sup > a {
+    text-decoration: none;
+}
+
+.content-container {
+    min-height: 0;
+}
+
+.note {
+    color: gray;
+    font-size: smaller;
+}
+
+.results-row {
+    background-color: white;
+}
+
+.results-row iframe, .results-row img {
+    width: 800px;
+    height: 600px;
+}
+
+.results-row[data-expanded="false"] {
+    display: none;
+}
+
+#toolbar {
+    position: fixed;
+    top: 2px;
+    right: 2px;
+    text-align: right;
+}
+
+.floating-panel {
+    padding: 6px;
+    background-color: rgba(255, 255, 255, 0.9);
+    border: 1px solid silver;
+    border-radius: 4px;
+}
+
+.expand-button {
+    background-color: white;
+    width: 11px;
+    height: 12px;
+    border: 1px solid gray;
+    display: inline-block;
+    margin: 0 3px 0 0;
+    position: relative;
+    cursor: default;
+}
+
+.current {
+    color: red;
+}
+
+.current .expand-button {
+    border-color: red;
+}
+
+.expand-button-text {
+    position: absolute;
+    top: -0.3em;
+    left: 1px;
+}
+
+tbody .flag {
+    display: none;
+}
+
+tbody.flagged .flag {
+    display: inline;
+}
+
+.stopped-running-early-message {
+    border: 3px solid #d00;
+    font-weight: bold;
+    display: inline-block;
+    padding: 3px;
+}
+
+.result-container {
+    display: inline-block;
+    border: 1px solid gray;
+    margin: 4px;
+}
+
+.result-container iframe, .result-container img {
+    border: 0;
+    vertical-align: top;
+}
+
+.leaks > table {
+    margin: 4px;
+}
+.leaks > td {
+    padding-left: 20px;
+}
+
+#options {
+    background-color: white;
+}
+
+#options-menu {
+    border: 1px solid gray;
+    border-radius: 4px;
+    margin-top: 1px;
+    padding: 2px 4px;
+    box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.6);
+    transition: opacity .2s;
+    text-align: left;
+    position: absolute;
+    right: 4px;
+    background-color: white;
+}
+
+#options-menu label {
+    display: block;
+}
+
+.hidden-menu {
+    pointer-events: none;
+    opacity: 0;
+}
+
+.label {
+    padding-left: 3px;
+    font-weight: bold;
+    font-size: small;
+    background-color: silver;
+}
+
+.pixel-zoom-container {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 96%;
+    margin: 10px;
+    padding: 10px;
+    display: -webkit-box;
+    display: -moz-box;
+    pointer-events: none;
+    background-color: silver;
+    border-radius: 20px;
+    border: 1px solid gray;
+    box-shadow: 0 0 5px rgba(0, 0, 0, 0.75);
+}
+
+.pixel-zoom-container > * {
+    -webkit-box-flex: 1;
+    -moz-box-flex: 1;
+    border: 1px solid black;
+    margin: 4px;
+    overflow: hidden;
+    background-color: white;
+}
+
+.pixel-zoom-container .scaled-image-container {
+    position: relative;
+    overflow: hidden;
+    width: 100%;
+    height: 400px;
+}
+
+.scaled-image-container > img {
+    position: absolute;
+    top: 0;
+    left: 0;
+    image-rendering: -webkit-optimize-contrast;
+}
+
+#flagged-test-container {
+    position: fixed;
+    bottom: 4px;
+    right: 4px;
+    width: 50%;
+    min-width: 400px;
+    background-color: rgba(255, 255, 255, 0.75);
+}
+
+#flagged-test-container > h2 {
+    margin: 0 0 4px 0;
+}
+
+#flagged-tests {
+    padding: 0 5px;
+    margin: 0;
+    height: 7em;
+    overflow-y: scroll;
+}
+</style>
+<style id="unexpected-style"></style>
+
+<script>
+
+class Utils
+{
+    static matchesSelector(node, selector)
+    {
+        if (node.matches)
+            return node.matches(selector);
+
+        if (node.webkitMatchesSelector)
+            return node.webkitMatchesSelector(selector);
+
+        if (node.mozMatchesSelector)
+            return node.mozMatchesSelector(selector);
+    }
+
+    static parentOfType(node, selector)
+    {
+        while (node = node.parentNode) {
+            if (Utils.matchesSelector(node, selector))
+                return node;
+        }
+        return null;
+    }
+
+    static stripExtension(testName)
+    {
+        // Temporary fix, also in Tools/Scripts/webkitpy/layout_tests/constrollers/test_result_writer.py, line 95.
+        // FIXME: Refactor to avoid confusing reference to both test and process names.
+        if (Utils.splitExtension(testName)[1].length > 5)
+            return testName;
+        return Utils.splitExtension(testName)[0];
+    }
+
+    static splitExtension(testName)
+    {
+        let index = testName.lastIndexOf('.');
+        if (index == -1) {
+            return [testName, ''];
+        }
+        return [testName.substring(0, index), testName.substring(index + 1)];
+    }
+
+    static forEach(nodeList, handler)
+    {
+        Array.prototype.forEach.call(nodeList, handler);
+    }
+
+    static toArray(nodeList)
+    {
+        return Array.prototype.slice.call(nodeList);
+    }
+
+    static trim(string)
+    {
+        return string.replace(/^[\s\xa0]+|[\s\xa0]+$/g, '');
+    }
+
+    static async(func, args)
+    {
+        setTimeout(() => { func.apply(null, args); }, 50);
+    }
+
+    static appendHTML(node, html)
+    {
+        if (node.insertAdjacentHTML)
+            node.insertAdjacentHTML('beforeEnd', html);
+        else
+            node.innerHTML += html;
+    }};
+
+class TestResult
+{
+    constructor(info, name)
+    {
+        this.name = name;
+        this.info = info; // FIXME: make this private.
+    }
+
+    isFailureExpected()
+    {
+        let actual = this.info.actual;    
+        let expected = this.info.expected || 'PASS';
+
+        if (actual != 'SKIP') {
+            let expectedArray = expected.split(' ');
+            let actualArray = actual.split(' ');
+            for (let actualValue of actualArray) {
+                if (expectedArray.indexOf(actualValue) == -1 && (expectedArray.indexOf('FAIL') == -1 || (actualValue != 'TEXT' && actualValue != 'IMAGE+TEXT' && actualValue != 'AUDIO')))
+                    return false;
+            }
+        }
+        return true;
+    }
+    
+    isMissingAllResults()
+    {
+        return this.info.actual == 'MISSING';
+    }
+    
+    hasMissingResult()
+    {
+        return this.info.actual.indexOf('MISSING') != -1;
+    }
+    
+    isFlakey(pixelTestsEnabled)
+    {
+        let actualTokens = this.info.actual.split(' ');
+        let passedWithImageOnlyFailureInRetry = actualTokens[0] == 'TEXT' && actualTokens[1] == 'IMAGE';
+        if (actualTokens[1] && this.info.actual.indexOf('PASS') != -1 || (!pixelTestsEnabled && passedWithImageOnlyFailureInRetry))
+            return true;
+        
+        return false;
+    }
+    
+    isPass()
+    {
+        return this.info.actual == 'PASS';
+    }
+
+    isTextFailure()
+    {
+        return this.info.actual.indexOf('TEXT') != -1;
+    }
+
+    isImageFailure()
+    {
+        return this.info.actual.indexOf('IMAGE') != -1;
+    }
+
+    isAudioFailure()
+    {
+        return this.info.actual.indexOf('AUDIO') != -1;
+    }
+
+    hasLeak()
+    {
+        return this.info.actual.indexOf('LEAK') != -1;
+    }
+
+    isCrash()
+    {
+        return this.info.actual == 'CRASH';
+    }
+    
+    isTimeout()
+    {
+        return this.info.actual == 'TIMEOUT';
+    }
+    
+    isUnexpectedPass(pixelTestsEnabled)
+    {
+        if (this.info.actual == 'PASS' && this.info.expected != 'PASS') {
+            if (this.info.expected != 'IMAGE' || (pixelTestsEnabled || this.isRefTest()))
+                return true;
+        }
+        
+        return false;
+    }
+    
+    isRefTest()
+    {
+        return !!this.info.reftest_type;
+    }
+
+    isMismatchRefTest()
+    {
+        return this.isRefTest() && this.info.reftest_type.indexOf('!=') != -1;
+    }
+
+    isMatchRefTest()
+    {
+        return this.isRefTest() && this.info.reftest_type.indexOf('==') != -1;
+    }
+    
+    isMissingText()
+    {
+        return this.info.is_missing_text;
+    }
+
+    isMissingImage()
+    {
+        return this.info.is_missing_image;
+    }
+
+    isMissingAudio()
+    {
+        return this.info.is_missing_audio;
+    }
+    
+    hasStdErr()
+    {
+        return this.info.has_stderr;
+    }
+};
+
+class TestResults
+{
+    constructor(results)
+    {
+        this._results = results;
+
+        this.crashTests = [];
+        this.crashOther = [];
+        this.missingResults = [];
+        this.failingTests = [];
+        this.testsWithStderr = [];
+        this.timeoutTests = [];
+        this.unexpectedPassTests = [];
+        this.flakyPassTests = [];
+
+        this.hasHttpTests = false;
+        this.hasImageFailures = false;
+        this.hasTextFailures = false;
+
+        this._testsByName = new Map;
+
+        this._forEachTest(this._results.tests, '');
+        this._forOtherCrashes(this._results.other_crashes);
+    }
+    
+    date()
+    {
+        return this._results.date;
+    }
+
+    layoutTestsDir()
+    {
+        return this._results.layout_tests_dir;
+    }
+    
+    usesExpectationsFile()
+    {
+        return this._results.uses_expectations_file;
+    }
+    
+    resultForTest(testName)
+    {
+        return this._resultsByTest[testName];
+    }
+    
+    wasInterrupted()
+    {
+        return this._results.interrupted;
+    }
+
+    hasPrettyPatch()
+    {
+        return this._results.has_pretty_patch;
+    }
+    
+    hasWDiff()
+    {
+        return this._results.has_wdiff;
+    }
+    
+    testWithName(testName)
+    {
+        return this._testsByName.get(testName);
+    }
+
+    _processResultForTest(testResult)
+    {
+        this._testsByName.set(testResult.name, testResult);
+
+        let test = testResult.name;
+        if (testResult.hasStdErr())
+            this.testsWithStderr.push(testResult);
+
+        this.hasHttpTests |= test.indexOf('http/') == 0;
+
+        if (this.usesExpectationsFile())
+            testResult.isExpected = testResult.isFailureExpected();
+        
+        if (testResult.isTextFailure())
+            this.hasTextFailures = true;
+
+        if (testResult.isImageFailure())
+            this.hasImageFailures = true;
+
+        if (testResult.isMissingAllResults()) {
+            // FIXME: make sure that new-run-webkit-tests spits out an -actual.txt file for tests with MISSING results.
+            this.missingResults.push(testResult);
+            return;
+        }
+
+        if (testResult.isFlakey(this._results.pixel_tests_enabled)) {
+            this.flakyPassTests.push(testResult);
+            return;
+        }
+
+        if (testResult.isPass()) {
+            if (testResult.isUnexpectedPass(this._results.pixel_tests_enabled))
+                this.unexpectedPassTests.push(testResult);
+            return;
+        }
+
+        if (testResult.isCrash()) {
+            this.crashTests.push(testResult);
+            return;
+        }
+
+        if (testResult.isTimeout()) {
+            this.timeoutTests.push(testResult);
+            return;
+        }
+    
+        this.failingTests.push(testResult);
+    }
+    
+    _forEachTest(tree, prefix)
+    {
+        for (let key in tree) {
+            let newPrefix = prefix ? (prefix + '/' + key) : key;
+            if ('actual' in tree[key]) {
+                let testObject = new TestResult(tree[key], newPrefix);
+                this._processResultForTest(testObject);
+            } else
+                this._forEachTest(tree[key], newPrefix);
+        }
+    }
+
+    _forOtherCrashes(tree)
+    {
+        for (let key in tree) {
+            let testObject = new TestResult(tree[key], key);
+            this.crashOther.push(testObject);
+        }
+    }
+    
+    static sortByName(tests)
+    {
+        tests.sort(function (a, b) { return a.name.localeCompare(b.name) });
+    }
+
+    static hasUnexpectedResult(tests)
+    {
+        return tests.some(function (test) { return !test.isExpected; });
+    }
+};
+
+class TestResultsController
+{        
+    constructor(containerElement, testResults)
+    {
+        this.containerElement = containerElement;
+        this.testResults = testResults;
+
+        this.shouldToggleImages = true;
+        this._togglingImageInterval = null;
+        
+        this._updatePageTitle();
+
+        this.buildResultsTables();
+        this.hideNonApplicableUI();
+        this.setupSorting();
+        this.setupOptions();
+    }
+    
+    buildResultsTables()
+    {
+        if (this.testResults.wasInterrupted()) {
+            let interruptionMessage = document.createElement('p');
+            interruptionMessage.textContent = 'Testing exited early';
+            interruptionMessage.classList.add('stopped-running-early-message');
+            this.containerElement.appendChild(interruptionMessage);
+        }
+
+        if (this.testResults.crashTests.length)
+            this.containerElement.appendChild(this.buildOneSection(this.testResults.crashTests, 'crash-tests-table'));
+
+        if (this.testResults.crashOther.length)
+            this.containerElement.appendChild(this.buildOneSection(this.testResults.crashOther, 'other-crash-tests-table'));
+
+        if (this.testResults.failingTests.length)
+            this.containerElement.appendChild(this.buildOneSection(this.testResults.failingTests, 'results-table'));
+
+        if (this.testResults.missingResults.length)
+            this.containerElement.appendChild(this.buildOneSection(this.testResults.missingResults, 'missing-table'));
+
+        if (this.testResults.timeoutTests.length)
+            this.containerElement.appendChild(this.buildOneSection(this.testResults.timeoutTests, 'timeout-tests-table'));
+
+        if (this.testResults.testsWithStderr.length)
+            this.containerElement.appendChild(this.buildOneSection(this.testResults.testsWithStderr, 'stderr-table'));
+
+        if (this.testResults.flakyPassTests.length)
+            this.containerElement.appendChild(this.buildOneSection(this.testResults.flakyPassTests, 'flaky-tests-table'));
+
+        if (this.testResults.usesExpectationsFile() && this.testResults.unexpectedPassTests.length)
+            this.containerElement.appendChild(this.buildOneSection(this.testResults.unexpectedPassTests, 'passes-table'));
+
+        if (this.testResults.hasHttpTests) {
+            let httpdAccessLogLink = document.createElement('p');
+            httpdAccessLogLink.innerHTML = 'httpd access log: <a href=""
+
+            let httpdErrorLogLink = document.createElement('p');
+            httpdErrorLogLink.innerHTML = 'httpd error log: <a href=""
+            
+            this.containerElement.appendChild(httpdAccessLogLink);
+            this.containerElement.appendChild(httpdErrorLogLink);
+        }
+        
+        this.updateTestlistCounts();
+    }
+
+    static sectionBuilderClassForTableID(tableID)
+    {
+        const idToBuilderClassMap = {
+            'crash-tests-table' : CrashingTestsSectionBuilder,
+            'other-crash-tests-table' : OtherCrashesSectionBuilder,
+            'results-table' : FailingTestsSectionBuilder,
+            'missing-table' : TestsWithMissingResultsSectionBuilder,
+            'timeout-tests-table' : TimedOutTestsSectionBuilder,
+            'stderr-table' : TestsWithStdErrSectionBuilder,
+            'flaky-tests-table' : FlakyPassTestsSectionBuilder,
+            'passes-table' : UnexpectedPassTestsSectionBuilder,
+        };
+        return idToBuilderClassMap[tableID];
+    }
+    
+    setupSorting()
+    {
+        let resultsTable = document.getElementById('results-table');
+        if (!resultsTable)
+            return;
+        
+        // FIXME: Make all the tables sortable. Maybe SectionBuilder should put a TableSorter on each table.
+        resultsTable.addEventListener('click', TableSorter.handleClick, false);
+        TableSorter.sortColumn(0);
+    }
+    
+    hideNonApplicableUI()
+    {
+        // FIXME: do this all through body classnames.
+        if (!this.testResults.hasTextFailures) {
+            let textResultsHeader = document.getElementById('text-results-header');
+            if (textResultsHeader)
+                textResultsHeader.textContent = '';
+        }
+
+        if (!this.testResults.hasImageFailures) {
+            let imageResultsHeader = document.getElementById('image-results-header');
+            if (imageResultsHeader)
+                imageResultsHeader.textContent = '';
+
+            Utils.parentOfType(document.getElementById('toggle-images'), 'label').style.display = 'none';
+        }
+    }
+    
+    setupOptions()
+    {
+        // FIXME: do this all through body classnames.
+        if (!this.testResults.usesExpectationsFile())
+            Utils.parentOfType(document.getElementById('unexpected-results'), 'label').style.display = 'none';
+    }
+
+    buildOneSection(tests, tableID)
+    {
+        TestResults.sortByName(tests);
+        
+        let sectionBuilderClass = TestResultsController.sectionBuilderClassForTableID(tableID);
+        let sectionBuilder = new sectionBuilderClass(tests, tableID, this);
+        return sectionBuilder.build();
+    }
+
+    updateTestlistCounts()
+    {
+        // FIXME: do this through the data model, not through the DOM.
+        let _onlyShowUnexpectedFailures_ = this.onlyShowUnexpectedFailures();
+        Utils.forEach(document.querySelectorAll('.test-list-count'), count => {
+            let container = Utils.parentOfType(count, 'section');
+            let testContainers;
+            if (onlyShowUnexpectedFailures)
+                testContainers = container.querySelectorAll('tbody:not(.expected)');
+            else
+                testContainers = container.querySelectorAll('tbody');
+
+            count.textContent = testContainers.length;
+        })
+    }
+    
+    flagAll(headerLink)
+    {
+        let tests = this.visibleTests(Utils.parentOfType(headerLink, 'section'));
+        Utils.forEach(tests, tests => {
+            let shouldFlag = true;
+            testNavigator.flagTest(tests, shouldFlag);
+        })
+    }
+
+    unflag(flag)
+    {
+        const shouldFlag = false;
+        testNavigator.flagTest(Utils.parentOfType(flag, 'tbody'), shouldFlag);
+    }
+
+    visibleTests(opt_container)
+    {
+        let container = opt_container || document;
+        if (this.onlyShowUnexpectedFailures())
+            return container.querySelectorAll('tbody:not(.expected)');
+        else
+            return container.querySelectorAll('tbody');
+    }
+
+    // FIXME: this is confusing. Flip the sense around.
+    onlyShowUnexpectedFailures()
+    {
+        return document.getElementById('unexpected-results').checked;
+    }
+
+    static _testListHeader(title)
+    {
+        let header = document.createElement('h1');
+        header.innerHTML = title + ' (<span class=test-list-count></span>): <a href="" class=flag-all _onclick_="controller.flagAll(this)">flag all</a>';
+        return header;
+    }
+
+    testToURL(testResult, layoutTestsPath)
+    {
+        const mappings = {
+            "http/tests/ssl/": "https://127.0.0.1:8443/ssl/",
+            "http/tests/": "http://127.0.0.1:8000/",
+            "http/wpt/": "http://localhost:8800/WebKit/",
+            "imported/w3c/web-platform-tests/": "http://localhost:8800/"
+        };
+
+        for (let key in mappings) {
+            if (testResult.name.startsWith(key))
+                return mappings[key] + testResult.name.substring(key.length);
+
+        }
+        return "file://" + layoutTestsPath + "/" + testResult.name;
+    }
+
+    layoutTestURL(testResult)
+    {
+        if (this.shouldUseTracLinks())
+            return this.layoutTestsBasePath() + testResult.name;
+
+        return this.testToURL(testResult, this.layoutTestsBasePath());
+    }
+
+    layoutTestsBasePath()
+    {
+        let basePath;
+        if (this.shouldUseTracLinks()) {
+            let revision = this.testResults.revision;
+            basePath = 'http://trac.webkit.org';
+            basePath += revision ? ('/export/' + revision) : '/browser';
+            basePath += '/trunk/LayoutTests/';
+        } else
+            basePath = this.testResults.layoutTestsDir() + '/';
+
+        return basePath;
+    }
+
+    convertToLayoutTestBaseRelativeURL(fullURL)
+    {
+        if (fullURL.startsWith('file://')) {
+            let urlPrefix = 'file://' + this.layoutTestsBasePath();
+            if (fullURL.startsWith(urlPrefix))
+                return fullURL.substring(urlPrefix.length);
+        }
+        
+        return fullURL;
+    }
+
+    shouldUseTracLinks()
+    {
+        return !this.testResults.layoutTestsDir() || !location.toString().indexOf('file://') == 0;
+    }
+
+    checkServerIsRunning(event)
+    {
+        if (this.shouldUseTracLinks())
+            return;
+
+        let url = ""
+        if (url.startsWith("file://"))
+            return;
+
+        event.preventDefault();
+        fetch(url, { mode: "no-cors" }).then(() => {
+            window.location = url;
+        }, () => {
+            alert("HTTP server does not seem to be running, please use the run-webkit-httpd script");
+        });
+    }
+
+    testLink(testResult)
+    {
+        return '<a class=test-link _onclick_="controller.checkServerIsRunning(event)" href="" + this.layoutTestURL(testResult) + '">' + testResult.name + '</a><span class=flag _onclick_="controller.unflag(this)"> \u2691</span>';
+    }
+    
+    expandButtonSpan()
+    {
+        return '<span class=expand-button _onclick_="controller.toggleExpectations(this)"><span class=expand-button-text>+</span></span>';
+    }
+    
+    static resultLink(testPrefix, suffix, contents)
+    {
+        return '<a class=result-link href="" + testPrefix + suffix + '" data-prefix="' + testPrefix + '">' + contents + '</a> ';
+    }
+
+    textResultLinks(prefix)
+    {
+        let html = TestResultsController.resultLink(prefix, '-expected.txt', 'expected') +
+            TestResultsController.resultLink(prefix, '-actual.txt', 'actual') +
+            TestResultsController.resultLink(prefix, '-diff.txt', 'diff');
+
+        if (this.testResults.hasPrettyPatch())
+            html += TestResultsController.resultLink(prefix, '-pretty-diff.html', 'pretty diff');
+
+        if (this.testResults.hasWDiff())
+            html += TestResultsController.resultLink(prefix, '-wdiff.html', 'wdiff');
+
+        return html;
+    }
+
+    flakinessDashboardURLForTests(testObjects)
+    {
+        // FIXME: just map and join here.
+        let testList = '';
+        for (let i = 0; i < testObjects.length; ++i) {
+            testList += testObjects[i].name;
+
+            if (i != testObjects.length - 1)
+                testList += ',';
+        }
+
+        return 'https://results.webkit.org/?suite=layout-tests&test=' + encodeURIComponent(testList);
+    }
+
+    _updatePageTitle()
+    {
+        let dateString = this.testResults.date();
+        let title = document.createElement('title');
+        title.textContent = 'Layout Test Results from ' + dateString;
+        document.head.appendChild(title);
+    }
+    
+    // Options handling. FIXME: move to a separate class?
+    updateAllOptions()
+    {
+        Utils.forEach(document.querySelectorAll('#options-menu input'), input => { input.onchange() });
+    }
+
+    toggleOptionsMenu()
+    {
+        let menu = document.getElementById('options-menu');
+        menu.className = (menu.className == 'hidden-menu') ? '' : 'hidden-menu';
+    }
+
+    handleToggleUseNewlines()
+    {
+        OptionWriter.save();
+        testNavigator.updateFlaggedTests();
+    }
+
+    handleUnexpectedResultsChange()
+    {
+        OptionWriter.save();
+        this._updateExpectedFailures();
+    }
+
+    expandAllExpectations()
+    {
+        let expandLinks = this._visibleExpandLinks();
+        for (let link of expandLinks)
+            Utils.async(link => { controller.expandExpectations(link) }, [ link ]);
+    }
+
+    collapseAllExpectations()
+    {
+        let expandLinks = this._visibleExpandLinks();
+        for (let link of expandLinks)
+            Utils.async(link => { controller.collapseExpectations(link) }, [ link ]);
+    }
+
+    expandExpectations(expandLink)
+    {
+        let row = Utils.parentOfType(expandLink, 'tr');
+        let parentTbody = row.parentNode;
+        let existingResultsRow = parentTbody.querySelector('.results-row');
+    
+        const enDash = '\u2013';
+        expandLink.textContent = enDash;
+        if (existingResultsRow) {
+            this._updateExpandedState(existingResultsRow, true);
+            return;
+        }
+
+        let testName = row.getAttribute('data-test-name');
+        let testResult = this.testResults.testWithName(testName);
+
+        let newRow = TestResultsController._buildExpandedRowForTest(testResult, row);
+        parentTbody.appendChild(newRow);
+
+        this._updateExpandedState(newRow, true);
+
+        this._updateImageTogglingTimer();
+    }
+
+    collapseExpectations(expandLink)
+    {
+        expandLink.textContent = '+';
+        let existingResultsRow = Utils.parentOfType(expandLink, 'tbody').querySelector('.results-row');
+        if (existingResultsRow)
+            this._updateExpandedState(existingResultsRow, false);
+    }
+
+    toggleExpectations(element)
+    {
+        let expandLink = element;
+        if (expandLink.className != 'expand-button-text')
+            expandLink = expandLink.querySelector('.expand-button-text');
+
+        if (expandLink.textContent == '+')
+            this.expandExpectations(expandLink);
+        else
+            this.collapseExpectations(expandLink);
+    }
+
+    _updateExpandedState(row, isExpanded)
+    {
+        row.setAttribute('data-expanded', isExpanded);
+        this._updateImageTogglingTimer();
+    }
+
+    handleToggleImagesChange()
+    {
+        OptionWriter.save();
+        this._updateTogglingImages();
+    }
+
+    _visibleExpandLinks()
+    {
+        if (this.onlyShowUnexpectedFailures())
+            return document.querySelectorAll('tbody:not(.expected) .expand-button-text');
+        else
+            return document.querySelectorAll('.expand-button-text');
+    }
+
+    static _togglingImage(prefix)
+    {
+        return '<div class=result-container><div class="label imageText"></div><img class=animatedImage data-prefix="' + prefix + '"></img></div>';
+    }
+
+    _updateTogglingImages()
+    {
+        this.shouldToggleImages = document.getElementById('toggle-images').checked;
+
+        // FIXME: this is all pretty confusing. Simplify.
+        if (this.shouldToggleImages) {
+            Utils.forEach(document.querySelectorAll('table:not(#missing-table) tbody:not([mismatchreftest]) a[href$=".png"]'), TestResultsController._convertToTogglingHandler(function(prefix) {
+                return TestResultsController.resultLink(prefix, '-diffs.html', 'images');
+            }));
+            Utils.forEach(document.querySelectorAll('table:not(#missing-table) tbody:not([mismatchreftest]) img[src$=".png"]'), TestResultsController._convertToTogglingHandler(TestResultsController._togglingImage));
+        } else {
+            Utils.forEach(document.querySelectorAll('a[href$="-diffs.html"]'), element => {
+                TestResultsController._convertToNonTogglingHandler(element);
+            });
+            Utils.forEach(document.querySelectorAll('.animatedImage'), TestResultsController._convertToNonTogglingHandler(function (absolutePrefix, suffix) {
+                return TestResultsController._resultIframe(absolutePrefix + suffix);
+            }));
+        }
+
+        this._updateImageTogglingTimer();
+    }
+
+    _updateExpectedFailures()
+    {
+        // Gross to do this by setting stylesheet text. Use a body class!
+        document.getElementById('unexpected-style').textContent = this.onlyShowUnexpectedFailures() ? '.expected { display: none; }' : '';
+
+        this.updateTestlistCounts();
+        testNavigator.onlyShowUnexpectedFailuresChanged();
+    }
+
+    static _buildExpandedRowForTest(testResult, row)
+    {
+        let newRow = document.createElement('tr');
+        newRow.className = 'results-row';
+        let newCell = document.createElement('td');
+        newCell.colSpan = row.querySelectorAll('td').length;
+
+        // FIXME: migrate more of code to using testResult for building the expanded content.
+        let resultLinks = row.querySelectorAll('.result-link');
+        let hasTogglingImages = false;
+        for (let link of resultLinks) {
+            let result;
+            if (link.textContent == 'images') {
+                hasTogglingImages = true;
+                result = TestResultsController._togglingImage(link.getAttribute('data-prefix'));
+            } else
+                result = TestResultsController._resultIframe(link.href);
+
+            Utils.appendHTML(newCell, result);    
+        }
+
+        if (testResult.hasLeak())
+            newCell.appendChild(TestResultsController._makeLeaksCell(testResult));
+
+        newRow.appendChild(newCell);
+
+        return newRow;
+    }
+    
+    static _makeLeaksCell(testResult)
+    {
+        let container = document.createElement('div');
+        container.className = 'result-container leaks';
+        
+        let label = document.createElement('div');
+        label.className = 'label';
+        label.textContent = "Leaks";
+        container.appendChild(label);
+
+        let leaksTable = document.createElement('table');
+        
+        for (let leak of testResult.info.leaks) {
+            let leakRow = document.createElement('tr');
+
+            for (let leakedObjectType in leak) {
+                let th = document.createElement('th');
+                th.textContent = leakedObjectType;
+                let td = document.createElement('td');
+                
+                let url = "" // FIXME: when we track leaks other than documents, this might not be a URL.
+                td.textContent = controller.convertToLayoutTestBaseRelativeURL(url)
+                
+                leakRow.appendChild(th);
+                leakRow.appendChild(td);
+            }
+            
+            leaksTable.appendChild(leakRow);
+        }
+        
+        container.appendChild(leaksTable);
+        return container;
+    }
+
+    static _resultIframe(src)
+    {
+        // FIXME: use audio tags for AUDIO tests?
+        let layoutTestsIndex = src.indexOf('LayoutTests');
+        let name;
+        if (layoutTestsIndex != -1) {
+            let hasTrac = src.indexOf('trac.webkit.org') != -1;
+            let prefix = hasTrac ? 'trac.webkit.org/.../' : '';
+            name = prefix + src.substring(layoutTestsIndex + 'LayoutTests/'.length);
+        } else {
+            let lastDashIndex = src.lastIndexOf('-pretty');
+            if (lastDashIndex == -1)
+                lastDashIndex = src.lastIndexOf('-');
+            name = src.substring(lastDashIndex + 1);
+        }
+
+        let tagName = (src.lastIndexOf('.png') == -1) ? 'iframe' : 'img';
+
+        if (tagName != 'img')
+            src += '?format=txt';
+        return '<div class=result-container><div class=label>' + name + '</div><' + tagName + ' src="" + src + '"></' + tagName + '></div>';
+    }
+
+
+    static _toggleImages()
+    {
+        let images = document.querySelectorAll('.animatedImage');
+        let imageTexts = document.querySelectorAll('.imageText');
+        for (let i = 0, len = images.length; i < len; i++) {
+            let image = images[i];
+            let text = imageTexts[i];
+            if (text.textContent == 'Expected Image') {
+                text.textContent = 'Actual Image';
+                image.src = "" + '-actual.png';
+            } else {
+                text.textContent = 'Expected Image';
+                image.src = "" + '-expected.png';
+            }
+        }
+    }
+
+    _updateImageTogglingTimer()
+    {
+        let hasVisibleAnimatedImage = document.querySelector('.results-row[data-expanded="true"] .animatedImage');
+        if (!hasVisibleAnimatedImage) {
+            clearInterval(this._togglingImageInterval);
+            this._togglingImageInterval = null;
+            return;
+        }
+
+        if (!this._togglingImageInterval) {
+            TestResultsController._toggleImages();
+            this._togglingImageInterval = setInterval(TestResultsController._toggleImages, 2000);
+        }
+    }
+    
+    static _getResultContainer(node)
+    {
+        return (node.tagName == 'IMG') ? Utils.parentOfType(node, '.result-container') : node;
+    }
+
+    static _convertToTogglingHandler(togglingImageFunction)
+    {
+        return function(node) {
+            let url = "" == 'IMG') ? node.src : node.href;
+            if (url.match('-expected.png$'))
+                TestResultsController._getResultContainer(node).remove();
+            else if (url.match('-actual.png$')) {
+                let name = Utils.parentOfType(node, 'tbody').querySelector('.test-link').textContent;
+                TestResultsController._getResultContainer(node).outerHTML = togglingImageFunction(Utils.stripExtension(name));
+            }
+        }
+    }
+    
+    static _convertToNonTogglingHandler(resultFunction)
+    {
+        return function(node) {
+            let prefix = node.getAttribute('data-prefix');
+            TestResultsController._getResultContainer(node).outerHTML = resultFunction(prefix, '-expected.png', 'expected') + resultFunction(prefix, '-actual.png', 'actual');
+        }
+    }
+};
+
+class SectionBuilder {
+    
+    constructor(tests, tableID, resultsController)
+    {
+        this._tests = tests;
+        this._table = null;
+        this._resultsController = resultsController;
+        this._tableID = tableID;
+    }
+
+    build()
+    {
+        TestResults.sortByName(this._tests);
+        
+        let section = document.createElement('section');
+        section.appendChild(TestResultsController._testListHeader(this.sectionTitle()));
+        if (this.hideWhenShowingUnexpectedResultsOnly())
+            section.classList.add('expected');
+
+        this._table = document.createElement('table');
+        this._table.id = this.tableID();
+        this.addTableHeader();
+
+        let visibleResultsCount = 0;
+        for (let testResult of this._tests) {
+            let tbody = this.createTableRow(testResult);
+            this._table.appendChild(tbody);
+            
+            if (!this._resultsController.onlyShowUnexpectedFailures() || testResult.isExpected)
+                ++visibleResultsCount;
+        }
+        
+        section.querySelector('.test-list-count').textContent = visibleResultsCount;
+        section.appendChild(this._table);
+        return section;
+    }
+
+    createTableRow(testResult)
+    {
+        let tbody = document.createElement('tbody');
+        if (testResult.isExpected)
+            tbody.classList.add('expected');
+        
+        let row = document.createElement('tr');
+        row.setAttribute('data-test-name', testResult.name);
+        tbody.appendChild(row);
+
+        let testNameCell = document.createElement('td');
+        this.fillTestCell(testResult, testNameCell);
+        row.appendChild(testNameCell);
+
+        let resultCell = document.createElement('td');
+        this.fillTestResultCell(testResult, resultCell);
+        row.appendChild(resultCell);
+
+        let historyCell = this.createHistoryCell(testResult);
+        if (historyCell)
+            row.appendChild(historyCell);
+
+        return tbody;
+    }
+    
+    hideWhenShowingUnexpectedResultsOnly()
+    {
+        return !TestResults.hasUnexpectedResult(this._tests);
+    }
+    
+    addTableHeader()
+    {
+    }
+    
+    fillTestCell(testResult, cell)
+    {
+        let testLink = this.linkifyTestName() ? this._resultsController.testLink(testResult) : testResult.name;
+        if (this.rowsAreExpandable()) {
+            cell.innerHTML = this._resultsController.expandButtonSpan() + testLink;
+            return;
+        }
+
+        cell.innerHTML = testLink;
+    }
+
+    fillTestResultCell(testResult, cell)
+    {
+    }
+    
+    createHistoryCell(testResult)
+    {
+        let historyCell = document.createElement('td');
+        historyCell.innerHTML = '<a href="" + this._resultsController.flakinessDashboardURLForTests([testResult]) + '">history</a>'
+        return historyCell;
+    }
+    
+    tableID()
+    {
+        return this._tableID;
+    }
+    
+    rowsAreExpandable()
+    {
+        return true;
+    }
+    
+    linkifyTestName()
+    {
+        return true;
+    }
+
+    sectionTitle() { return ''; }
+};
+
+class FailuresSectionBuilder extends SectionBuilder {
+    
+    addTableHeader()
+    {
+        let header = document.createElement('thead');
+        let html = '<th>test</th><th id="text-results-header">results</th><th id="image-results-header">image results</th>';
+
+        if (this._resultsController.testResults.usesExpectationsFile())
+            html += '<th>actual failure</th><th>expected failure</th>';
+
+        html += '<th><a href="" + this._resultsController.flakinessDashboardURLForTests(this._tests) + '">history</a></th>';
+
+        if (this.tableID() == 'flaky-tests-table') // FIXME: use the classes, Luke!
+            html += '<th>failures</th>';
+
+        header.innerHTML = html;
+        this._table.appendChild(header);
+    }
+    
+    createTableRow(testResult)
+    {
+        let tbody = document.createElement('tbody');
+        if (testResult.isExpected)
+            tbody.classList.add('expected');
+        
+        if (testResult.isMismatchRefTest())
+            tbody.setAttribute('mismatchreftest', 'true');
+
+        let row = document.createElement('tr');
+        row.setAttribute('data-test-name', testResult.name);
+        tbody.appendChild(row);
+        
+        let testNameCell = document.createElement('td');
+        this.fillTestCell(testResult, testNameCell);
+        row.appendChild(testNameCell);
+
+        let resultCell = document.createElement('td');
+        this.fillTestResultCell(testResult, resultCell);
+        row.appendChild(resultCell);
+
+        if (testResult.isTextFailure())
+            this.appendTextFailureLinks(testResult, resultCell);
+
+        if (testResult.isAudioFailure())
+            this.appendAudioFailureLinks(testResult, resultCell);
+            
+        if (testResult.hasMissingResult())
+            this.appendActualOnlyLinks(testResult, resultCell);
+
+        let actualTokens = testResult.info.actual.split(/\s+/);
+
+        let testPrefix = Utils.stripExtension(testResult.name);
+        let imageResults = this.imageResultLinks(testResult, testPrefix, actualTokens[0]);
+        if (!imageResults && actualTokens.length > 1)
+            imageResults = this.imageResultLinks(testResult, 'retries/' + testPrefix, actualTokens[1]);
+
+        let imageResultsCell = document.createElement('td');
+        imageResultsCell.innerHTML = imageResults;
+        row.appendChild(imageResultsCell);
+
+        if (this._resultsController.testResults.usesExpectationsFile() || actualTokens.length) {
+            let actualCell = document.createElement('td');
+            actualCell.textContent = testResult.info.actual;
+            row.appendChild(actualCell);
+        }
+
+        if (this._resultsController.testResults.usesExpectationsFile()) {
+            let expectedCell = document.createElement('td');
+            expectedCell.textContent = testResult.hasMissingResult() ? '' : testResult.info.expected;
+            row.appendChild(expectedCell);
+        }
+
+        let historyCell = this.createHistoryCell(testResult);
+        if (historyCell)
+            row.appendChild(historyCell);
+
+        return tbody;
+    }
+
+    appendTextFailureLinks(testResult, cell)
+    {
+        cell.innerHTML += this._resultsController.textResultLinks(Utils.stripExtension(testResult.name));
+    }
+    
+    appendAudioFailureLinks(testResult, cell)
+    {
+        let prefix = Utils.stripExtension(testResult.name);
+        cell.innerHTML += TestResultsController.resultLink(prefix, '-expected.wav', 'expected audio')
+            + TestResultsController.resultLink(prefix, '-actual.wav', 'actual audio')
+            + TestResultsController.resultLink(prefix, '-diff.txt', 'textual diff');
+    }
+    
+    appendActualOnlyLinks(testResult, cell)
+    {
+        let prefix = Utils.stripExtension(testResult.name);
+        if (testResult.isMissingAudio())
+            cell.innerHTML += TestResultsController.resultLink(prefix, '-actual.wav', 'audio result');
+
+        if (testResult.isMissingText())
+            cell.innerHTML += TestResultsController.resultLink(prefix, '-actual.txt', 'result');
+    }
+
+    imageResultLinks(testResult, testPrefix, resultToken)
+    {
+        let result = '';
+        if (resultToken.indexOf('IMAGE') != -1) {
+            let testExtension = Utils.splitExtension(testResult.name)[1];
+
+            if (testResult.isMismatchRefTest()) {
+                result += TestResultsController.resultLink(this._resultsController.layoutTestsBasePath() + testPrefix, '-expected-mismatch.' + testExtension, 'ref mismatch');
+                result += TestResultsController.resultLink(testPrefix, '-actual.png', 'actual');
+            } else {
+                if (testResult.isMatchRefTest())
+                    result += TestResultsController.resultLink(this._resultsController.layoutTestsBasePath() + testPrefix, '-expected.' + testExtension, 'reference');
+
+                if (this._resultsController.shouldToggleImages)
+                    result += TestResultsController.resultLink(testPrefix, '-diffs.html', 'images');
+                else {
+                    result += TestResultsController.resultLink(testPrefix, '-expected.png', 'expected');
+                    result += TestResultsController.resultLink(testPrefix, '-actual.png', 'actual');
+                }
+
+                let diff = testResult.info.image_diff_percent;
+                result += TestResultsController.resultLink(testPrefix, '-diff.png', 'diff (' + diff + '%)');
+            }
+        }
+        
+        if (testResult.hasMissingResult() && testResult.isMissingImage())
+            result += TestResultsController.resultLink(testPrefix, '-actual.png', 'png result');
+        
+        return result;
+    }
+};
+
+class FailingTestsSectionBuilder extends FailuresSectionBuilder {
+    sectionTitle() { return 'Tests that failed text/pixel/audio diff'; }
+};
+
+class TestsWithMissingResultsSectionBuilder extends FailuresSectionBuilder {
+    sectionTitle() { return 'Tests that had no expected results (probably new)'; }
+
+    rowsAreExpandable()
+    {
+        return false;
+    }
+};
+
+class FlakyPassTestsSectionBuilder extends FailuresSectionBuilder {
+    sectionTitle() { return 'Flaky tests (failed the first run and passed on retry)'; }
+};
+
+class UnexpectedPassTestsSectionBuilder extends SectionBuilder {
+    sectionTitle() { return 'Tests expected to fail but passed'; }
+
+    addTableHeader()
+    {
+        let header = document.createElement('thead');
+        header.innerHTML = '<th>test</th><th>expected failure</th><th>history</th>';
+        this._table.appendChild(header);
+    }
+    
+    fillTestResultCell(testResult, cell)
+    {
+        cell.innerHTML = testResult.info.expected;
+    }
+
+    rowsAreExpandable()
+    {
+        return false;
+    }
+};
+
+class TestsWithStdErrSectionBuilder extends SectionBuilder {
+    sectionTitle() { return 'Tests that had stderr output'; }
+    hideWhenShowingUnexpectedResultsOnly() { return false; }
+
+    fillTestResultCell(testResult, cell)
+    {
+        cell.innerHTML = TestResultsController.resultLink(Utils.stripExtension(testResult.name), '-stderr.txt', 'stderr');
+    }
+};
+
+class TimedOutTestsSectionBuilder extends SectionBuilder {
+    sectionTitle() { return 'Tests that timed out'; }
+
+    fillTestResultCell(testResult, cell)
+    {
+        // FIXME: only include timeout actual/diff results here if we actually spit out results for timeout tests.
+        cell.innerHTML = this._resultsController.textResultLinks(Utils.stripExtension(testResult.name));
+    }
+};
+
+class CrashingTestsSectionBuilder extends SectionBuilder {
+    sectionTitle() { return 'Tests that crashed'; }
+
+    fillTestResultCell(testResult, cell)
+    {
+        cell.innerHTML = TestResultsController.resultLink(Utils.stripExtension(testResult.name), '-crash-log.txt', 'crash log')
+                       + TestResultsController.resultLink(Utils.stripExtension(testResult.name), '-sample.txt', 'sample');
+    }
+};
+
+class OtherCrashesSectionBuilder extends SectionBuilder {
+    sectionTitle() { return 'Other crashes'; }
+    fillTestResultCell(testResult, cell)
+    {
+        cell.innerHTML = TestResultsController.resultLink(Utils.stripExtension(testResult.name), '-crash-log.txt', 'crash log');
+    }
+
+    createHistoryCell(testResult)
+    {
+        return null;
+    }
+
+    linkifyTestName()
+    {
+        return false;
+    }
+};
+
+class PixelZoomer {
+    constructor()
+    {
+        this.showOnDelay = true;
+        this._zoomFactor = 6;
+
+        this._resultWidth = 800;
+        this._resultHeight = 600;
+        
+        this._percentX = 0;
+        this._percentY = 0;
+
+        document.addEventListener('mousemove', this, false);
+        document.addEventListener('mouseout', this, false);
+    }
+
+    _zoomedResultWidth()
+    {
+        return this._resultWidth * this._zoomFactor;
+    }
+    
+    _zoomedResultHeight()
+    {
+        return this._resultHeight * this._zoomFactor;
+    }
+    
+    _zoomImageContainer(url)
+    {
+        let container = document.createElement('div');
+        container.className = 'zoom-image-container';
+
+        let title = url.match(/\-([^\-]*)\.png/)[1];
+    
+        let label = document.createElement('div');
+        label.className = 'label';
+        label.appendChild(document.createTextNode(title));
+        container.appendChild(label);
+    
+        let imageContainer = document.createElement('div');
+        imageContainer.className = 'scaled-image-container';
+    
+        let image = new Image();
+        image.src = ""
+        image.style.width = this._zoomedResultWidth() + 'px';
+        image.style.height = this._zoomedResultHeight() + 'px';
+        image.style.border = '1px solid black';
+        imageContainer.appendChild(image);
+        container.appendChild(imageContainer);
+    
+        return container;
+    }
+
+    _createContainer(e)
+    {
+        let tbody = Utils.parentOfType(e.target, 'tbody');
+        let row = tbody.querySelector('tr');
+        let imageDiffLinks = row.querySelectorAll('a[href$=".png"]');
+    
+        let container = document.createElement('div');
+        container.className = 'pixel-zoom-container';
+    
+        let html = '';
+    
+        let togglingImageLink = row.querySelector('a[href$="-diffs.html"]');
+        if (togglingImageLink) {
+            let prefix = togglingImageLink.getAttribute('data-prefix');
+            container.appendChild(this._zoomImageContainer(prefix + '-expected.png'));
+            container.appendChild(this._zoomImageContainer(prefix + '-actual.png'));
+        }
+    
+        for (let link of imageDiffLinks)
+            container.appendChild(this._zoomImageContainer(link.href));
+
+        document.body.appendChild(container);
+        this._drawAll();
+    }
+
+    _draw(imageContainer)
+    {
+        let image = imageContainer.querySelector('img');
+        let containerBounds = imageContainer.getBoundingClientRect();
+        image.style.left = (containerBounds.width / 2 - this._percentX * this._zoomedResultWidth()) + 'px';
+        image.style.top = (containerBounds.height / 2 - this._percentY * this._zoomedResultHeight()) + 'px';
+    }
+
+    _drawAll()
+    {
+        Utils.forEach(document.querySelectorAll('.pixel-zoom-container .scaled-image-container'), element => { this._draw(element) });
+    }
+    
+    handleEvent(event)
+    {
+        if (event.type == 'mousemove') {
+            this._handleMouseMove(event);
+            return;
+        }
+
+        if (event.type == 'mouseout') {
+            this._handleMouseOut(event);
+            return;
+        }
+    }
+
+    _handleMouseOut(event)
+    {
+        if (event.relatedTarget && event.relatedTarget.tagName != 'IFRAME')
+            return;
+
+        // If e.relatedTarget is null, we've moused out of the document.
+        let container = document.querySelector('.pixel-zoom-container');
+        if (container)
+            container.remove();
+    }
+
+    _handleMouseMove(event)
+    {
+        if (this._mouseMoveTimeout) {
+            clearTimeout(this._mouseMoveTimeout);
+            this._mouseMoveTimeout = 0;
+        }
+
+        if (Utils.parentOfType(event.target, '.pixel-zoom-container'))
+            return;
+
+        let container = document.querySelector('.pixel-zoom-container');
+    
+        let resultContainer = (event.target.className == 'result-container') ? event.target : Utils.parentOfType(event.target, '.result-container');
+        if (!resultContainer || !resultContainer.querySelector('img')) {
+            if (container)
+                container.remove();
+            return;
+        }
+
+        let targetLocation = event.target.getBoundingClientRect();
+        this._percentX = (event.clientX - targetLocation.left) / targetLocation.width;
+        this._percentY = (event.clientY - targetLocation.top) / targetLocation.height;
+
+        if (!container) {
+            if (this.showOnDelay) {
+                this._mouseMoveTimeout = setTimeout(() => {
+                    this._createContainer(event);
+                }, 400);
+                return;
+            }
+
+            this._createContainer(event);
+            return;
+        }
+    
+        this._drawAll();
+    }
+};
+
+class TableSorter
+{
+    static _forwardArrow()
+    {
+        return '<svg style="width:10px;height:10px"><polygon points="0,0 10,0 5,10" style="fill:#ccc"></svg>';
+    }
+
+    static _backwardArrow()
+    {
+        return '<svg style="width:10px;height:10px"><polygon points="0,10 10,10 5,0" style="fill:#ccc"></svg>';
+    }
+
+    static _sortedContents(header, arrow)
+    {
+        return arrow + ' ' + Utils.trim(header.textContent) + ' ' + arrow;
+    }
+
+    static _updateHeaderClassNames(newHeader)
+    {
+        let sortHeader = document.querySelector('.sortHeader');
+        if (sortHeader) {
+            if (sortHeader == newHeader) {
+                let isAlreadyReversed = sortHeader.classList.contains('reversed');
+                if (isAlreadyReversed)
+                    sortHeader.classList.remove('reversed');
+                else
+                    sortHeader.classList.add('reversed');
+            } else {
+                sortHeader.textContent = sortHeader.textContent;
+                sortHeader.classList.remove('sortHeader');
+                sortHeader.classList.remove('reversed');
+            }
+        }
+
+        newHeader.classList.add('sortHeader');
+    }
+
+    static _textContent(tbodyRow, column)
+    {
+        return tbodyRow.querySelectorAll('td')[column].textContent;
+    }
+
+    static _sortRows(newHeader, reversed)
+    {
+        let testsTable = document.getElementById('results-table');
+        let headers = Utils.toArray(testsTable.querySelectorAll('th'));
+        let sortColumn = headers.indexOf(newHeader);
+
+        let rows = Utils.toArray(testsTable.querySelectorAll('tbody'));
+
+        rows.sort(function(a, b) {
+            // Only need to support lexicographic sort for now.
+            let aText = TableSorter._textContent(a, sortColumn);
+            let bText = TableSorter._textContent(b, sortColumn);
+        
+            // Forward sort equal values by test name.
+            if (sortColumn && aText == bText) {
+                let aTestName = TableSorter._textContent(a, 0);
+                let bTestName = TableSorter._textContent(b, 0);
+                if (aTestName == bTestName)
+                    return 0;
+                return aTestName < bTestName ? -1 : 1;
+            }
+
+            if (reversed)
+                return aText < bText ? 1 : -1;
+            else
+                return aText < bText ? -1 : 1;
+        });
+
+        for (let row of rows)
+            testsTable.appendChild(row);
+    }
+
+    static sortColumn(columnNumber)
+    {
+        let newHeader = document.getElementById('results-table').querySelectorAll('th')[columnNumber];
+        TableSorter._sort(newHeader);
+    }
+
+    static handleClick(e)
+    {
+        let newHeader = e.target;
+        if (newHeader.localName != 'th')
+            return;
+        TableSorter._sort(newHeader);
+    }
+
+    static _sort(newHeader)
+    {
+        TableSorter._updateHeaderClassNames(newHeader);
+    
+        let reversed = newHeader.classList.contains('reversed');
+        let sortArrow = reversed ? TableSorter._backwardArrow() : TableSorter._forwardArrow();
+        newHeader.innerHTML = TableSorter._sortedContents(newHeader, sortArrow);
+    
+        TableSorter._sortRows(newHeader, reversed);
+    }    
+};
+
+class OptionWriter {
+    static save()
+    {
+        let options = document.querySelectorAll('label input');
+        let data = ""
+        for (let option of options)
+            data[option.id] = option.checked;
+
+        try {
+            localStorage.setItem(OptionWriter._key, JSON.stringify(data));
+        } catch (err) {
+            if (err.name != "SecurityError")
+                throw err;
+        }
+    }
+
+    static apply()
+    {
+        let json;
+        try {
+            json = localStorage.getItem(OptionWriter._key);
+        } catch (err) {
+           if (err.name != "SecurityError")
+              throw err;
+        }
+
+        if (!json) {
+            controller.updateAllOptions();
+            return;
+        }
+
+        let data = ""
+        for (let id in data) {
+            let input = document.getElementById(id);
+            if (input)
+                input.checked = data[id];
+        }
+        controller.updateAllOptions();
+    }
+
+    static get _key()
+    {
+        return 'run-webkit-tests-options';
+    }
+};
+
+let testResults;
+function ADD_RESULTS(input)
+{
+    testResults = new TestResults(input);
+}
+</script>
+
+<script src=""
+
+<script>
+
+class TestNavigator
+{
+    constructor() {
+        this.currentTestIndex = -1;
+        this.flaggedTests = {};
+        document.addEventListener('keypress', this, false);
+    }
+    
+    handleEvent(event)
+    {
+        if (event.type == 'keypress') {
+            this.handleKeyEvent(event);
+            return;
+        }
+    }
+
+    handleKeyEvent(event)
+    {
+        if (event.metaKey || event.shiftKey || event.ctrlKey)
+            return;
+
+        switch (String.fromCharCode(event.charCode)) {
+            case 'i':
+                this._scrollToFirstTest();
+                break;
+            case 'j':
+                this._scrollToNextTest();
+                break;
+            case 'k':
+                this._scrollToPreviousTest();
+                break;
+            case 'l':
+                this._scrollToLastTest();
+                break;
+            case 'e':
+                this._expandCurrentTest();
+                break;
+            case 'c':
+                this._collapseCurrentTest();
+                break;
+            case 't':
+                this._toggleCurrentTest();
+                break;
+            case 'f':
+                this._toggleCurrentTestFlagged();
+                break;
+        }
+    }
+
+    _scrollToFirstTest()
+    {
+        if (this._setCurrentTest(0))
+            this._scrollToCurrentTest();
+    }
+
+    _scrollToLastTest()
+    {
+        let links = controller.visibleTests();
+        if (this._setCurrentTest(links.length - 1))
+            this._scrollToCurrentTest();
+    }
+
+    _scrollToNextTest()
+    {
+        if (this.currentTestIndex == -1)
+            this._scrollToFirstTest();
+        else if (this._setCurrentTest(this.currentTestIndex + 1))
+            this._scrollToCurrentTest();
+    }
+
+    _scrollToPreviousTest()
+    {
+        if (this.currentTestIndex == -1)
+            this._scrollToLastTest();
+        else if (this._setCurrentTest(this.currentTestIndex - 1))
+            this._scrollToCurrentTest();
+    }
+
+    _currentTestLink()
+    {
+        let links = controller.visibleTests();
+        return links[this.currentTestIndex];
+    }
+
+    _currentTestExpandLink()
+    {
+        return this._currentTestLink().querySelector('.expand-button-text');
+    }
+
+    _expandCurrentTest()
+    {
+        controller.expandExpectations(this._currentTestExpandLink());
+    }
+
+    _collapseCurrentTest()
+    {
+        controller.collapseExpectations(this._currentTestExpandLink());
+    }
+
+    _toggleCurrentTest()
+    {
+        controller.toggleExpectations(this._currentTestExpandLink());
+    }
+
+    _toggleCurrentTestFlagged()
+    {
+        let testLink = this._currentTestLink();
+        this.flagTest(testLink, !testLink.classList.contains('flagged'));
+    }
+
+    // FIXME: Test navigator shouldn't know anything about flagging. It should probably call out to TestFlagger or something.
+    // FIXME: Batch flagging (avoid updateFlaggedTests on each test).
+    flagTest(testTbody, shouldFlag)
+    {
+        let testName = testTbody.querySelector('.test-link').innerText;
+    
+        if (shouldFlag) {
+            testTbody.classList.add('flagged');
+            this.flaggedTests[testName] = 1;
+        } else {
+            testTbody.classList.remove('flagged');
+            delete this.flaggedTests[testName];
+        }
+
+        this.updateFlaggedTests();
+    }
+
+    updateFlaggedTests()
+    {
+        let flaggedTestTextbox = document.getElementById('flagged-tests');
+        if (!flaggedTestTextbox) {
+            let flaggedTestContainer = document.createElement('div');
+            flaggedTestContainer.id = 'flagged-test-container';
+            flaggedTestContainer.className = 'floating-panel';
+            flaggedTestContainer.innerHTML = '<h2>Flagged Tests</h2><pre id="flagged-tests" contentEditable></pre>';
+            document.body.appendChild(flaggedTestContainer);
+
+            flaggedTestTextbox = document.getElementById('flagged-tests');
+        }
+
+        let flaggedTests = Object.keys(this.flaggedTests);
+        flaggedTests.sort();
+        let separator = document.getElementById('use-newlines').checked ? '\n' : ' ';
+        flaggedTestTextbox.innerHTML = flaggedTests.join(separator);
+        document.getElementById('flagged-test-container').style.display = flaggedTests.length ? '' : 'none';
+    }
+
+    _setCurrentTest(testIndex)
+    {
+        let links = controller.visibleTests();
+        if (testIndex < 0 || testIndex >= links.length)
+            return false;
+
+        let currentTest = links[this.currentTestIndex];
+        if (currentTest)
+            currentTest.classList.remove('current');
+
+        this.currentTestIndex = testIndex;
+
+        currentTest = links[this.currentTestIndex];
+        currentTest.classList.add('current');
+
+        return true;
+    }
+
+    _scrollToCurrentTest()
+    {
+        let targetLink = this._currentTestLink();
+        if (!targetLink)
+            return;
+
+        let rowRect = targetLink.getBoundingClientRect();
+        // rowRect is in client coords (i.e. relative to viewport), so we just want to add its top to the current scroll position.
+        document.scrollingElement.scrollTop += rowRect.top;
+    }
+
+    onlyShowUnexpectedFailuresChanged()
+    {
+        let currentTest = document.querySelector('.current');
+        if (!currentTest)
+            return;
+
+        // If our currentTest became hidden, reset the currentTestIndex.
+        if (controller.onlyShowUnexpectedFailures() && currentTest.classList.contains('expected'))
+            this._scrollToFirstTest();
+        else {
+            // Recompute this.currentTestIndex
+            let links = controller.visibleTests();
+            this.currentTestIndex = links.indexOf(currentTest);
+        }
+    }
+};
+
+function handleMouseDown(e)
+{
+    if (!Utils.parentOfType(e.target, '#options-menu') && e.target.id != 'options-link')
+        document.getElementById('options-menu').className = 'hidden-menu';
+}
+
+document.addEventListener('mousedown', handleMouseDown, false);
+
+let controller;
+let pixelZoomer;
+let testNavigator;
+
+function generatePage()
+{
+    let container = document.getElementById('main-content');
+
+    controller = new TestResultsController(container, testResults);
+    pixelZoomer = new PixelZoomer();
+    testNavigator = new TestNavigator();
+
+    OptionWriter.apply();
+
+    console.log("rendered test results page!");
+}
+
+window.addEventListener('load', generatePage, false);
+
+</script>
+<body>
+    
+    <div class="content-container">
+        <div id="toolbar" class="floating-panel">
+        <div class="note">Use the i, j, k and l keys to navigate, e, c to expand and collapse, and f to flag</div>
+        <a class="clickable" _onclick_="controller.expandAllExpectations()">expand all</a>
+        <a class="clickable" _onclick_="controller.collapseAllExpectations()">collapse all</a>
+        <a class="clickable" id=options-link _onclick_="controller.toggleOptionsMenu()">options</a>
+        <div id="options-menu" class="hidden-menu">
+            <label><input id="unexpected-results" type="checkbox" checked _onchange_="controller.handleUnexpectedResultsChange()">Only unexpected results</label>
+            <label><input id="toggle-images" type="checkbox" checked _onchange_="controller.handleToggleImagesChange()">Toggle images</label>
+            <label title="Use newlines instead of spaces to separate flagged tests"><input id="use-newlines" type="checkbox" checked _onchange_="controller.handleToggleUseNewlines()">Use newlines in flagged list</label>
+        </div>
+    </div>
+
+<div id="main-content"></div>
+
+</body>

Modified: trunk/Source/_javascript_Core/ChangeLog (272173 => 272174)


--- trunk/Source/_javascript_Core/ChangeLog	2021-02-01 21:27:01 UTC (rev 272173)
+++ trunk/Source/_javascript_Core/ChangeLog	2021-02-01 21:40:55 UTC (rev 272174)
@@ -1,3 +1,18 @@
+2021-02-01  Saam Barati  <[email protected]>
+
+        Lazily create m_windowCloseWatchpoints so we don't mistakenly think we have a frame when re-associating a document to a given cached frame
+        https://bugs.webkit.org/show_bug.cgi?id=221098
+        <rdar://72894454>
+
+        Reviewed by Ryosuke Niwa and Mark Lam.
+
+        * bytecode/AccessCase.cpp:
+        (JSC::AccessCase::commit):
+        * bytecode/Watchpoint.h:
+        (JSC::WatchpointSet::isStillValidOnJSThread const):
+        * runtime/PropertySlot.h:
+        (JSC::PropertySlot::setWatchpointSet):
+
 2021-01-31  Yusuke Suzuki  <[email protected]>
 
         [JSC] Implement BigInt64Array and BigUint64Array

Modified: trunk/Source/_javascript_Core/bytecode/AccessCase.cpp (272173 => 272174)


--- trunk/Source/_javascript_Core/bytecode/AccessCase.cpp	2021-02-01 21:27:01 UTC (rev 272173)
+++ trunk/Source/_javascript_Core/bytecode/AccessCase.cpp	2021-02-01 21:40:55 UTC (rev 272174)
@@ -192,16 +192,20 @@
 
     Vector<WatchpointSet*, 2> result;
     Structure* structure = this->structure();
+    auto append = [&] (auto* set) {
+        ASSERT(set->isStillValid());
+        result.append(set);
+    };
 
     if (m_identifier) {
         if ((structure && structure->needImpurePropertyWatchpoint())
             || m_conditionSet.needImpurePropertyWatchpoint()
             || (m_polyProtoAccessChain && m_polyProtoAccessChain->needImpurePropertyWatchpoint(vm)))
-            result.append(vm.ensureWatchpointSetForImpureProperty(m_identifier.uid()));
+            append(vm.ensureWatchpointSetForImpureProperty(m_identifier.uid()));
     }
 
     if (additionalSet())
-        result.append(additionalSet());
+        append(additionalSet());
 
     if (structure
         && structure->hasRareData()
@@ -208,7 +212,7 @@
         && structure->rareData()->hasSharedPolyProtoWatchpoint()
         && structure->rareData()->sharedPolyProtoWatchpoint()->isStillValid()) {
         WatchpointSet* set = structure->rareData()->sharedPolyProtoWatchpoint()->inflate();
-        result.append(set);
+        append(set);
     }
 
     m_state = Committed;

Modified: trunk/Source/_javascript_Core/bytecode/Watchpoint.h (272173 => 272174)


--- trunk/Source/_javascript_Core/bytecode/Watchpoint.h	2021-02-01 21:27:01 UTC (rev 272173)
+++ trunk/Source/_javascript_Core/bytecode/Watchpoint.h	2021-02-01 21:40:55 UTC (rev 272174)
@@ -224,6 +224,11 @@
     {
         return state() != IsInvalidated;
     }
+    // Fast way of testing isStillValid(), which only works from the main thread.
+    bool isStillValidOnJSThread() const
+    {
+        return stateOnJSThread() != IsInvalidated;
+    }
     // Like isStillValid(), may be called from another thread.
     bool hasBeenInvalidated() const { return !isStillValid(); }
     

Modified: trunk/Source/_javascript_Core/runtime/PropertySlot.h (272173 => 272174)


--- trunk/Source/_javascript_Core/runtime/PropertySlot.h	2021-02-01 21:27:01 UTC (rev 272173)
+++ trunk/Source/_javascript_Core/runtime/PropertySlot.h	2021-02-01 21:40:55 UTC (rev 272174)
@@ -370,6 +370,7 @@
 
     void setWatchpointSet(WatchpointSet& set)
     {
+        ASSERT(set.isStillValid());
         m_watchpointSet = &set;
     }
 

Modified: trunk/Source/WebCore/ChangeLog (272173 => 272174)


--- trunk/Source/WebCore/ChangeLog	2021-02-01 21:27:01 UTC (rev 272173)
+++ trunk/Source/WebCore/ChangeLog	2021-02-01 21:40:55 UTC (rev 272174)
@@ -1,3 +1,39 @@
+2021-02-01  Saam Barati  <[email protected]>
+
+        Lazily create m_windowCloseWatchpoints so we don't mistakenly think we have a frame when re-associating a document to a given cached frame
+        https://bugs.webkit.org/show_bug.cgi?id=221098
+        <rdar://72894454>
+
+        Reviewed by Ryosuke Niwa and Mark Lam.
+
+        There's a scenario when we go back while using the back forward cache, and
+        we re-associate a cached frame with a document, that we were creating an
+        already invalidated "frame cleared" watchpoint.
+        There were a few things we were doing wrong:
+        1. In JSDOMWindowBase's constructor, we thought that we didn't have a frame,
+        even though we did. It was because we hadn't finished the bookkeeping in
+        `FrameLoader::open(CachedFrameBase& cachedFrame)` that associates a document with a
+        frame. And DOMWindow relies on its document to get its frame. 
+        2. When the watchpoint was invalidated, we were still telling the PropertySlot
+        about it. This was breaking JSC's invariant that these had to be valid
+        watchpoints.
+        
+        This patch resolves:
+        1. We now lazily create the watchpoint when we first need it. By that time, we
+        would've already been associated with a frame in the above example.
+        2. We check if the watchpoint is still valid before telling the PropertySlot
+        about it, instead of always assuming it's valid.
+        
+        I wasn't able to get this test to fail in WKTR/DRT, because it seems to be
+        we're crashing when running some JS code from Safari's injected bundle. I've
+        added a manual test instead.
+
+        * bindings/js/JSDOMWindowBase.cpp:
+        (WebCore::JSDOMWindowBase::JSDOMWindowBase):
+        * bindings/js/JSDOMWindowBase.h:
+        * bindings/js/JSDOMWindowCustom.cpp:
+        (WebCore::JSDOMWindow::getOwnPropertySlot):
+
 2021-02-01  Don Olmstead  <[email protected]>
 
         [Curl] Enable FTP protocol only when ENABLE_FTPDIR is on

Modified: trunk/Source/WebCore/bindings/js/JSDOMWindowBase.cpp (272173 => 272174)


--- trunk/Source/WebCore/bindings/js/JSDOMWindowBase.cpp	2021-02-01 21:27:01 UTC (rev 272173)
+++ trunk/Source/WebCore/bindings/js/JSDOMWindowBase.cpp	2021-02-01 21:40:55 UTC (rev 272174)
@@ -95,7 +95,6 @@
 
 JSDOMWindowBase::JSDOMWindowBase(VM& vm, Structure* structure, RefPtr<DOMWindow>&& window, JSWindowProxy* proxy)
     : JSDOMGlobalObject(vm, structure, proxy->world(), &s_globalObjectMethodTable)
-    , m_windowCloseWatchpoints(WatchpointSet::create((window && window->frame()) ? IsWatched : IsInvalidated))
     , m_wrapped(WTFMove(window))
 {
     m_proxy.set(vm, this, proxy);

Modified: trunk/Source/WebCore/bindings/js/JSDOMWindowBase.h (272173 => 272174)


--- trunk/Source/WebCore/bindings/js/JSDOMWindowBase.h	2021-02-01 21:27:01 UTC (rev 272173)
+++ trunk/Source/WebCore/bindings/js/JSDOMWindowBase.h	2021-02-01 21:40:55 UTC (rev 272174)
@@ -94,7 +94,7 @@
     JSDOMWindowBase(JSC::VM&, JSC::Structure*, RefPtr<DOMWindow>&&, JSWindowProxy*);
     void finishCreation(JSC::VM&, JSWindowProxy*);
 
-    Ref<JSC::WatchpointSet> m_windowCloseWatchpoints;
+    RefPtr<JSC::WatchpointSet> m_windowCloseWatchpoints;
 
 private:
     using ResponseCallback = WTF::Function<void(const char*, size_t)>;

Modified: trunk/Source/WebCore/bindings/js/JSDOMWindowCustom.cpp (272173 => 272174)


--- trunk/Source/WebCore/bindings/js/JSDOMWindowCustom.cpp	2021-02-01 21:27:01 UTC (rev 272173)
+++ trunk/Source/WebCore/bindings/js/JSDOMWindowCustom.cpp	2021-02-01 21:40:55 UTC (rev 272174)
@@ -243,9 +243,12 @@
     if (!BindingSecurity::shouldAllowAccessToDOMWindow(*lexicalGlobalObject, thisObject->wrapped(), errorMessage))
         return jsDOMWindowGetOwnPropertySlotRestrictedAccess<DOMWindowType::Local>(thisObject, thisObject->wrapped(), *lexicalGlobalObject, propertyName, slot, errorMessage);
 
-    // FIXME: this needs more explanation.
-    // (Particularly, is it correct that this exists here but not in getOwnPropertySlotByIndex?)
-    slot.setWatchpointSet(thisObject->m_windowCloseWatchpoints.get());
+    if (UNLIKELY(!thisObject->m_windowCloseWatchpoints))
+        thisObject->m_windowCloseWatchpoints = WatchpointSet::create(thisObject->wrapped().frame() ? IsWatched : IsInvalidated);
+    // We use m_windowCloseWatchpoints to clear any inline caches once the frame is cleared.
+    // This is sound because DOMWindow can be associated with at most one frame in its lifetime.
+    if (thisObject->m_windowCloseWatchpoints->isStillValidOnJSThread())
+        slot.setWatchpointSet(*thisObject->m_windowCloseWatchpoints);
 
     // (2) Regular own properties.
     PropertySlot slotCopy = slot;
_______________________________________________
webkit-changes mailing list
[email protected]
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to