d1f29d7648d141ee0c125c4b8946356542fc73e0
[platform/framework/web/crosswalk.git] / src / chrome / browser / test_presubmit.py
1 #!/usr/bin/env python
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
5
6 """Unit tests for Web Development Style Guide checker."""
7
8 import os
9 import re
10 import sys
11 import unittest
12
13 test_dir = os.path.dirname(os.path.abspath(__file__))
14 sys.path.extend([
15     os.path.normpath(os.path.join(test_dir, '..', '..', 'tools')),
16     os.path.join(test_dir),
17 ])
18
19 import find_depot_tools # pylint: disable=W0611
20 from testing_support.super_mox import SuperMoxTestBase
21 from web_dev_style import resource_checker, css_checker, js_checker # pylint: disable=F0401
22
23
24 def GetHighlight(line, error):
25   """Returns the substring of |line| that is highlighted in |error|."""
26   error_lines = error.split('\n')
27   highlight = error_lines[error_lines.index(line) + 1]
28   return ''.join(ch1 for (ch1, ch2) in zip(line, highlight) if ch2 == '^')
29
30
31 class ResourceStyleGuideTest(SuperMoxTestBase):
32   def setUp(self):
33     SuperMoxTestBase.setUp(self)
34
35     input_api = self.mox.CreateMockAnything()
36     input_api.re = re
37     output_api = self.mox.CreateMockAnything()
38     self.checker = resource_checker.ResourceChecker(input_api, output_api)
39
40   def ShouldFailIncludeCheck(self, line):
41     """Checks that the '</include>' checker flags |line| as a style error."""
42     error = self.checker.IncludeCheck(1, line)
43     self.assertNotEqual('', error,
44         'Should be flagged as style error: ' + line)
45     highlight = GetHighlight(line, error).strip()
46     self.assertTrue('include' in highlight and highlight[0] == '<')
47
48   def ShouldPassIncludeCheck(self, line):
49     """Checks that the '</include>' checker doesn't flag |line| as an error."""
50     self.assertEqual('', self.checker.IncludeCheck(1, line),
51         'Should not be flagged as style error: ' + line)
52
53   def testIncludeFails(self):
54     lines = [
55         "</include>   ",
56         "    </include>",
57         "    </include>   ",
58         '  <include src="blah.js" />   ',
59         '<include src="blee.js"/>',
60     ]
61     for line in lines:
62       self.ShouldFailIncludeCheck(line)
63
64   def testIncludePasses(self):
65     lines = [
66         '<include src="assert.js">',
67         "<include src='../../assert.js'>",
68         "<i>include src='blah'</i>",
69         "</i>nclude",
70         "</i>include",
71     ]
72     for line in lines:
73       self.ShouldPassIncludeCheck(line)
74
75
76 class JsStyleGuideTest(SuperMoxTestBase):
77   def setUp(self):
78     SuperMoxTestBase.setUp(self)
79
80     input_api = self.mox.CreateMockAnything()
81     input_api.re = re
82     output_api = self.mox.CreateMockAnything()
83     self.checker = js_checker.JSChecker(input_api, output_api)
84
85   def ShouldFailConstCheck(self, line):
86     """Checks that the 'const' checker flags |line| as a style error."""
87     error = self.checker.ConstCheck(1, line)
88     self.assertNotEqual('', error,
89         'Should be flagged as style error: ' + line)
90     self.assertEqual(GetHighlight(line, error), 'const')
91
92   def ShouldPassConstCheck(self, line):
93     """Checks that the 'const' checker doesn't flag |line| as a style error."""
94     self.assertEqual('', self.checker.ConstCheck(1, line),
95         'Should not be flagged as style error: ' + line)
96
97   def testConstFails(self):
98     lines = [
99         "const foo = 'bar';",
100         "    const bar = 'foo';",
101
102         # Trying to use |const| as a variable name
103         "var const = 0;",
104
105         "var x = 5; const y = 6;",
106         "for (var i=0, const e=10; i<e; i++) {",
107         "for (const x=0; x<foo; i++) {",
108         "while (const x = 7) {",
109     ]
110     for line in lines:
111       self.ShouldFailConstCheck(line)
112
113   def testConstPasses(self):
114     lines = [
115         # sanity check
116         "var foo = 'bar'",
117
118         # @const JsDoc tag
119         "/** @const */ var SEVEN = 7;",
120
121         # @const tag in multi-line comment
122         " * @const",
123         "   * @const",
124
125         # @constructor tag in multi-line comment
126         " * @constructor",
127         "   * @constructor",
128
129         # words containing 'const'
130         "if (foo.constructor) {",
131         "var deconstruction = 'something';",
132         "var madeUpWordconst = 10;",
133
134         # Strings containing the word |const|
135         "var str = 'const at the beginning';",
136         "var str = 'At the end: const';",
137
138         # doing this one with regex is probably not practical
139         #"var str = 'a const in the middle';",
140     ]
141     for line in lines:
142       self.ShouldPassConstCheck(line)
143
144   def ShouldFailChromeSendCheck(self, line):
145     """Checks that the 'chrome.send' checker flags |line| as a style error."""
146     error = self.checker.ChromeSendCheck(1, line)
147     self.assertNotEqual('', error,
148         'Should be flagged as style error: ' + line)
149     self.assertEqual(GetHighlight(line, error), ', []')
150
151   def ShouldPassChromeSendCheck(self, line):
152     """Checks that the 'chrome.send' checker doesn't flag |line| as a style
153        error.
154     """
155     self.assertEqual('', self.checker.ChromeSendCheck(1, line),
156         'Should not be flagged as style error: ' + line)
157
158   def testChromeSendFails(self):
159     lines = [
160         "chrome.send('message', []);",
161         "  chrome.send('message', []);",
162     ]
163     for line in lines:
164       self.ShouldFailChromeSendCheck(line)
165
166   def testChromeSendPasses(self):
167     lines = [
168         "chrome.send('message', constructArgs('foo', []));",
169         "  chrome.send('message', constructArgs('foo', []));",
170         "chrome.send('message', constructArgs([]));",
171         "  chrome.send('message', constructArgs([]));",
172     ]
173     for line in lines:
174       self.ShouldPassChromeSendCheck(line)
175
176   def ShouldFailEndJsDocCommentCheck(self, line):
177     """Checks that the **/ checker flags |line| as a style error."""
178     error = self.checker.EndJsDocCommentCheck(1, line)
179     self.assertNotEqual('', error,
180         'Should be flagged as style error: ' + line)
181     self.assertEqual(GetHighlight(line, error), '**/')
182
183   def ShouldPassEndJsDocCommentCheck(self, line):
184     """Checks that the **/ checker doesn't flag |line| as a style error."""
185     self.assertEqual('', self.checker.EndJsDocCommentCheck(1, line),
186         'Should not be flagged as style error: ' + line)
187
188   def testEndJsDocCommentFails(self):
189     lines = [
190         "/** @override **/",
191         "/** @type {number} @const **/",
192         "  **/",
193         "**/  ",
194     ]
195     for line in lines:
196       self.ShouldFailEndJsDocCommentCheck(line)
197
198   def testEndJsDocCommentPasses(self):
199     lines = [
200         "/***************/",  # visual separators
201         "  */",  # valid JSDoc comment ends
202         "*/  ",
203         "/**/",  # funky multi-line comment enders
204         "/** @override */",  # legit JSDoc one-liners
205     ]
206     for line in lines:
207       self.ShouldPassEndJsDocCommentCheck(line)
208
209   def ShouldFailGetElementByIdCheck(self, line):
210     """Checks that the 'getElementById' checker flags |line| as a style
211        error.
212     """
213     error = self.checker.GetElementByIdCheck(1, line)
214     self.assertNotEqual('', error,
215         'Should be flagged as style error: ' + line)
216     self.assertEqual(GetHighlight(line, error), 'document.getElementById')
217
218   def ShouldPassGetElementByIdCheck(self, line):
219     """Checks that the 'getElementById' checker doesn't flag |line| as a style
220        error.
221     """
222     self.assertEqual('', self.checker.GetElementByIdCheck(1, line),
223         'Should not be flagged as style error: ' + line)
224
225   def testGetElementByIdFails(self):
226     lines = [
227         "document.getElementById('foo');",
228         "  document.getElementById('foo');",
229         "var x = document.getElementById('foo');",
230         "if (document.getElementById('foo').hidden) {",
231     ]
232     for line in lines:
233       self.ShouldFailGetElementByIdCheck(line)
234
235   def testGetElementByIdPasses(self):
236     lines = [
237         "elem.ownerDocument.getElementById('foo');",
238         "  elem.ownerDocument.getElementById('foo');",
239         "var x = elem.ownerDocument.getElementById('foo');",
240         "if (elem.ownerDocument.getElementById('foo').hidden) {",
241         "doc.getElementById('foo');",
242         "  doc.getElementById('foo');",
243         "cr.doc.getElementById('foo');",
244         "  cr.doc.getElementById('foo');",
245         "var x = doc.getElementById('foo');",
246         "if (doc.getElementById('foo').hidden) {",
247     ]
248     for line in lines:
249       self.ShouldPassGetElementByIdCheck(line)
250
251   def ShouldFailInheritDocCheck(self, line):
252     """Checks that the '@inheritDoc' checker flags |line| as a style error."""
253     error = self.checker.InheritDocCheck(1, line)
254     self.assertNotEqual('', error,
255         msg='Should be flagged as style error: ' + line)
256     self.assertEqual(GetHighlight(line, error), '@inheritDoc')
257
258   def ShouldPassInheritDocCheck(self, line):
259     """Checks that the '@inheritDoc' checker doesn't flag |line| as a style
260        error.
261     """
262     self.assertEqual('', self.checker.InheritDocCheck(1, line),
263         msg='Should not be flagged as style error: ' + line)
264
265   def testInheritDocFails(self):
266     lines = [
267         " /** @inheritDoc */",
268         "   * @inheritDoc",
269     ]
270     for line in lines:
271       self.ShouldFailInheritDocCheck(line)
272
273   def testInheritDocPasses(self):
274     lines = [
275         "And then I said, but I won't @inheritDoc! Hahaha!",
276         " If your dad's a doctor, do you inheritDoc?",
277         "  What's up, inherit doc?",
278         "   this.inheritDoc(someDoc)",
279     ]
280     for line in lines:
281       self.ShouldPassInheritDocCheck(line)
282
283   def ShouldFailWrapperTypeCheck(self, line):
284     """Checks that the use of wrapper types (i.e. new Number(), @type {Number})
285        is a style error.
286     """
287     error = self.checker.WrapperTypeCheck(1, line)
288     self.assertNotEqual('', error,
289         msg='Should be flagged as style error: ' + line)
290     highlight = GetHighlight(line, error)
291     self.assertTrue(highlight in ('Boolean', 'Number', 'String'))
292
293   def ShouldPassWrapperTypeCheck(self, line):
294     """Checks that the wrapper type checker doesn't flag |line| as a style
295        error.
296     """
297     self.assertEqual('', self.checker.WrapperTypeCheck(1, line),
298         msg='Should not be flagged as style error: ' + line)
299
300   def testWrapperTypePasses(self):
301     lines = [
302         "/** @param {!ComplexType} */",
303         "  * @type {Object}",
304         "   * @param {Function=} opt_callback",
305         "    * @param {} num Number of things to add to {blah}.",
306         "   *  @return {!print_preview.PageNumberSet}",
307         " /* @returns {Number} */",  # Should be /** @return {Number} */
308         "* @param {!LocalStrings}"
309         " Your type of Boolean is false!",
310         "  Then I parameterized her Number from her friend!",
311         "   A String of Pearls",
312         "    types.params.aBoolean.typeString(someNumber)",
313     ]
314     for line in lines:
315       self.ShouldPassWrapperTypeCheck(line)
316
317   def testWrapperTypeFails(self):
318     lines = [
319         "  /**@type {String}*/(string)",
320         "   * @param{Number=} opt_blah A number",
321         "/** @private @return {!Boolean} */",
322         " * @param {number|String}",
323     ]
324     for line in lines:
325       self.ShouldFailWrapperTypeCheck(line)
326
327   def ShouldFailVarNameCheck(self, line):
328     """Checks that var unix_hacker, $dollar are style errors."""
329     error = self.checker.VarNameCheck(1, line)
330     self.assertNotEqual('', error,
331         msg='Should be flagged as style error: ' + line)
332     highlight = GetHighlight(line, error)
333     self.assertFalse('var ' in highlight);
334
335   def ShouldPassVarNameCheck(self, line):
336     """Checks that variableNamesLikeThis aren't style errors."""
337     self.assertEqual('', self.checker.VarNameCheck(1, line),
338         msg='Should not be flagged as style error: ' + line)
339
340   def testVarNameFails(self):
341     lines = [
342         "var private_;",
343         " var _super_private",
344         "  var unix_hacker = someFunc();",
345     ]
346     for line in lines:
347       self.ShouldFailVarNameCheck(line)
348
349   def testVarNamePasses(self):
350     lines = [
351         "  var namesLikeThis = [];",
352         " for (var i = 0; i < 10; ++i) { ",
353         "for (var i in obj) {",
354         " var one, two, three;",
355         "  var magnumPI = {};",
356         " var g_browser = 'da browzer';",
357         "/** @const */ var Bla = options.Bla;",  # goog.scope() replacement.
358         " var $ = function() {",                 # For legacy reasons.
359         "  var StudlyCaps = cr.define('bla')",   # Classes.
360         " var SCARE_SMALL_CHILDREN = [",         # TODO(dbeam): add @const in
361                                                  # front of all these vars like
362         "/** @const */ CONST_VAR = 1;",          # this line has (<--).
363     ]
364     for line in lines:
365       self.ShouldPassVarNameCheck(line)
366
367
368 class ClosureLintTest(SuperMoxTestBase):
369   def setUp(self):
370     SuperMoxTestBase.setUp(self)
371
372     input_api = self.mox.CreateMockAnything()
373     input_api.os_path = os.path
374     input_api.re = re
375
376     input_api.change = self.mox.CreateMockAnything()
377     self.mox.StubOutWithMock(input_api.change, 'RepositoryRoot')
378     src_root = os.path.join(os.path.dirname(__file__), '..', '..')
379     input_api.change.RepositoryRoot().MultipleTimes().AndReturn(src_root)
380
381     output_api = self.mox.CreateMockAnything()
382
383     self.mox.ReplayAll()
384
385     self.checker = js_checker.JSChecker(input_api, output_api)
386
387   def ShouldPassClosureLint(self, source):
388     errors = self.checker.ClosureLint('', source=source)
389
390     for error in errors:
391       print 'Error: ' + error.message
392
393     self.assertListEqual([], errors)
394
395   def testBindFalsePositives(self):
396     sources = [
397       [
398         'var addOne = function(prop) {\n',
399         '  this[prop] += 1;\n',
400         '}.bind(counter, timer);\n',
401         '\n',
402         'setInterval(addOne, 1000);\n',
403         '\n',
404       ],
405       [
406         '/** Da clickz. */\n',
407         'button.onclick = function() { this.add_(this.total_); }.bind(this);\n',
408       ],
409     ]
410     for source in sources:
411       self.ShouldPassClosureLint(source)
412
413   def testPromiseFalsePositives(self):
414     sources = [
415       [
416         'Promise.reject(1).catch(function(error) {\n',
417         '  alert(error);\n',
418         '});\n',
419       ],
420       [
421         'var loaded = new Promise();\n',
422         'loaded.then(runAwesomeApp);\n',
423         'loaded.catch(showSadFace);\n',
424         '\n',
425         '/** Da loadz. */\n',
426         'document.onload = function() { loaded.resolve(); };\n',
427         '\n',
428         '/** Da errorz. */\n',
429         'document.onerror = function() { loaded.reject(); };\n',
430         '\n',
431         "if (document.readystate == 'complete') loaded.resolve();\n",
432       ],
433     ]
434     for source in sources:
435       self.ShouldPassClosureLint(source)
436
437
438 class CssStyleGuideTest(SuperMoxTestBase):
439   def setUp(self):
440     SuperMoxTestBase.setUp(self)
441
442     self.fake_file_name = 'fake.css'
443
444     self.fake_file = self.mox.CreateMockAnything()
445     self.mox.StubOutWithMock(self.fake_file, 'LocalPath')
446     self.fake_file.LocalPath().AndReturn(self.fake_file_name)
447     # Actual calls to NewContents() are defined in each test.
448     self.mox.StubOutWithMock(self.fake_file, 'NewContents')
449
450     self.input_api = self.mox.CreateMockAnything()
451     self.input_api.re = re
452     self.mox.StubOutWithMock(self.input_api, 'AffectedSourceFiles')
453     self.input_api.AffectedFiles(
454         include_deletes=False, file_filter=None).AndReturn([self.fake_file])
455
456     # Actual creations of PresubmitPromptWarning are defined in each test.
457     self.output_api = self.mox.CreateMockAnything()
458     self.mox.StubOutWithMock(self.output_api, 'PresubmitPromptWarning',
459                              use_mock_anything=True)
460
461     self.output_api = self.mox.CreateMockAnything()
462     self.mox.StubOutWithMock(self.output_api, 'PresubmitNotifyResult',
463                              use_mock_anything=True)
464
465   def VerifyContentsIsValid(self, contents):
466     self.fake_file.NewContents().AndReturn(contents.splitlines())
467     self.mox.ReplayAll()
468     css_checker.CSSChecker(self.input_api, self.output_api).RunChecks()
469
470   def VerifyContentsProducesOutput(self, contents, output):
471     self.fake_file.NewContents().AndReturn(contents.splitlines())
472     author_msg = ('Was the CSS checker useful? '
473                   'Send feedback or hate mail to dbeam@chromium.org.')
474     self.output_api.PresubmitNotifyResult(author_msg).AndReturn(None)
475     self.output_api.PresubmitPromptWarning(
476         self.fake_file_name + ':\n' + output.strip()).AndReturn(None)
477     self.mox.ReplayAll()
478     css_checker.CSSChecker(self.input_api, self.output_api).RunChecks()
479
480   def testCssAlphaWithAtBlock(self):
481     self.VerifyContentsProducesOutput("""
482 <include src="../shared/css/cr/ui/overlay.css">
483 <include src="chrome://resources/totally-cool.css" />
484
485 /* A hopefully safely ignored comment and @media statement. /**/
486 @media print {
487   div {
488     display: block;
489     color: red;
490   }
491 }
492
493 .rule {
494   z-index: 5;
495 <if expr="not is macosx">
496   background-image: url(chrome://resources/BLAH); /* TODO(dbeam): Fix this. */
497   background-color: rgb(235, 239, 249);
498 </if>
499 <if expr="is_macosx">
500   background-color: white;
501   background-image: url(chrome://resources/BLAH2);
502 </if>
503   color: black;
504 }
505
506 <if expr="is_macosx">
507 .language-options-right {
508   visibility: hidden;
509   opacity: 1; /* TODO(dbeam): Fix this. */
510 }
511 </if>""", """
512 - Alphabetize properties and list vendor specific (i.e. -webkit) above standard.
513     display: block;
514     color: red;
515
516     z-index: 5;
517     color: black;""")
518
519   def testCssStringWithAt(self):
520     self.VerifyContentsIsValid("""
521 #logo {
522   background-image: url('images/google_logo.png@2x');
523 }
524
525 body.alternate-logo #logo {
526   -webkit-mask-image: url('images/google_logo.png@2x');
527   background: none;
528 }
529
530 .stuff1 {
531 }
532
533 .stuff2 {
534 }
535       """)
536
537   def testCssAlphaWithNonStandard(self):
538     self.VerifyContentsProducesOutput("""
539 div {
540   /* A hopefully safely ignored comment and @media statement. /**/
541   color: red;
542   -webkit-margin-start: 5px;
543 }""", """
544 - Alphabetize properties and list vendor specific (i.e. -webkit) above standard.
545     color: red;
546     -webkit-margin-start: 5px;""")
547
548   def testCssAlphaWithLongerDashedProps(self):
549     self.VerifyContentsProducesOutput("""
550 div {
551   border-left: 5px;  /* A hopefully removed comment. */
552   border: 5px solid red;
553 }""", """
554 - Alphabetize properties and list vendor specific (i.e. -webkit) above standard.
555     border-left: 5px;
556     border: 5px solid red;""")
557
558   def testCssBracesHaveSpaceBeforeAndNothingAfter(self):
559     self.VerifyContentsProducesOutput("""
560 /* Hello! */div/* Comment here*/{
561   display: block;
562 }
563
564 blah /* hey! */
565 {
566   rule: value;
567 }
568
569 .this.is { /* allowed */
570   rule: value;
571 }""", """
572 - Start braces ({) end a selector, have a space before them and no rules after.
573     div{
574     {""")
575
576   def testCssClassesUseDashes(self):
577     self.VerifyContentsProducesOutput("""
578 .className,
579 .ClassName,
580 .class-name /* We should not catch this. */,
581 .class_name {
582   display: block;
583 }""", """
584  - Classes use .dash-form.
585     .className,
586     .ClassName,
587     .class_name {""")
588
589   def testCssCloseBraceOnNewLine(self):
590     self.VerifyContentsProducesOutput("""
591 @media { /* TODO(dbeam) Fix this case. */
592   .rule {
593     display: block;
594   }}
595
596 @-webkit-keyframe blah {
597   100% { height: -500px 0; }
598 }
599
600 #rule {
601   rule: value; }""", """
602 - Always put a rule closing brace (}) on a new line.
603     rule: value; }""")
604
605   def testCssColonsHaveSpaceAfter(self):
606     self.VerifyContentsProducesOutput("""
607 div:not(.class):not([attr=5]), /* We should not catch this. */
608 div:not(.class):not([attr]) /* Nor this. */ {
609   background: url(data:image/jpeg,asdfasdfsadf); /* Ignore this. */
610   background: -webkit-linear-gradient(left, red,
611                                       80% blah blee blar);
612   color: red;
613   display:block;
614 }""", """
615 - Colons (:) should have a space after them.
616     display:block;
617
618 - Don't use data URIs in source files. Use grit instead.
619     background: url(data:image/jpeg,asdfasdfsadf);""")
620
621   def testCssFavorSingleQuotes(self):
622     self.VerifyContentsProducesOutput("""
623 html[dir="rtl"] body,
624 html[dir=ltr] body /* TODO(dbeam): Require '' around rtl in future? */ {
625   background: url("chrome://resources/BLAH");
626   font-family: "Open Sans";
627 <if expr="is_macosx">
628   blah: blee;
629 </if>
630 }""", """
631 - Use single quotes (') instead of double quotes (") in strings.
632     html[dir="rtl"] body,
633     background: url("chrome://resources/BLAH");
634     font-family: "Open Sans";""")
635
636   def testCssHexCouldBeShorter(self):
637     self.VerifyContentsProducesOutput("""
638 #abc,
639 #abc-,
640 #abc-ghij,
641 #abcdef-,
642 #abcdef-ghij,
643 #aaaaaa,
644 #bbaacc {
645   background-color: #336699; /* Ignore short hex rule if not gray. */
646   color: #999999;
647   color: #666;
648 }""", """
649 - Use abbreviated hex (#rgb) when in form #rrggbb.
650     color: #999999; (replace with #999)
651
652 - Use rgb() over #hex when not a shade of gray (like #333).
653     background-color: #336699; (replace with rgb(51, 102, 153))""")
654
655   def testCssUseMillisecondsForSmallTimes(self):
656     self.VerifyContentsProducesOutput("""
657 .transition-0s /* This is gross but may happen. */ {
658   transform: one 0.2s;
659   transform: two .1s;
660   transform: tree 1s;
661   transform: four 300ms;
662 }""", """
663 - Use milliseconds for time measurements under 1 second.
664     transform: one 0.2s; (replace with 200ms)
665     transform: two .1s; (replace with 100ms)""")
666
667   def testCssNoDataUrisInSourceFiles(self):
668     self.VerifyContentsProducesOutput("""
669 img {
670   background: url( data:image/jpeg,4\/\/350|\/|3|2 );
671   background: url('data:image/jpeg,4\/\/350|\/|3|2');
672 }""", """
673 - Don't use data URIs in source files. Use grit instead.
674     background: url( data:image/jpeg,4\/\/350|\/|3|2 );
675     background: url('data:image/jpeg,4\/\/350|\/|3|2');""")
676
677   def testCssOneRulePerLine(self):
678     self.VerifyContentsProducesOutput("""
679 a:not([hidden]):not(.custom-appearance):not([version=1]):first-of-type,
680 a:not([hidden]):not(.custom-appearance):not([version=1]):first-of-type ~
681     input[type='checkbox']:not([hidden]),
682 div {
683   background: url(chrome://resources/BLAH);
684   rule: value; /* rule: value; */
685   rule: value; rule: value;
686 }""", """
687 - One rule per line (what not to do: color: red; margin: 0;).
688     rule: value; rule: value;""")
689
690   def testCssOneSelectorPerLine(self):
691     self.VerifyContentsProducesOutput("""
692 a,
693 div,a,
694 div,/* Hello! */ span,
695 #id.class([dir=rtl):not(.class):any(a, b, d) {
696   rule: value;
697 }
698
699 a,
700 div,a {
701   some-other: rule here;
702 }""", """
703 - One selector per line (what not to do: a, b {}).
704     div,a,
705     div, span,
706     div,a {""")
707
708   def testCssPseudoElementDoubleColon(self):
709     self.VerifyContentsProducesOutput("""
710 a:href,
711 br::after,
712 ::-webkit-scrollbar-thumb,
713 a:not([empty]):hover:focus:active, /* shouldn't catch here and above */
714 abbr:after,
715 .tree-label:empty:after,
716 b:before,
717 :-WebKit-ScrollBar {
718   rule: value;
719 }""", """
720 - Pseudo-elements should use double colon (i.e. ::after).
721     :after (should be ::after)
722     :after (should be ::after)
723     :before (should be ::before)
724     :-WebKit-ScrollBar (should be ::-WebKit-ScrollBar)
725     """)
726
727   def testCssRgbIfNotGray(self):
728     self.VerifyContentsProducesOutput("""
729 #abc,
730 #aaa,
731 #aabbcc {
732   background: -webkit-linear-gradient(left, from(#abc), to(#def));
733   color: #bad;
734   color: #bada55;
735 }""", """
736 - Use rgb() over #hex when not a shade of gray (like #333).
737     background: -webkit-linear-gradient(left, from(#abc), to(#def)); """
738 """(replace with rgb(170, 187, 204), rgb(221, 238, 255))
739     color: #bad; (replace with rgb(187, 170, 221))
740     color: #bada55; (replace with rgb(186, 218, 85))""")
741
742   def testCssZeroLengthTerms(self):
743     self.VerifyContentsProducesOutput("""
744 @-webkit-keyframe anim {
745   0% { /* Ignore key frames */
746     width: 0px;
747   }
748   10% {
749     width: 10px;
750   }
751   100% {
752     width: 100px;
753   }
754 }
755
756 /* http://crbug.com/359682 */
757 #spinner-container #spinner {
758   -webkit-animation-duration: 1.0s;
759 }
760
761 .media-button.play > .state0.active,
762 .media-button[state='0'] > .state0.normal /* blah */, /* blee */
763 .media-button[state='0']:not(.disabled):hover > .state0.hover {
764   -webkit-animation: anim 0s;
765   -webkit-animation-duration: anim 0ms;
766   -webkit-transform: scale(0%),
767                      translateX(0deg),
768                      translateY(0rad),
769                      translateZ(0grad);
770   background-position-x: 0em;
771   background-position-y: 0ex;
772   border-width: 0em;
773   color: hsl(0, 0%, 85%); /* Shouldn't trigger error. */
774   opacity: .0;
775   opacity: 0.0;
776   opacity: 0.;
777 }
778
779 @page {
780   border-width: 0mm;
781   height: 0cm;
782   width: 0in;
783 }""", """
784 - Make all zero length terms (i.e. 0px) 0 unless inside of hsl() or part of"""
785 """ @keyframe.
786     width: 0px;
787     -webkit-animation: anim 0s;
788     -webkit-animation-duration: anim 0ms;
789     -webkit-transform: scale(0%),
790     translateX(0deg),
791     translateY(0rad),
792     translateZ(0grad);
793     background-position-x: 0em;
794     background-position-y: 0ex;
795     border-width: 0em;
796     opacity: .0;
797     opacity: 0.0;
798     opacity: 0.;
799     border-width: 0mm;
800     height: 0cm;
801     width: 0in;
802 """)
803
804 if __name__ == '__main__':
805   unittest.main()