| OLD | NEW | 
 | (Empty) | 
|    1 // Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file |  | 
|    2 // for details. All rights reserved. Use of this source code is governed by a |  | 
|    3 // BSD-style license that can be found in the LICENSE file. |  | 
|    4  |  | 
|    5 /** |  | 
|    6  * This configuration can be used to rerun selected tests, as well |  | 
|    7  * as see diagnostic output from tests. It runs each test in its own |  | 
|    8  * IFrame, so the configuration consists of two parts - a 'parent' |  | 
|    9  * config that manages all the tests, and a 'child' config for the |  | 
|   10  * IFrame that runs the individual tests. |  | 
|   11  */ |  | 
|   12 #library('interactive_config'); |  | 
|   13  |  | 
|   14 // TODO(gram) - add options for: remove IFrame on done/keep |  | 
|   15 // IFrame for failed tests/keep IFrame for all tests. |  | 
|   16  |  | 
|   17 #import('dart:html'); |  | 
|   18 #import('dart:math'); |  | 
|   19 #import('unittest.dart'); |  | 
|   20  |  | 
|   21 /** The messages exchanged between parent and child. */ |  | 
|   22  |  | 
|   23 class _Message { |  | 
|   24   static const START = 'start'; |  | 
|   25   static const LOG = 'log'; |  | 
|   26   static const STACK = 'stack'; |  | 
|   27   static const PASS = 'pass'; |  | 
|   28   static const FAIL = 'fail'; |  | 
|   29   static const ERROR = 'error'; |  | 
|   30  |  | 
|   31   String messageType; |  | 
|   32   int elapsed; |  | 
|   33   String body; |  | 
|   34  |  | 
|   35   static String text(String messageType, |  | 
|   36                      [int elapsed = 0, String body = '']) => |  | 
|   37       '$messageType $elapsed $body'; |  | 
|   38  |  | 
|   39   _Message(this.messageType, [this.elapsed = 0, this.body = '']); |  | 
|   40  |  | 
|   41   _Message.fromString(String msg) { |  | 
|   42     int idx = msg.indexOf(' '); |  | 
|   43     messageType = msg.substring(0, idx); |  | 
|   44     ++idx; |  | 
|   45     int idx2 = msg.indexOf(' ', idx); |  | 
|   46     elapsed = parseInt(msg.substring(idx, idx2)); |  | 
|   47     ++idx2; |  | 
|   48     body = msg.substring(idx2); |  | 
|   49   } |  | 
|   50  |  | 
|   51   String toString() => text(messageType, elapsed, body); |  | 
|   52 } |  | 
|   53  |  | 
|   54 /** |  | 
|   55  * The child configuration that is used to run individual tests in |  | 
|   56  * an IFrame and post the results back to the parent. In principle |  | 
|   57  * this can run more than one test in the IFrame but currently only |  | 
|   58  * one is used. |  | 
|   59  */ |  | 
|   60 class ChildInteractiveHtmlConfiguration extends Configuration { |  | 
|   61   // TODO(rnystrom): Get rid of this if we get canonical closures for methods. |  | 
|   62   EventListener _onErrorClosure; |  | 
|   63  |  | 
|   64   /** The window to which results must be posted. */ |  | 
|   65   Window parentWindow; |  | 
|   66  |  | 
|   67   /** The time at which tests start. */ |  | 
|   68   Map<int,Date> _testStarts; |  | 
|   69  |  | 
|   70   ChildInteractiveHtmlConfiguration() : |  | 
|   71       _testStarts = new Map<int,Date>(); |  | 
|   72  |  | 
|   73   /** Don't start running tests automatically. */ |  | 
|   74   get autoStart => false; |  | 
|   75  |  | 
|   76   void onInit() { |  | 
|   77     _onErrorClosure = |  | 
|   78         (e) => handleExternalError(e, '(DOM callback has errors)'); |  | 
|   79  |  | 
|   80     /** |  | 
|   81      *  The parent posts a 'start' message to kick things off, |  | 
|   82      *  which is handled by this handler. It saves the parent |  | 
|   83      *  window, gets the test ID from the query parameter in the |  | 
|   84      *  IFrame URL, sets that as a solo test and starts test execution. |  | 
|   85      */ |  | 
|   86     window.on.message.add((MessageEvent e) { |  | 
|   87       // Get the result, do any logging, then do a pass/fail. |  | 
|   88       var m = new _Message.fromString(e.data); |  | 
|   89       if (m.messageType == _Message.START) { |  | 
|   90         parentWindow = e.source; |  | 
|   91         String search = window.location.search; |  | 
|   92         int pos = search.indexOf('t='); |  | 
|   93         String ids = search.substring(pos+2); |  | 
|   94         int id = parseInt(ids); |  | 
|   95         setSoloTest(id); |  | 
|   96         runTests(); |  | 
|   97       } |  | 
|   98     }); |  | 
|   99   } |  | 
|  100  |  | 
|  101   void onStart() { |  | 
|  102     // Listen for uncaught errors. |  | 
|  103     window.on.error.add(_onErrorClosure); |  | 
|  104   } |  | 
|  105  |  | 
|  106   /** Record the start time of the test. */ |  | 
|  107   void onTestStart(TestCase testCase) { |  | 
|  108     super.onTestStart(testCase); |  | 
|  109     _testStarts[testCase.id]= new Date.now(); |  | 
|  110   } |  | 
|  111  |  | 
|  112   /** |  | 
|  113    * Tests can call [logMessage] for diagnostic output. These log |  | 
|  114    * messages in turn get passed to this method, which adds |  | 
|  115    * a timestamp and posts them back to the parent window. |  | 
|  116    */ |  | 
|  117   void logTestCaseMessage(TestCase testCase, String message) { |  | 
|  118     int elapsed; |  | 
|  119     if (testCase == null) { |  | 
|  120       elapsed = -1; |  | 
|  121     } else { |  | 
|  122       Date end = new Date.now(); |  | 
|  123       elapsed = end.difference(_testStarts[testCase.id]).inMilliseconds; |  | 
|  124     } |  | 
|  125     parentWindow.postMessage( |  | 
|  126       _Message.text(_Message.LOG, elapsed, message).toString(), '*'); |  | 
|  127   } |  | 
|  128  |  | 
|  129   /** |  | 
|  130    * Get the elapsed time for the test, anbd post the test result |  | 
|  131    * back to the parent window. If the test failed due to an exception |  | 
|  132    * the stack is posted back too (before the test result). |  | 
|  133    */ |  | 
|  134   void onTestResult(TestCase testCase) { |  | 
|  135     super.onTestResult(testCase); |  | 
|  136     Date end = new Date.now(); |  | 
|  137     int elapsed = end.difference(_testStarts[testCase.id]).inMilliseconds; |  | 
|  138     if (testCase.stackTrace != null) { |  | 
|  139       parentWindow.postMessage( |  | 
|  140           _Message.text(_Message.STACK, elapsed, testCase.stackTrace), '*'); |  | 
|  141     } |  | 
|  142     parentWindow.postMessage( |  | 
|  143         _Message.text(testCase.result, elapsed, testCase.message), '*'); |  | 
|  144   } |  | 
|  145  |  | 
|  146   void onDone(int passed, int failed, int errors, List<TestCase> results, |  | 
|  147               String uncaughtError) { |  | 
|  148     window.on.error.remove(_onErrorClosure); |  | 
|  149   } |  | 
|  150 } |  | 
|  151  |  | 
|  152 /** |  | 
|  153  * The parent configuration runs in the top-level window; it wraps the tests |  | 
|  154  * in new functions that create child IFrames and run the real tests. |  | 
|  155  */ |  | 
|  156 class ParentInteractiveHtmlConfiguration extends Configuration { |  | 
|  157   Map<int,Date> _testStarts; |  | 
|  158   // TODO(rnystrom): Get rid of this if we get canonical closures for methods. |  | 
|  159   EventListener _onErrorClosure; |  | 
|  160  |  | 
|  161   /** The stack that was posted back from the child, if any. */ |  | 
|  162   String _stack; |  | 
|  163  |  | 
|  164   int _testTime; |  | 
|  165   /** |  | 
|  166    * Whether or not we have already wrapped the TestCase test functions |  | 
|  167    * in new closures that instead create an IFrame and get it to run the |  | 
|  168    * test. |  | 
|  169    */ |  | 
|  170   bool _doneWrap = false; |  | 
|  171  |  | 
|  172   /** |  | 
|  173    * We use this to make a single closure from _handleMessage so we |  | 
|  174    * can remove the handler later. |  | 
|  175    */ |  | 
|  176   Function _messageHandler; |  | 
|  177  |  | 
|  178   ParentInteractiveHtmlConfiguration() : |  | 
|  179       _testStarts = new Map<int,Date>(); |  | 
|  180  |  | 
|  181   // We need to block until the test is done, so we make a |  | 
|  182   // dummy async callback that we will use to flag completion. |  | 
|  183   Function completeTest = null; |  | 
|  184  |  | 
|  185   wrapTest(TestCase testCase) { |  | 
|  186     String baseUrl = window.location.toString(); |  | 
|  187     String url = '${baseUrl}?t=${testCase.id}'; |  | 
|  188     return () { |  | 
|  189       // Rebuild the child IFrame. |  | 
|  190       Element childDiv = document.query('#child'); |  | 
|  191       childDiv.nodes.clear(); |  | 
|  192       IFrameElement child = new Element.html(""" |  | 
|  193           <iframe id='childFrame${testCase.id}' src='$url' style='display:none'> |  | 
|  194           </iframe>"""); |  | 
|  195       childDiv.nodes.add(child); |  | 
|  196       completeTest = expectAsync0((){ }); |  | 
|  197       // Kick off the test when the IFrame is loaded. |  | 
|  198       child.on.load.add((e) { |  | 
|  199         child.contentWindow.postMessage(_Message.text(_Message.START), '*'); |  | 
|  200       }); |  | 
|  201     }; |  | 
|  202   } |  | 
|  203  |  | 
|  204   void _handleMessage(MessageEvent e) { |  | 
|  205     // Get the result, do any logging, then do a pass/fail. |  | 
|  206     var msg = new _Message.fromString(e.data); |  | 
|  207     if (msg.messageType == _Message.LOG) { |  | 
|  208       logMessage(e.data); |  | 
|  209     } else if (msg.messageType == _Message.STACK) { |  | 
|  210       _stack = msg.body; |  | 
|  211     } else { |  | 
|  212       _testTime = msg.elapsed; |  | 
|  213       logMessage(_Message.text(_Message.LOG, _testTime, 'Complete')); |  | 
|  214       if (msg.messageType == _Message.PASS) { |  | 
|  215         currentTestCase.pass(); |  | 
|  216       } else if (msg.messageType == _Message.FAIL) { |  | 
|  217         currentTestCase.fail(msg.body, _stack); |  | 
|  218       } else if (msg.messageType == _Message.ERROR) { |  | 
|  219         currentTestCase.error(msg.body, _stack); |  | 
|  220       } |  | 
|  221       completeTest(); |  | 
|  222     } |  | 
|  223   } |  | 
|  224  |  | 
|  225   void onInit() { |  | 
|  226     _messageHandler = _handleMessage; // We need to make just one closure. |  | 
|  227     _onErrorClosure = |  | 
|  228         (e) => handleExternalError(e, '(DOM callback has errors)'); |  | 
|  229     document.query('#group-divs').innerHTML = ""; |  | 
|  230   } |  | 
|  231  |  | 
|  232   void onStart() { |  | 
|  233     // Listen for uncaught errors. |  | 
|  234     window.on.error.add(_onErrorClosure); |  | 
|  235     if (!_doneWrap) { |  | 
|  236       _doneWrap = true; |  | 
|  237       for (int i = 0; i < testCases.length; i++) { |  | 
|  238         testCases[i].test = wrapTest(testCases[i]); |  | 
|  239         testCases[i].setUp = null; |  | 
|  240         testCases[i].tearDown = null; |  | 
|  241       } |  | 
|  242     } |  | 
|  243     window.on.message.add(_messageHandler); |  | 
|  244   } |  | 
|  245  |  | 
|  246   static const _notAlphaNumeric = const RegExp('[^a-z0-9A-Z]'); |  | 
|  247  |  | 
|  248   String _stringToDomId(String s) { |  | 
|  249     if (s.length == 0) { |  | 
|  250       return '-None-'; |  | 
|  251     } |  | 
|  252     return s.trim().replaceAll(_notAlphaNumeric, '-'); |  | 
|  253   } |  | 
|  254  |  | 
|  255   // Used for DOM element IDs for tests result list entries. |  | 
|  256   static const _testIdPrefix = 'test-'; |  | 
|  257   // Used for DOM element IDs for test log message lists. |  | 
|  258   static const _actionIdPrefix = 'act-'; |  | 
|  259   // Used for DOM element IDs for test checkboxes. |  | 
|  260   static const _selectedIdPrefix = 'selected-'; |  | 
|  261  |  | 
|  262   void onTestStart(TestCase testCase) { |  | 
|  263     var id = testCase.id; |  | 
|  264     _testStarts[testCase.id]= new Date.now(); |  | 
|  265     super.onTestStart(testCase); |  | 
|  266     _stack = null; |  | 
|  267     // Convert the group name to a DOM id. |  | 
|  268     String groupId = _stringToDomId(testCase.currentGroup); |  | 
|  269     // Get the div for the group. If it doesn't exist, |  | 
|  270     // create it. |  | 
|  271     var groupDiv = document.query('#$groupId'); |  | 
|  272     if (groupDiv == null) { |  | 
|  273       groupDiv = new Element.html(""" |  | 
|  274           <div class='test-describe' id='$groupId'> |  | 
|  275             <h2> |  | 
|  276               <input type='checkbox' checked='true' class='groupselect'> |  | 
|  277               Group: ${testCase.currentGroup} |  | 
|  278             </h2> |  | 
|  279             <ul class='tests'> |  | 
|  280             </ul> |  | 
|  281           </div>"""); |  | 
|  282       document.query('#group-divs').nodes.add(groupDiv); |  | 
|  283       groupDiv.query('.groupselect').on.click.add((e) { |  | 
|  284         var parent = document.query('#$groupId'); |  | 
|  285         InputElement cb = parent.query('.groupselect'); |  | 
|  286         var state = cb.checked; |  | 
|  287         var tests = parent.query('.tests'); |  | 
|  288         for (Element t in tests.elements) { |  | 
|  289           cb = t.query('.testselect') as InputElement; |  | 
|  290           cb.checked = state; |  | 
|  291           var testId = parseInt(t.id.substring(_testIdPrefix.length)); |  | 
|  292           if (state) { |  | 
|  293             enableTest(testId); |  | 
|  294           } else { |  | 
|  295             disableTest(testId); |  | 
|  296           } |  | 
|  297         } |  | 
|  298       }); |  | 
|  299     } |  | 
|  300     var list = groupDiv.query('.tests'); |  | 
|  301     var testItem = list.query('#$_testIdPrefix$id'); |  | 
|  302     if (testItem == null) { |  | 
|  303       // Create the li element for the test. |  | 
|  304       testItem = new Element.html(""" |  | 
|  305           <li id='$_testIdPrefix$id' class='test-it status-pending'> |  | 
|  306             <div class='test-info'> |  | 
|  307               <p class='test-title'> |  | 
|  308                 <input type='checkbox' checked='true' class='testselect'  |  | 
|  309                     id='$_selectedIdPrefix$id'> |  | 
|  310                 <span class='test-label'> |  | 
|  311                 <span class='timer-result test-timer-result'></span> |  | 
|  312                 <span class='test-name closed'>${testCase.description}</span> |  | 
|  313                 </span> |  | 
|  314               </p> |  | 
|  315             </div> |  | 
|  316             <div class='scrollpane'> |  | 
|  317               <ol class='test-actions' id='$_actionIdPrefix$id'></ol> |  | 
|  318             </div>           |  | 
|  319           </li>"""); |  | 
|  320       list.nodes.add(testItem); |  | 
|  321       testItem.query('#$_selectedIdPrefix$id').on.change.add((e) { |  | 
|  322         InputElement cb = testItem.query('#$_selectedIdPrefix$id'); |  | 
|  323         testCase.enabled = cb.checked; |  | 
|  324       }); |  | 
|  325       testItem.query('.test-label').on.click.add((e) { |  | 
|  326         var _testItem = document.query('#$_testIdPrefix$id'); |  | 
|  327         var _actions = _testItem.query('#$_actionIdPrefix$id'); |  | 
|  328         var _label = _testItem.query('.test-name'); |  | 
|  329         if (_actions.style.display == 'none') { |  | 
|  330           _actions.style.display = 'table'; |  | 
|  331           _label.classes.remove('closed'); |  | 
|  332           _label.classes.add('open'); |  | 
|  333         } else { |  | 
|  334           _actions.style.display = 'none'; |  | 
|  335           _label.classes.remove('open'); |  | 
|  336           _label.classes.add('closed'); |  | 
|  337         } |  | 
|  338       }); |  | 
|  339     } else { // Reset the test element. |  | 
|  340       testItem.classes.clear(); |  | 
|  341       testItem.classes.add('test-it'); |  | 
|  342       testItem.classes.add('status-pending'); |  | 
|  343       testItem.query('#$_actionIdPrefix$id').innerHTML = ''; |  | 
|  344     } |  | 
|  345   } |  | 
|  346  |  | 
|  347   // Actually test logging is handled by the child, then posted |  | 
|  348   // back to the parent. So here we know that the [message] argument |  | 
|  349   // is in the format used by [_Message]. |  | 
|  350   void logTestCaseMessage(TestCase testCase, String message) { |  | 
|  351     var msg = new _Message.fromString(message); |  | 
|  352     if (msg.elapsed < 0) { // No associated test case. |  | 
|  353       document.query('#otherlogs').nodes.add( |  | 
|  354           new Element.html('<p>${msg.body}</p>')); |  | 
|  355     } else { |  | 
|  356       var actions = document.query('#$_testIdPrefix${testCase.id}'). |  | 
|  357           query('.test-actions'); |  | 
|  358       String elapsedText = msg.elapsed >= 0 ? "${msg.elapsed}ms" : ""; |  | 
|  359       actions.nodes.add(new Element.html( |  | 
|  360           "<li style='list-style-stype:none>" |  | 
|  361               "<div class='timer-result'>${elapsedText}</div>" |  | 
|  362               "<div class='test-title'>${msg.body}</div>" |  | 
|  363           "</li>")); |  | 
|  364     } |  | 
|  365   } |  | 
|  366  |  | 
|  367   void onTestResult(TestCase testCase) { |  | 
|  368     if (!testCase.enabled) return; |  | 
|  369     super.onTestResult(testCase); |  | 
|  370     if (testCase.message != '') { |  | 
|  371       logTestCaseMessage(testCase, |  | 
|  372           _Message.text(_Message.LOG, -1, testCase.message)); |  | 
|  373     } |  | 
|  374     int id = testCase.id; |  | 
|  375     var testItem = document.query('#$_testIdPrefix$id'); |  | 
|  376     var timeSpan = testItem.query('.test-timer-result'); |  | 
|  377     timeSpan.text = '${_testTime}ms'; |  | 
|  378     // Convert status into what we need for our CSS. |  | 
|  379     String result = 'status-error'; |  | 
|  380     if (testCase.result == 'pass') { |  | 
|  381       result = 'status-success'; |  | 
|  382     } else if (testCase.result == 'fail') { |  | 
|  383       result = 'status-failure'; |  | 
|  384     } |  | 
|  385     testItem.classes.remove('status-pending'); |  | 
|  386     testItem.classes.add(result); |  | 
|  387     // hide the actions |  | 
|  388     var actions = testItem.query('.test-actions'); |  | 
|  389     for (Element e in actions.nodes) { |  | 
|  390       e.classes.add(result); |  | 
|  391     } |  | 
|  392     actions.style.display = 'none'; |  | 
|  393   } |  | 
|  394  |  | 
|  395   void onDone(int passed, int failed, int errors, List<TestCase> results, |  | 
|  396       String uncaughtError) { |  | 
|  397     window.on.message.remove(_messageHandler); |  | 
|  398     window.on.error.remove(_onErrorClosure); |  | 
|  399     document.query('#busy').style.display = 'none'; |  | 
|  400     InputElement startButton = document.query('#start'); |  | 
|  401     startButton.disabled = false; |  | 
|  402   } |  | 
|  403 } |  | 
|  404  |  | 
|  405 /** |  | 
|  406  * Add the divs to the DOM if they are not present. We have a 'controls' |  | 
|  407  * div for control, 'specs' div with test results, a 'busy' div for the |  | 
|  408  * animated GIF used to indicate tests are running, and a 'child' div to |  | 
|  409  * hold the iframe for the test. |  | 
|  410  */ |  | 
|  411 void _prepareDom() { |  | 
|  412   if (document.query('#control') == null) { |  | 
|  413     // Use this as an opportunity for adding the CSS too. |  | 
|  414     // I wanted to avoid having to include a css element explicitly |  | 
|  415     // in the main html file. I considered moving all the styles |  | 
|  416     // inline as attributes but that started getting very messy, |  | 
|  417     // so we do it this way. |  | 
|  418     document.body.nodes.add(new Element.html("<style>$_CSS</style>")); |  | 
|  419     document.body.nodes.add(new Element.html( |  | 
|  420         "<div id='control'>" |  | 
|  421             "<input id='start' disabled='true' type='button' value='Run'>" |  | 
|  422         "</div>")); |  | 
|  423     document.query('#start').on.click.add((e) { |  | 
|  424       InputElement startButton = document.query('#start'); |  | 
|  425       startButton.disabled = true; |  | 
|  426       rerunTests(); |  | 
|  427     }); |  | 
|  428   } |  | 
|  429   if (document.query('#otherlogs') == null) { |  | 
|  430     document.body.nodes.add(new Element.html( |  | 
|  431         "<div id='otherlogs'></div>")); |  | 
|  432   } |  | 
|  433   if (document.query('#specs') == null) { |  | 
|  434     document.body.nodes.add(new Element.html( |  | 
|  435         "<div id='specs'><div id='group-divs'></div></div>")); |  | 
|  436   } |  | 
|  437   if (document.query('#busy') == null) { |  | 
|  438     document.body.nodes.add(new Element.html( |  | 
|  439         "<div id='busy' style='display:none'><img src='googleballs.gif'>" |  | 
|  440         "</img></div>")); |  | 
|  441   } |  | 
|  442   if (document.query('#child') == null) { |  | 
|  443     document.body.nodes.add(new Element.html("<div id='child'></div>")); |  | 
|  444   } |  | 
|  445 } |  | 
|  446  |  | 
|  447 /** |  | 
|  448  * Allocate a Configuration. We allocate either a parent or |  | 
|  449  * child, depedning on whether the URL has a search part. |  | 
|  450  */ |  | 
|  451 void useInteractiveHtmlConfiguration() { |  | 
|  452   if (window.location.search == '') { // This is the parent. |  | 
|  453     _prepareDom(); |  | 
|  454     configure(new ParentInteractiveHtmlConfiguration()); |  | 
|  455   } else { |  | 
|  456     configure(new ChildInteractiveHtmlConfiguration()); |  | 
|  457   } |  | 
|  458 } |  | 
|  459  |  | 
|  460 String _CSS = """ |  | 
|  461 body { |  | 
|  462 font-family: Arial, sans-serif; |  | 
|  463 margin: 0; |  | 
|  464 font-size: 14px; |  | 
|  465 } |  | 
|  466  |  | 
|  467 #application h2, |  | 
|  468 #specs h2 { |  | 
|  469 margin: 0; |  | 
|  470 padding: 0.5em; |  | 
|  471 font-size: 1.1em; |  | 
|  472 } |  | 
|  473  |  | 
|  474 #header, |  | 
|  475 #application, |  | 
|  476 .test-info, |  | 
|  477 .test-actions li { |  | 
|  478 overflow: hidden; |  | 
|  479 } |  | 
|  480  |  | 
|  481 #application { |  | 
|  482 margin: 10px; |  | 
|  483 } |  | 
|  484  |  | 
|  485 #application iframe { |  | 
|  486 width: 100%; |  | 
|  487 height: 758px; |  | 
|  488 } |  | 
|  489  |  | 
|  490 #application iframe { |  | 
|  491 border: none; |  | 
|  492 } |  | 
|  493  |  | 
|  494 #specs { |  | 
|  495 padding-top: 50px |  | 
|  496 } |  | 
|  497  |  | 
|  498 .test-describe h2 { |  | 
|  499 border-top: 2px solid #BABAD1; |  | 
|  500 background-color: #efefef; |  | 
|  501 } |  | 
|  502  |  | 
|  503 .tests, |  | 
|  504 .test-it ol, |  | 
|  505 .status-display { |  | 
|  506 margin: 0; |  | 
|  507 padding: 0; |  | 
|  508 } |  | 
|  509  |  | 
|  510 .test-info { |  | 
|  511 margin-left: 1em; |  | 
|  512 margin-top: 0.5em; |  | 
|  513 border-radius: 8px 0 0 8px; |  | 
|  514 -webkit-border-radius: 8px 0 0 8px; |  | 
|  515 -moz-border-radius: 8px 0 0 8px; |  | 
|  516 cursor: pointer; |  | 
|  517 } |  | 
|  518  |  | 
|  519 .test-info:hover .test-name { |  | 
|  520 text-decoration: underline; |  | 
|  521 } |  | 
|  522  |  | 
|  523 .test-info .closed:before { |  | 
|  524 content: '\\25b8\\00A0'; |  | 
|  525 } |  | 
|  526  |  | 
|  527 .test-info .open:before { |  | 
|  528 content: '\\25be\\00A0'; |  | 
|  529 font-weight: bold; |  | 
|  530 } |  | 
|  531  |  | 
|  532 .test-it ol { |  | 
|  533 margin-left: 2.5em; |  | 
|  534 } |  | 
|  535  |  | 
|  536 .status-display, |  | 
|  537 .status-display li { |  | 
|  538 float: right; |  | 
|  539 } |  | 
|  540  |  | 
|  541 .status-display li { |  | 
|  542 padding: 5px 10px; |  | 
|  543 } |  | 
|  544  |  | 
|  545 .timer-result, |  | 
|  546 .test-title { |  | 
|  547 display: inline-block; |  | 
|  548 margin: 0; |  | 
|  549 padding: 4px; |  | 
|  550 } |  | 
|  551  |  | 
|  552 .test-actions .test-title, |  | 
|  553 .test-actions .test-result { |  | 
|  554 display: table-cell; |  | 
|  555 padding-left: 0.5em; |  | 
|  556 padding-right: 0.5em; |  | 
|  557 } |  | 
|  558  |  | 
|  559 .test-it { |  | 
|  560 list-style-type: none; |  | 
|  561 } |  | 
|  562  |  | 
|  563 .test-actions { |  | 
|  564 display: table; |  | 
|  565 } |  | 
|  566  |  | 
|  567 .test-actions li { |  | 
|  568 display: table-row; |  | 
|  569 } |  | 
|  570  |  | 
|  571 .timer-result { |  | 
|  572 width: 4em; |  | 
|  573 padding: 0 10px; |  | 
|  574 text-align: right; |  | 
|  575 font-family: monospace; |  | 
|  576 } |  | 
|  577  |  | 
|  578 .test-it pre, |  | 
|  579 .test-actions pre { |  | 
|  580 clear: left; |  | 
|  581 color: black; |  | 
|  582 margin-left: 6em; |  | 
|  583 } |  | 
|  584  |  | 
|  585 .test-describe { |  | 
|  586 margin: 5px 5px 10px 2em; |  | 
|  587 border-left: 1px solid #BABAD1; |  | 
|  588 border-right: 1px solid #BABAD1; |  | 
|  589 border-bottom: 1px solid #BABAD1; |  | 
|  590 padding-bottom: 0.5em; |  | 
|  591 } |  | 
|  592  |  | 
|  593 .test-actions .status-pending .test-title:before { |  | 
|  594 content: \\'\\\\00bb\\\\00A0\\'; |  | 
|  595 } |  | 
|  596  |  | 
|  597 .scrollpane { |  | 
|  598  max-height: 20em; |  | 
|  599  overflow: auto; |  | 
|  600 } |  | 
|  601  |  | 
|  602 #busy { |  | 
|  603 display: block; |  | 
|  604 } |  | 
|  605 /** Colors */ |  | 
|  606  |  | 
|  607 #header { |  | 
|  608 background-color: #F2C200; |  | 
|  609 } |  | 
|  610  |  | 
|  611 #application { |  | 
|  612 border: 1px solid #BABAD1; |  | 
|  613 } |  | 
|  614  |  | 
|  615 .status-pending .test-info { |  | 
|  616 background-color: #F9EEBC; |  | 
|  617 } |  | 
|  618  |  | 
|  619 .status-success .test-info { |  | 
|  620 background-color: #B1D7A1; |  | 
|  621 } |  | 
|  622  |  | 
|  623 .status-failure .test-info { |  | 
|  624 background-color: #FF8286; |  | 
|  625 } |  | 
|  626  |  | 
|  627 .status-error .test-info { |  | 
|  628 background-color: black; |  | 
|  629 color: white; |  | 
|  630 } |  | 
|  631  |  | 
|  632 .test-actions .status-success .test-title { |  | 
|  633 color: #30B30A; |  | 
|  634 } |  | 
|  635  |  | 
|  636 .test-actions .status-failure .test-title { |  | 
|  637 color: #DF0000; |  | 
|  638 } |  | 
|  639  |  | 
|  640 .test-actions .status-error .test-title { |  | 
|  641 color: black; |  | 
|  642 } |  | 
|  643  |  | 
|  644 .test-actions .timer-result { |  | 
|  645 color: #888; |  | 
|  646 } |  | 
|  647  |  | 
|  648 ul, menu, dir { |  | 
|  649 display: block; |  | 
|  650 list-style-type: disc; |  | 
|  651 -webkit-margin-before: 1em; |  | 
|  652 -webkit-margin-after: 1em; |  | 
|  653 -webkit-margin-start: 0px; |  | 
|  654 -webkit-margin-end: 0px; |  | 
|  655 -webkit-padding-start: 40px; |  | 
|  656 } |  | 
|  657  |  | 
|  658   """; |  | 
| OLD | NEW |