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