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