| OLD | NEW |
| 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 /** | 5 /** |
| 6 * This configuration can be used to rerun selected tests, as well | 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 | 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' | 8 * IFrame, so the configuration consists of two parts - a 'parent' |
| 9 * config that manages all the tests, and a 'child' config for the | 9 * config that manages all the tests, and a 'child' config for the |
| 10 * IFrame that runs the individual tests. | 10 * IFrame that runs the individual tests. |
| 11 */ | 11 */ |
| 12 library unittest_interactive_html_config; | 12 library unittest_interactive_html_config; |
| 13 | 13 |
| 14 // TODO(gram) - add options for: remove IFrame on done/keep | 14 // TODO(gram) - add options for: remove IFrame on done/keep |
| 15 // IFrame for failed tests/keep IFrame for all tests. | 15 // IFrame for failed tests/keep IFrame for all tests. |
| 16 | 16 |
| 17 import 'dart:html'; | 17 import 'dart:html'; |
| 18 import 'dart:async'; |
| 18 import 'dart:math'; | 19 import 'dart:math'; |
| 19 import 'unittest.dart'; | 20 import 'unittest.dart'; |
| 20 | 21 |
| 21 /** The messages exchanged between parent and child. */ | 22 /** The messages exchanged between parent and child. */ |
| 22 | 23 |
| 23 class _Message { | 24 class _Message { |
| 24 static const START = 'start'; | 25 static const START = 'start'; |
| 25 static const LOG = 'log'; | 26 static const LOG = 'log'; |
| 26 static const STACK = 'stack'; | 27 static const STACK = 'stack'; |
| 27 static const PASS = 'pass'; | 28 static const PASS = 'pass'; |
| (...skipping 18 matching lines...) Expand all Loading... |
| 46 elapsed = int.parse(msg.substring(idx, idx2)); | 47 elapsed = int.parse(msg.substring(idx, idx2)); |
| 47 ++idx2; | 48 ++idx2; |
| 48 body = msg.substring(idx2); | 49 body = msg.substring(idx2); |
| 49 } | 50 } |
| 50 | 51 |
| 51 String toString() => text(messageType, elapsed, body); | 52 String toString() => text(messageType, elapsed, body); |
| 52 } | 53 } |
| 53 | 54 |
| 54 | 55 |
| 55 class HtmlConfiguration extends Configuration { | 56 class HtmlConfiguration extends Configuration { |
| 56 // TODO(rnystrom): Get rid of this if we get canonical closures for methods. | 57 StreamSubscription _errorSubscription; |
| 57 EventListener _onErrorClosure; | |
| 58 | 58 |
| 59 void _installErrorHandler() { | 59 void _installErrorHandler() { |
| 60 if (_onErrorClosure == null) { | 60 if (_errorSubscription == null) { |
| 61 _onErrorClosure = | 61 _errorSubscription = window.onError.listen((e) { |
| 62 (e) => handleExternalError(e, '(DOM callback has errors)'); | 62 handleExternalError(e, '(DOM callback has errors)'); |
| 63 // Listen for uncaught errors. | 63 }); |
| 64 window.on.error.add(_onErrorClosure); | |
| 65 } | 64 } |
| 66 } | 65 } |
| 67 | 66 |
| 68 void _uninstallErrorHandler() { | 67 void _uninstallErrorHandler() { |
| 69 if (_onErrorClosure != null) { | 68 if (_errorSubscription != null) { |
| 70 window.on.error.remove(_onErrorClosure); | 69 _errorSubscription.cancel(); |
| 71 _onErrorClosure = null; | 70 _errorSubscription = null; |
| 72 } | 71 } |
| 73 } | 72 } |
| 74 } | 73 } |
| 75 | 74 |
| 76 /** | 75 /** |
| 77 * The child configuration that is used to run individual tests in | 76 * The child configuration that is used to run individual tests in |
| 78 * an IFrame and post the results back to the parent. In principle | 77 * an IFrame and post the results back to the parent. In principle |
| 79 * this can run more than one test in the IFrame but currently only | 78 * this can run more than one test in the IFrame but currently only |
| 80 * one is used. | 79 * one is used. |
| 81 */ | 80 */ |
| 82 class ChildInteractiveHtmlConfiguration extends HtmlConfiguration { | 81 class ChildInteractiveHtmlConfiguration extends HtmlConfiguration { |
| 83 | 82 |
| 84 /** The window to which results must be posted. */ | 83 /** The window to which results must be posted. */ |
| 85 Window parentWindow; | 84 WindowBase parentWindow; |
| 86 | 85 |
| 87 /** The time at which tests start. */ | 86 /** The time at which tests start. */ |
| 88 Map<int,DateTime> _testStarts; | 87 Map<int,DateTime> _testStarts; |
| 89 | 88 |
| 90 ChildInteractiveHtmlConfiguration() : | 89 ChildInteractiveHtmlConfiguration() : |
| 91 _testStarts = new Map<int,DateTime>(); | 90 _testStarts = new Map<int,DateTime>(); |
| 92 | 91 |
| 93 /** Don't start running tests automatically. */ | 92 /** Don't start running tests automatically. */ |
| 94 get autoStart => false; | 93 get autoStart => false; |
| 95 | 94 |
| 96 void onInit() { | 95 void onInit() { |
| 97 _installErrorHandler(); | 96 _installErrorHandler(); |
| 98 | 97 |
| 99 /** | 98 /** |
| 100 * The parent posts a 'start' message to kick things off, | 99 * The parent posts a 'start' message to kick things off, |
| 101 * which is handled by this handler. It saves the parent | 100 * which is handled by this handler. It saves the parent |
| 102 * window, gets the test ID from the query parameter in the | 101 * window, gets the test ID from the query parameter in the |
| 103 * IFrame URL, sets that as a solo test and starts test execution. | 102 * IFrame URL, sets that as a solo test and starts test execution. |
| 104 */ | 103 */ |
| 105 window.on.message.add((MessageEvent e) { | 104 window.onMessage.listen((MessageEvent e) { |
| 106 // Get the result, do any logging, then do a pass/fail. | 105 // Get the result, do any logging, then do a pass/fail. |
| 107 var m = new _Message.fromString(e.data); | 106 var m = new _Message.fromString(e.data); |
| 108 if (m.messageType == _Message.START) { | 107 if (m.messageType == _Message.START) { |
| 109 parentWindow = e.source; | 108 parentWindow = e.source; |
| 110 String search = window.location.search; | 109 String search = window.location.search; |
| 111 int pos = search.indexOf('t='); | 110 int pos = search.indexOf('t='); |
| 112 String ids = search.substring(pos+2); | 111 String ids = search.substring(pos+2); |
| 113 int id = int.parse(ids); | 112 int id = int.parse(ids); |
| 114 setSoloTest(id); | 113 setSoloTest(id); |
| 115 runTests(); | 114 runTests(); |
| (...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 181 String _stack; | 180 String _stack; |
| 182 | 181 |
| 183 int _testTime; | 182 int _testTime; |
| 184 /** | 183 /** |
| 185 * Whether or not we have already wrapped the TestCase test functions | 184 * Whether or not we have already wrapped the TestCase test functions |
| 186 * in new closures that instead create an IFrame and get it to run the | 185 * in new closures that instead create an IFrame and get it to run the |
| 187 * test. | 186 * test. |
| 188 */ | 187 */ |
| 189 bool _doneWrap = false; | 188 bool _doneWrap = false; |
| 190 | 189 |
| 191 /** | 190 StreamSubscription _messageSubscription; |
| 192 * We use this to make a single closure from _handleMessage so we | |
| 193 * can remove the handler later. | |
| 194 */ | |
| 195 Function _messageHandler; | |
| 196 | 191 |
| 197 ParentInteractiveHtmlConfiguration() : | 192 ParentInteractiveHtmlConfiguration() : |
| 198 _testStarts = new Map<int,DateTime>(); | 193 _testStarts = new Map<int,DateTime>(); |
| 199 | 194 |
| 200 // We need to block until the test is done, so we make a | 195 // We need to block until the test is done, so we make a |
| 201 // dummy async callback that we will use to flag completion. | 196 // dummy async callback that we will use to flag completion. |
| 202 Function completeTest = null; | 197 Function completeTest = null; |
| 203 | 198 |
| 204 wrapTest(TestCase testCase) { | 199 wrapTest(TestCase testCase) { |
| 205 String baseUrl = window.location.toString(); | 200 String baseUrl = window.location.toString(); |
| 206 String url = '${baseUrl}?t=${testCase.id}'; | 201 String url = '${baseUrl}?t=${testCase.id}'; |
| 207 return () { | 202 return () { |
| 208 // Rebuild the child IFrame. | 203 // Rebuild the child IFrame. |
| 209 Element childDiv = document.query('#child'); | 204 Element childDiv = document.query('#child'); |
| 210 childDiv.nodes.clear(); | 205 childDiv.nodes.clear(); |
| 211 IFrameElement child = new Element.html(""" | 206 IFrameElement child = new Element.html(""" |
| 212 <iframe id='childFrame${testCase.id}' src='$url' style='display:none'> | 207 <iframe id='childFrame${testCase.id}' src='$url' style='display:none'> |
| 213 </iframe>"""); | 208 </iframe>"""); |
| 214 childDiv.nodes.add(child); | 209 childDiv.nodes.add(child); |
| 215 completeTest = expectAsync0((){ }); | 210 completeTest = expectAsync0((){ }); |
| 216 // Kick off the test when the IFrame is loaded. | 211 // Kick off the test when the IFrame is loaded. |
| 217 child.on.load.add((e) { | 212 child.onLoad.listen((e) { |
| 218 child.contentWindow.postMessage(_Message.text(_Message.START), '*'); | 213 child.contentWindow.postMessage(_Message.text(_Message.START), '*'); |
| 219 }); | 214 }); |
| 220 }; | 215 }; |
| 221 } | 216 } |
| 222 | 217 |
| 223 void _handleMessage(MessageEvent e) { | 218 void _handleMessage(MessageEvent e) { |
| 224 // Get the result, do any logging, then do a pass/fail. | 219 // Get the result, do any logging, then do a pass/fail. |
| 225 var msg = new _Message.fromString(e.data); | 220 var msg = new _Message.fromString(e.data); |
| 226 if (msg.messageType == _Message.LOG) { | 221 if (msg.messageType == _Message.LOG) { |
| 227 logMessage(e.data); | 222 logMessage(e.data); |
| 228 } else if (msg.messageType == _Message.STACK) { | 223 } else if (msg.messageType == _Message.STACK) { |
| 229 _stack = msg.body; | 224 _stack = msg.body; |
| 230 } else { | 225 } else { |
| 231 _testTime = msg.elapsed; | 226 _testTime = msg.elapsed; |
| 232 logMessage(_Message.text(_Message.LOG, _testTime, 'Complete')); | 227 logMessage(_Message.text(_Message.LOG, _testTime, 'Complete')); |
| 233 if (msg.messageType == _Message.PASS) { | 228 if (msg.messageType == _Message.PASS) { |
| 234 currentTestCase.pass(); | 229 currentTestCase.pass(); |
| 235 } else if (msg.messageType == _Message.FAIL) { | 230 } else if (msg.messageType == _Message.FAIL) { |
| 236 currentTestCase.fail(msg.body, _stack); | 231 currentTestCase.fail(msg.body, _stack); |
| 237 } else if (msg.messageType == _Message.ERROR) { | 232 } else if (msg.messageType == _Message.ERROR) { |
| 238 currentTestCase.error(msg.body, _stack); | 233 currentTestCase.error(msg.body, _stack); |
| 239 } | 234 } |
| 240 completeTest(); | 235 completeTest(); |
| 241 } | 236 } |
| 242 } | 237 } |
| 243 | 238 |
| 244 void onInit() { | 239 void onInit() { |
| 245 _installErrorHandler(); | 240 _installErrorHandler(); |
| 246 _messageHandler = _handleMessage; // We need to make just one closure. | |
| 247 document.query('#group-divs').innerHtml = ""; | 241 document.query('#group-divs').innerHtml = ""; |
| 248 } | 242 } |
| 249 | 243 |
| 250 void onStart() { | 244 void onStart() { |
| 251 _installErrorHandler(); | 245 _installErrorHandler(); |
| 252 if (!_doneWrap) { | 246 if (!_doneWrap) { |
| 253 _doneWrap = true; | 247 _doneWrap = true; |
| 254 for (int i = 0; i < testCases.length; i++) { | 248 for (int i = 0; i < testCases.length; i++) { |
| 255 testCases[i].test = wrapTest(testCases[i]); | 249 testCases[i].test = wrapTest(testCases[i]); |
| 256 testCases[i].setUp = null; | 250 testCases[i].setUp = null; |
| 257 testCases[i].tearDown = null; | 251 testCases[i].tearDown = null; |
| 258 } | 252 } |
| 259 } | 253 } |
| 260 window.on.message.add(_messageHandler); | 254 assert(_messageSubscription == null); |
| 255 _messageSubscription = window.onMessage.listen(_handleMessage); |
| 261 } | 256 } |
| 262 | 257 |
| 263 static final _notAlphaNumeric = new RegExp('[^a-z0-9A-Z]'); | 258 static final _notAlphaNumeric = new RegExp('[^a-z0-9A-Z]'); |
| 264 | 259 |
| 265 String _stringToDomId(String s) { | 260 String _stringToDomId(String s) { |
| 266 if (s.length == 0) { | 261 if (s.length == 0) { |
| 267 return '-None-'; | 262 return '-None-'; |
| 268 } | 263 } |
| 269 return s.trim().replaceAll(_notAlphaNumeric, '-'); | 264 return s.trim().replaceAll(_notAlphaNumeric, '-'); |
| 270 } | 265 } |
| (...skipping 19 matching lines...) Expand all Loading... |
| 290 groupDiv = new Element.html(""" | 285 groupDiv = new Element.html(""" |
| 291 <div class='test-describe' id='$groupId'> | 286 <div class='test-describe' id='$groupId'> |
| 292 <h2> | 287 <h2> |
| 293 <input type='checkbox' checked='true' class='groupselect'> | 288 <input type='checkbox' checked='true' class='groupselect'> |
| 294 Group: ${testCase.currentGroup} | 289 Group: ${testCase.currentGroup} |
| 295 </h2> | 290 </h2> |
| 296 <ul class='tests'> | 291 <ul class='tests'> |
| 297 </ul> | 292 </ul> |
| 298 </div>"""); | 293 </div>"""); |
| 299 document.query('#group-divs').nodes.add(groupDiv); | 294 document.query('#group-divs').nodes.add(groupDiv); |
| 300 groupDiv.query('.groupselect').on.click.add((e) { | 295 groupDiv.query('.groupselect').onClick.listen((e) { |
| 301 var parent = document.query('#$groupId'); | 296 var parent = document.query('#$groupId'); |
| 302 InputElement cb = parent.query('.groupselect'); | 297 InputElement cb = parent.query('.groupselect'); |
| 303 var state = cb.checked; | 298 var state = cb.checked; |
| 304 var tests = parent.query('.tests'); | 299 var tests = parent.query('.tests'); |
| 305 for (Element t in tests.elements) { | 300 for (Element t in tests.children) { |
| 306 cb = t.query('.testselect') as InputElement; | 301 cb = t.query('.testselect') as InputElement; |
| 307 cb.checked = state; | 302 cb.checked = state; |
| 308 var testId = int.parse(t.id.substring(_testIdPrefix.length)); | 303 var testId = int.parse(t.id.substring(_testIdPrefix.length)); |
| 309 if (state) { | 304 if (state) { |
| 310 enableTest(testId); | 305 enableTest(testId); |
| 311 } else { | 306 } else { |
| 312 disableTest(testId); | 307 disableTest(testId); |
| 313 } | 308 } |
| 314 } | 309 } |
| 315 }); | 310 }); |
| (...skipping 12 matching lines...) Expand all Loading... |
| 328 <span class='timer-result test-timer-result'></span> | 323 <span class='timer-result test-timer-result'></span> |
| 329 <span class='test-name closed'>${testCase.description}</span> | 324 <span class='test-name closed'>${testCase.description}</span> |
| 330 </span> | 325 </span> |
| 331 </p> | 326 </p> |
| 332 </div> | 327 </div> |
| 333 <div class='scrollpane'> | 328 <div class='scrollpane'> |
| 334 <ol class='test-actions' id='$_actionIdPrefix$id'></ol> | 329 <ol class='test-actions' id='$_actionIdPrefix$id'></ol> |
| 335 </div> | 330 </div> |
| 336 </li>"""); | 331 </li>"""); |
| 337 list.nodes.add(testItem); | 332 list.nodes.add(testItem); |
| 338 testItem.query('#$_selectedIdPrefix$id').on.change.add((e) { | 333 testItem.query('#$_selectedIdPrefix$id').onChange.listen((e) { |
| 339 InputElement cb = testItem.query('#$_selectedIdPrefix$id'); | 334 InputElement cb = testItem.query('#$_selectedIdPrefix$id'); |
| 340 testCase.enabled = cb.checked; | 335 testCase.enabled = cb.checked; |
| 341 }); | 336 }); |
| 342 testItem.query('.test-label').on.click.add((e) { | 337 testItem.query('.test-label').onClick.listen((e) { |
| 343 var _testItem = document.query('#$_testIdPrefix$id'); | 338 var _testItem = document.query('#$_testIdPrefix$id'); |
| 344 var _actions = _testItem.query('#$_actionIdPrefix$id'); | 339 var _actions = _testItem.query('#$_actionIdPrefix$id'); |
| 345 var _label = _testItem.query('.test-name'); | 340 var _label = _testItem.query('.test-name'); |
| 346 if (_actions.style.display == 'none') { | 341 if (_actions.style.display == 'none') { |
| 347 _actions.style.display = 'table'; | 342 _actions.style.display = 'table'; |
| 348 _label.classes.remove('closed'); | 343 _label.classes.remove('closed'); |
| 349 _label.classes.add('open'); | 344 _label.classes.add('open'); |
| 350 } else { | 345 } else { |
| 351 _actions.style.display = 'none'; | 346 _actions.style.display = 'none'; |
| 352 _label.classes.remove('open'); | 347 _label.classes.remove('open'); |
| (...skipping 54 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 407 e.classes.add(result); | 402 e.classes.add(result); |
| 408 } | 403 } |
| 409 actions.style.display = 'none'; | 404 actions.style.display = 'none'; |
| 410 } | 405 } |
| 411 | 406 |
| 412 void onSummary(int passed, int failed, int errors, List<TestCase> results, | 407 void onSummary(int passed, int failed, int errors, List<TestCase> results, |
| 413 String uncaughtError) { | 408 String uncaughtError) { |
| 414 } | 409 } |
| 415 | 410 |
| 416 void onDone(bool success) { | 411 void onDone(bool success) { |
| 417 window.on.message.remove(_messageHandler); | 412 assert(_messageSubscription != null); |
| 413 _messageSubscription.cancel(); |
| 414 _messageSubscription = null; |
| 418 _uninstallErrorHandler(); | 415 _uninstallErrorHandler(); |
| 419 document.query('#busy').style.display = 'none'; | 416 document.query('#busy').style.display = 'none'; |
| 420 InputElement startButton = document.query('#start'); | 417 InputElement startButton = document.query('#start'); |
| 421 startButton.disabled = false; | 418 startButton.disabled = false; |
| 422 } | 419 } |
| 423 } | 420 } |
| 424 | 421 |
| 425 /** | 422 /** |
| 426 * Add the divs to the DOM if they are not present. We have a 'controls' | 423 * Add the divs to the DOM if they are not present. We have a 'controls' |
| 427 * div for control, 'specs' div with test results, a 'busy' div for the | 424 * div for control, 'specs' div with test results, a 'busy' div for the |
| 428 * animated GIF used to indicate tests are running, and a 'child' div to | 425 * animated GIF used to indicate tests are running, and a 'child' div to |
| 429 * hold the iframe for the test. | 426 * hold the iframe for the test. |
| 430 */ | 427 */ |
| 431 void _prepareDom() { | 428 void _prepareDom() { |
| 432 if (document.query('#control') == null) { | 429 if (document.query('#control') == null) { |
| 433 // Use this as an opportunity for adding the CSS too. | 430 // Use this as an opportunity for adding the CSS too. |
| 434 // I wanted to avoid having to include a css element explicitly | 431 // I wanted to avoid having to include a css element explicitly |
| 435 // in the main html file. I considered moving all the styles | 432 // in the main html file. I considered moving all the styles |
| 436 // inline as attributes but that started getting very messy, | 433 // inline as attributes but that started getting very messy, |
| 437 // so we do it this way. | 434 // so we do it this way. |
| 438 document.body.nodes.add(new Element.html("<style>$_CSS</style>")); | 435 document.body.nodes.add(new Element.html("<style>$_CSS</style>")); |
| 439 document.body.nodes.add(new Element.html( | 436 document.body.nodes.add(new Element.html( |
| 440 "<div id='control'>" | 437 "<div id='control'>" |
| 441 "<input id='start' disabled='true' type='button' value='Run'>" | 438 "<input id='start' disabled='true' type='button' value='Run'>" |
| 442 "</div>")); | 439 "</div>")); |
| 443 document.query('#start').on.click.add((e) { | 440 document.query('#start').onClick.listen((e) { |
| 444 InputElement startButton = document.query('#start'); | 441 InputElement startButton = document.query('#start'); |
| 445 startButton.disabled = true; | 442 startButton.disabled = true; |
| 446 rerunTests(); | 443 rerunTests(); |
| 447 }); | 444 }); |
| 448 } | 445 } |
| 449 if (document.query('#otherlogs') == null) { | 446 if (document.query('#otherlogs') == null) { |
| 450 document.body.nodes.add(new Element.html( | 447 document.body.nodes.add(new Element.html( |
| 451 "<div id='otherlogs'></div>")); | 448 "<div id='otherlogs'></div>")); |
| 452 } | 449 } |
| 453 if (document.query('#specs') == null) { | 450 if (document.query('#specs') == null) { |
| 454 document.body.nodes.add(new Element.html( | 451 document.body.nodes.add(new Element.html( |
| 455 "<div id='specs'><div id='group-divs'></div></div>")); | 452 "<div id='specs'><div id='group-divs'></div></div>")); |
| 456 } | 453 } |
| 457 if (document.query('#busy') == null) { | 454 if (document.query('#busy') == null) { |
| 458 document.body.nodes.add(new Element.html( | 455 document.body.nodes.add(new Element.html( |
| 459 "<div id='busy' style='display:none'><img src='googleballs.gif'>" | 456 "<div id='busy' style='display:none'><img src='googleballs.gif'>" |
| 460 "</img></div>")); | 457 "</img></div>")); |
| 461 } | 458 } |
| 462 if (document.query('#child') == null) { | 459 if (document.query('#child') == null) { |
| 463 document.body.nodes.add(new Element.html("<div id='child'></div>")); | 460 document.body.nodes.add(new Element.html("<div id='child'></div>")); |
| 464 } | 461 } |
| 465 } | 462 } |
| 466 | 463 |
| 467 /** | 464 /** |
| 468 * Allocate a Configuration. We allocate either a parent or | 465 * Allocate a Configuration. We allocate either a parent or |
| 469 * child, depedning on whether the URL has a search part. | 466 * child, depending on whether the URL has a search part. |
| 470 */ | 467 */ |
| 471 void useInteractiveHtmlConfiguration() { | 468 void useInteractiveHtmlConfiguration() { |
| 472 if (config != null) return; | 469 if (config != null) return; |
| 473 if (window.location.search == '') { // This is the parent. | 470 if (window.location.search == '') { // This is the parent. |
| 474 _prepareDom(); | 471 _prepareDom(); |
| 475 configure(new ParentInteractiveHtmlConfiguration()); | 472 configure(new ParentInteractiveHtmlConfiguration()); |
| 476 } else { | 473 } else { |
| 477 configure(new ChildInteractiveHtmlConfiguration()); | 474 configure(new ChildInteractiveHtmlConfiguration()); |
| 478 } | 475 } |
| 479 } | 476 } |
| (...skipping 190 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 670 display: block; | 667 display: block; |
| 671 list-style-type: disc; | 668 list-style-type: disc; |
| 672 -webkit-margin-before: 1em; | 669 -webkit-margin-before: 1em; |
| 673 -webkit-margin-after: 1em; | 670 -webkit-margin-after: 1em; |
| 674 -webkit-margin-start: 0px; | 671 -webkit-margin-start: 0px; |
| 675 -webkit-margin-end: 0px; | 672 -webkit-margin-end: 0px; |
| 676 -webkit-padding-start: 40px; | 673 -webkit-padding-start: 40px; |
| 677 } | 674 } |
| 678 | 675 |
| 679 """; | 676 """; |
| OLD | NEW |