| OLD | NEW |
| (Empty) |
| 1 #!/usr/bin/env python | |
| 2 # Copyright 2015 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 import css_checker | |
| 7 from os import path as os_path | |
| 8 import re | |
| 9 from sys import path as sys_path | |
| 10 import unittest | |
| 11 | |
| 12 _HERE = os_path.dirname(os_path.abspath(__file__)) | |
| 13 sys_path.append(os_path.join(_HERE, '..', '..', '..', 'build')) | |
| 14 | |
| 15 import find_depot_tools # pylint: disable=W0611 | |
| 16 from testing_support.super_mox import SuperMoxTestBase | |
| 17 | |
| 18 | |
| 19 class CssCheckerTest(SuperMoxTestBase): | |
| 20 def setUp(self): | |
| 21 SuperMoxTestBase.setUp(self) | |
| 22 | |
| 23 self.fake_file = self.mox.CreateMockAnything() | |
| 24 # Actual calls to NewContents() and LocalPath() are defined in each test. | |
| 25 self.mox.StubOutWithMock(self.fake_file, 'LocalPath') | |
| 26 self.mox.StubOutWithMock(self.fake_file, 'NewContents') | |
| 27 | |
| 28 self.input_api = self.mox.CreateMockAnything() | |
| 29 self.input_api.re = re | |
| 30 self.mox.StubOutWithMock(self.input_api, 'AffectedSourceFiles') | |
| 31 self.input_api.AffectedFiles( | |
| 32 include_deletes=False, file_filter=None).AndReturn([self.fake_file]) | |
| 33 | |
| 34 # Actual creations of PresubmitPromptWarning are defined in each test. | |
| 35 self.output_api = self.mox.CreateMockAnything() | |
| 36 self.mox.StubOutWithMock(self.output_api, 'PresubmitPromptWarning', | |
| 37 use_mock_anything=True) | |
| 38 | |
| 39 self.output_api = self.mox.CreateMockAnything() | |
| 40 self.mox.StubOutWithMock(self.output_api, 'PresubmitNotifyResult', | |
| 41 use_mock_anything=True) | |
| 42 | |
| 43 def _create_file(self, contents, filename): | |
| 44 self.fake_file_name = filename | |
| 45 self.fake_file.LocalPath().AndReturn(self.fake_file_name) | |
| 46 self.fake_file.NewContents().AndReturn(contents.splitlines()) | |
| 47 | |
| 48 def VerifyContentIsValid(self, contents, filename='fake.css'): | |
| 49 self._create_file(contents, filename) | |
| 50 self.mox.ReplayAll() | |
| 51 css_checker.CSSChecker(self.input_api, self.output_api).RunChecks() | |
| 52 | |
| 53 def VerifyContentsProducesOutput(self, contents, output, filename='fake.css'): | |
| 54 self._create_file(contents, filename) | |
| 55 self.output_api.PresubmitPromptWarning( | |
| 56 self.fake_file_name + ':\n' + output.strip()).AndReturn(None) | |
| 57 self.mox.ReplayAll() | |
| 58 css_checker.CSSChecker(self.input_api, self.output_api).RunChecks() | |
| 59 | |
| 60 def testCssAlphaWithAtBlock(self): | |
| 61 self.VerifyContentsProducesOutput(""" | |
| 62 <include src="../shared/css/cr/ui/overlay.css"> | |
| 63 <include src="chrome://resources/totally-cool.css" /> | |
| 64 | |
| 65 /* A hopefully safely ignored comment and @media statement. /**/ | |
| 66 @media print { | |
| 67 div { | |
| 68 display: block; | |
| 69 color: red; | |
| 70 } | |
| 71 } | |
| 72 | |
| 73 .rule { | |
| 74 z-index: 5; | |
| 75 <if expr="not is macosx"> | |
| 76 background-image: url(chrome://resources/BLAH); /* TODO(dbeam): Fix this. */ | |
| 77 background-color: rgb(235, 239, 249); | |
| 78 </if> | |
| 79 <if expr="is_macosx"> | |
| 80 background-color: white; | |
| 81 background-image: url(chrome://resources/BLAH2); | |
| 82 </if> | |
| 83 color: black; | |
| 84 } | |
| 85 | |
| 86 <if expr="is_macosx"> | |
| 87 .language-options-right { | |
| 88 visibility: hidden; | |
| 89 opacity: 1; /* TODO(dbeam): Fix this. */ | |
| 90 } | |
| 91 </if>""", """ | |
| 92 - Alphabetize properties and list vendor specific (i.e. -webkit) above standard. | |
| 93 display: block; | |
| 94 color: red; | |
| 95 | |
| 96 z-index: 5; | |
| 97 color: black;""") | |
| 98 | |
| 99 def testCssStringWithAt(self): | |
| 100 self.VerifyContentIsValid(""" | |
| 101 #logo { | |
| 102 background-image: url(images/google_logo.png@2x); | |
| 103 } | |
| 104 | |
| 105 body.alternate-logo #logo { | |
| 106 -webkit-mask-image: url(images/google_logo.png@2x); | |
| 107 background: none; | |
| 108 @apply(--some-variable); | |
| 109 } | |
| 110 | |
| 111 div { | |
| 112 -webkit-margin-start: 5px; | |
| 113 } | |
| 114 | |
| 115 .stuff1 { | |
| 116 } | |
| 117 | |
| 118 .stuff2 { | |
| 119 } | |
| 120 """) | |
| 121 | |
| 122 def testCssAlphaWithNonStandard(self): | |
| 123 self.VerifyContentsProducesOutput(""" | |
| 124 div { | |
| 125 /* A hopefully safely ignored comment and @media statement. /**/ | |
| 126 color: red; | |
| 127 -webkit-margin-start: 5px; | |
| 128 }""", """ | |
| 129 - Alphabetize properties and list vendor specific (i.e. -webkit) above standard. | |
| 130 color: red; | |
| 131 -webkit-margin-start: 5px;""") | |
| 132 | |
| 133 def testCssAlphaWithLongerDashedProps(self): | |
| 134 self.VerifyContentsProducesOutput(""" | |
| 135 div { | |
| 136 border-left: 5px; /* A hopefully removed comment. */ | |
| 137 border: 5px solid red; | |
| 138 }""", """ | |
| 139 - Alphabetize properties and list vendor specific (i.e. -webkit) above standard. | |
| 140 border-left: 5px; | |
| 141 border: 5px solid red;""") | |
| 142 | |
| 143 def testCssAlphaWithVariables(self): | |
| 144 self.VerifyContentIsValid(""" | |
| 145 #id { | |
| 146 --zzyxx-xylophone: 3px; | |
| 147 --ignore-me: { | |
| 148 /* TODO(dbeam): fix this by creating a "sort context". If we simply strip | |
| 149 * off the mixin, the inside contents will be compared to the outside | |
| 150 * contents, which isn't what we want. */ | |
| 151 visibility: hidden; | |
| 152 color: black; | |
| 153 }; | |
| 154 --aardvark-animal: var(--zzyxz-xylophone); | |
| 155 } | |
| 156 """) | |
| 157 | |
| 158 def testCssBracesHaveSpaceBeforeAndNothingAfter(self): | |
| 159 self.VerifyContentsProducesOutput(""" | |
| 160 /* Hello! */div/* Comment here*/{ | |
| 161 display: block; | |
| 162 } | |
| 163 | |
| 164 blah /* hey! */ | |
| 165 { | |
| 166 rule: value; | |
| 167 } | |
| 168 | |
| 169 .mixed-in { | |
| 170 display: none; | |
| 171 --css-mixin: { | |
| 172 color: red; | |
| 173 }; /* This should be ignored. */ | |
| 174 } | |
| 175 | |
| 176 .this.is { /* allowed */ | |
| 177 rule: value; | |
| 178 }""", """ | |
| 179 - Start braces ({) end a selector, have a space before them and no rules after. | |
| 180 div{ | |
| 181 {""") | |
| 182 | |
| 183 def testCssClassesUseDashes(self): | |
| 184 self.VerifyContentsProducesOutput(""" | |
| 185 .className, | |
| 186 .ClassName, | |
| 187 .class-name /* We should not catch this. */, | |
| 188 .class_name { | |
| 189 display: block; | |
| 190 }""", """ | |
| 191 - Classes use .dash-form. | |
| 192 .className, | |
| 193 .ClassName, | |
| 194 .class_name {""") | |
| 195 | |
| 196 def testCssCloseBraceOnNewLine(self): | |
| 197 self.VerifyContentsProducesOutput(""" | |
| 198 @media { /* TODO(dbeam) Fix this case. */ | |
| 199 .rule { | |
| 200 display: block; | |
| 201 }} | |
| 202 | |
| 203 @-webkit-keyframe blah { | |
| 204 from { height: rotate(-10turn); } | |
| 205 100% { height: 500px; } | |
| 206 } | |
| 207 | |
| 208 #id { /* $i18n{*} and $i18nRaw{*} should be ignored. */ | |
| 209 rule: $i18n{someValue}; | |
| 210 rule2: $i18nRaw{someValue}; | |
| 211 --css-mixin: { | |
| 212 color: red; | |
| 213 }; | |
| 214 } | |
| 215 | |
| 216 .paper-wrapper { | |
| 217 --paper-thinger: { | |
| 218 background: blue; | |
| 219 }; | |
| 220 } | |
| 221 | |
| 222 #rule { | |
| 223 rule: value; }""", """ | |
| 224 - Always put a rule closing brace (}) on a new line. | |
| 225 rule: value; }""") | |
| 226 | |
| 227 def testCssColonsHaveSpaceAfter(self): | |
| 228 self.VerifyContentsProducesOutput(""" | |
| 229 div:not(.class):not([attr=5]), /* We should not catch this. */ | |
| 230 div:not(.class):not([attr]) /* Nor this. */ { | |
| 231 background: url(data:image/jpeg,asdfasdfsadf); /* Ignore this. */ | |
| 232 background: -webkit-linear-gradient(left, red, | |
| 233 80% blah blee blar); | |
| 234 color: red; | |
| 235 display:block; | |
| 236 }""", """ | |
| 237 - Colons (:) should have a space after them. | |
| 238 display:block; | |
| 239 | |
| 240 - Don't use data URIs in source files. Use grit instead. | |
| 241 background: url(data:image/jpeg,asdfasdfsadf);""") | |
| 242 | |
| 243 def testCssFavorSingleQuotes(self): | |
| 244 self.VerifyContentsProducesOutput(""" | |
| 245 html[dir="rtl"] body, | |
| 246 html[dir=ltr] body /* TODO(dbeam): Require '' around rtl in future? */ { | |
| 247 font-family: "Open Sans"; | |
| 248 <if expr="is_macosx"> | |
| 249 blah: blee; | |
| 250 </if> | |
| 251 }""", """ | |
| 252 - Use single quotes (') instead of double quotes (") in strings. | |
| 253 html[dir="rtl"] body, | |
| 254 font-family: "Open Sans";""") | |
| 255 | |
| 256 def testCssHexCouldBeShorter(self): | |
| 257 self.VerifyContentsProducesOutput(""" | |
| 258 #abc, | |
| 259 #abc-, | |
| 260 #abc-ghij, | |
| 261 #abcdef-, | |
| 262 #abcdef-ghij, | |
| 263 #aaaaaa, | |
| 264 #bbaacc { | |
| 265 background-color: #336699; /* Ignore short hex rule if not gray. */ | |
| 266 color: #999999; | |
| 267 color: #666; | |
| 268 }""", """ | |
| 269 - Use abbreviated hex (#rgb) when in form #rrggbb. | |
| 270 color: #999999; (replace with #999) | |
| 271 | |
| 272 - Use rgb() over #hex when not a shade of gray (like #333). | |
| 273 background-color: #336699; (replace with rgb(51, 102, 153))""") | |
| 274 | |
| 275 def testCssUseMillisecondsForSmallTimes(self): | |
| 276 self.VerifyContentsProducesOutput(""" | |
| 277 .transition-0s /* This is gross but may happen. */ { | |
| 278 transform: one 0.2s; | |
| 279 transform: two .1s; | |
| 280 transform: tree 1s; | |
| 281 transform: four 300ms; | |
| 282 }""", """ | |
| 283 - Use milliseconds for time measurements under 1 second. | |
| 284 transform: one 0.2s; (replace with 200ms) | |
| 285 transform: two .1s; (replace with 100ms)""") | |
| 286 | |
| 287 def testCssNoDataUrisInSourceFiles(self): | |
| 288 self.VerifyContentsProducesOutput(""" | |
| 289 img { | |
| 290 background: url( data:image/jpeg,4\/\/350|\/|3|2 ); | |
| 291 }""", """ | |
| 292 - Don't use data URIs in source files. Use grit instead. | |
| 293 background: url( data:image/jpeg,4\/\/350|\/|3|2 );""") | |
| 294 | |
| 295 def testCssNoMixinShims(self): | |
| 296 self.VerifyContentsProducesOutput(""" | |
| 297 :host { | |
| 298 --good-property: red; | |
| 299 --not-okay-mixin_-_not-okay-property: green; | |
| 300 }""", """ | |
| 301 - Don't override custom properties created by Polymer's mixin shim. Set \ | |
| 302 mixins or documented custom properties directly. | |
| 303 --not-okay-mixin_-_not-okay-property: green;""") | |
| 304 | |
| 305 def testCssNoQuotesInUrl(self): | |
| 306 self.VerifyContentsProducesOutput(""" | |
| 307 img { | |
| 308 background: url('chrome://resources/images/blah.jpg'); | |
| 309 background: url("../../folder/hello.png"); | |
| 310 }""", """ | |
| 311 - Use single quotes (') instead of double quotes (") in strings. | |
| 312 background: url("../../folder/hello.png"); | |
| 313 | |
| 314 - Don't use quotes in url(). | |
| 315 background: url('chrome://resources/images/blah.jpg'); | |
| 316 background: url("../../folder/hello.png");""") | |
| 317 | |
| 318 def testCssOneRulePerLine(self): | |
| 319 self.VerifyContentsProducesOutput(""" | |
| 320 a:not([hidden]):not(.custom-appearance):not([version=1]):first-of-type, | |
| 321 a:not([hidden]):not(.custom-appearance):not([version=1]):first-of-type ~ | |
| 322 input[type='checkbox']:not([hidden]), | |
| 323 div { | |
| 324 background: url(chrome://resources/BLAH); | |
| 325 rule: value; /* rule: value; */ | |
| 326 rule: value; rule: value; | |
| 327 } | |
| 328 | |
| 329 .remix { | |
| 330 --dj: { | |
| 331 spin: that; | |
| 332 }; | |
| 333 } | |
| 334 """, """ | |
| 335 - One rule per line (what not to do: color: red; margin: 0;). | |
| 336 rule: value; rule: value;""") | |
| 337 | |
| 338 def testCssOneSelectorPerLine(self): | |
| 339 self.VerifyContentsProducesOutput(""" | |
| 340 a, | |
| 341 div,a, | |
| 342 div,/* Hello! */ span, | |
| 343 #id.class([dir=rtl):not(.class):any(a, b, d) { | |
| 344 rule: value; | |
| 345 } | |
| 346 | |
| 347 a, | |
| 348 div,a { | |
| 349 some-other: rule here; | |
| 350 }""", """ | |
| 351 - One selector per line (what not to do: a, b {}). | |
| 352 div,a, | |
| 353 div, span, | |
| 354 div,a {""") | |
| 355 | |
| 356 def testCssPseudoElementDoubleColon(self): | |
| 357 self.VerifyContentsProducesOutput(""" | |
| 358 a:href, | |
| 359 br::after, | |
| 360 ::-webkit-scrollbar-thumb, | |
| 361 a:not([empty]):hover:focus:active, /* shouldn't catch here and above */ | |
| 362 abbr:after, | |
| 363 .tree-label:empty:after, | |
| 364 b:before, | |
| 365 :-WebKit-ScrollBar { | |
| 366 rule: value; | |
| 367 }""", """ | |
| 368 - Pseudo-elements should use double colon (i.e. ::after). | |
| 369 :after (should be ::after) | |
| 370 :after (should be ::after) | |
| 371 :before (should be ::before) | |
| 372 :-WebKit-ScrollBar (should be ::-WebKit-ScrollBar) | |
| 373 """) | |
| 374 | |
| 375 def testCssRgbIfNotGray(self): | |
| 376 self.VerifyContentsProducesOutput(""" | |
| 377 #abc, | |
| 378 #aaa, | |
| 379 #aabbcc { | |
| 380 background: -webkit-linear-gradient(left, from(#abc), to(#def)); | |
| 381 color: #bad; | |
| 382 color: #bada55; | |
| 383 }""", """ | |
| 384 - Use rgb() over #hex when not a shade of gray (like #333). | |
| 385 background: -webkit-linear-gradient(left, from(#abc), to(#def)); """ | |
| 386 """(replace with rgb(170, 187, 204), rgb(221, 238, 255)) | |
| 387 color: #bad; (replace with rgb(187, 170, 221)) | |
| 388 color: #bada55; (replace with rgb(186, 218, 85))""") | |
| 389 | |
| 390 def testWebkitBeforeOrAfter(self): | |
| 391 self.VerifyContentsProducesOutput(""" | |
| 392 .test { | |
| 393 -webkit-margin-before: 10px; | |
| 394 -webkit-margin-start: 20px; | |
| 395 -webkit-padding-after: 3px; | |
| 396 -webkit-padding-end: 5px; | |
| 397 } | |
| 398 """, """ | |
| 399 - Use *-top/bottom instead of -webkit-*-before/after. | |
| 400 -webkit-margin-before: 10px; (replace with margin-top) | |
| 401 -webkit-padding-after: 3px; (replace with padding-bottom)""") | |
| 402 | |
| 403 def testCssZeroWidthLengths(self): | |
| 404 self.VerifyContentsProducesOutput(""" | |
| 405 @-webkit-keyframe anim { | |
| 406 0% { /* Ignore key frames */ | |
| 407 width: 0px; | |
| 408 } | |
| 409 10% { | |
| 410 width: 10px; | |
| 411 } | |
| 412 100% { | |
| 413 width: 100px; | |
| 414 } | |
| 415 } | |
| 416 | |
| 417 #logo { | |
| 418 background-image: url(images/google_logo.png@2x); | |
| 419 } | |
| 420 | |
| 421 body.alternate-logo #logo { | |
| 422 -webkit-mask-image: url(images/google_logo.png@2x); | |
| 423 } | |
| 424 | |
| 425 /* http://crbug.com/359682 */ | |
| 426 #spinner-container #spinner { | |
| 427 -webkit-animation-duration: 1.0s; | |
| 428 } | |
| 429 | |
| 430 .media-button.play > .state0.active, | |
| 431 .media-button[state='0'] > .state0.normal /* blah */, /* blee */ | |
| 432 .media-button[state='0']:not(.disabled):hover > .state0.hover { | |
| 433 -webkit-animation: anim 0s; | |
| 434 -webkit-animation-duration: anim 0ms; | |
| 435 -webkit-transform: scale(0%); | |
| 436 background-position-x: 0em; | |
| 437 background-position-y: 0ex; | |
| 438 border-width: 0em; | |
| 439 color: hsl(0, 0%, 85%); /* Shouldn't trigger error. */ | |
| 440 opacity: .0; | |
| 441 opacity: 0.0; | |
| 442 opacity: 0.; | |
| 443 } | |
| 444 | |
| 445 @page { | |
| 446 border-width: 0mm; | |
| 447 height: 0cm; | |
| 448 width: 0in; | |
| 449 }""", """ | |
| 450 - Use "0" for zero-width lengths (i.e. 0px -> 0) | |
| 451 width: 0px; | |
| 452 -webkit-transform: scale(0%); | |
| 453 background-position-x: 0em; | |
| 454 background-position-y: 0ex; | |
| 455 border-width: 0em; | |
| 456 opacity: .0; | |
| 457 opacity: 0.0; | |
| 458 opacity: 0.; | |
| 459 border-width: 0mm; | |
| 460 height: 0cm; | |
| 461 width: 0in; | |
| 462 """) | |
| 463 | |
| 464 def testInlineStyleInHtml(self): | |
| 465 self.VerifyContentsProducesOutput("""<!doctype html> | |
| 466 <html> | |
| 467 <head> | |
| 468 <!-- Don't warn about problems outside of style tags | |
| 469 html, | |
| 470 body { | |
| 471 margin: 0; | |
| 472 height: 100%; | |
| 473 } | |
| 474 --> | |
| 475 <style> | |
| 476 body { | |
| 477 flex-direction:column; | |
| 478 } | |
| 479 </style> | |
| 480 </head> | |
| 481 </html>""", """ | |
| 482 - Colons (:) should have a space after them. | |
| 483 flex-direction:column; | |
| 484 """, filename='test.html') | |
| 485 | |
| 486 def testInlineStyleInHtmlWithIncludes(self): | |
| 487 self.VerifyContentsProducesOutput("""<!doctype html> | |
| 488 <html> | |
| 489 <style include="fake-shared-css other-shared-css"> | |
| 490 body { | |
| 491 flex-direction:column; | |
| 492 } | |
| 493 </style> | |
| 494 </head> | |
| 495 </html>""", """ | |
| 496 - Colons (:) should have a space after them. | |
| 497 flex-direction:column; | |
| 498 """, filename='test.html') | |
| 499 | |
| 500 def testInlineSTyleInHtmlWithTagsInComments(self): | |
| 501 self.VerifyContentsProducesOutput("""<!doctype html> | |
| 502 <html> | |
| 503 <style> | |
| 504 body { | |
| 505 /* You better ignore the <tag> in this comment! */ | |
| 506 flex-direction:column; | |
| 507 } | |
| 508 </style> | |
| 509 </head> | |
| 510 </html>""", """ | |
| 511 - Colons (:) should have a space after them. | |
| 512 flex-direction:column; | |
| 513 """, filename='test.html') | |
| 514 | |
| 515 | |
| 516 if __name__ == '__main__': | |
| 517 unittest.main() | |
| OLD | NEW |