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