2016-08-04 00:35:53 +02:00
|
|
|
from __future__ import absolute_import
|
|
|
|
from __future__ import print_function
|
|
|
|
|
2016-12-16 14:38:26 +01:00
|
|
|
from typing import cast, Any
|
2016-08-04 00:35:53 +02:00
|
|
|
|
|
|
|
import sys
|
|
|
|
import unittest
|
|
|
|
|
|
|
|
try:
|
|
|
|
from tools.lib.css_parser import (
|
|
|
|
CssParserException,
|
|
|
|
CssSection,
|
|
|
|
parse,
|
2017-03-17 12:34:45 +01:00
|
|
|
handle_prefluff,
|
|
|
|
handle_postfluff
|
2016-08-04 00:35:53 +02:00
|
|
|
)
|
|
|
|
except ImportError:
|
|
|
|
print('ERROR!!! You need to run this via tools/test-tools.')
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
class ParserTestHappyPath(unittest.TestCase):
|
2016-12-16 14:38:26 +01:00
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
# type: (*Any, **Any) -> None
|
2016-12-18 23:11:24 +01:00
|
|
|
# This method should be removed when we migrate to version 3 of Python
|
2016-12-16 14:38:26 +01:00
|
|
|
import six
|
|
|
|
if six.PY2:
|
|
|
|
self.assertRaisesRegex = self.assertRaisesRegexp # type: ignore
|
|
|
|
super(ParserTestHappyPath, self).__init__(*args, **kwargs)
|
|
|
|
|
2016-08-04 00:35:53 +02:00
|
|
|
def test_basic_parse(self):
|
|
|
|
# type: () -> None
|
|
|
|
my_selector = 'li.foo'
|
|
|
|
my_block = '''{
|
|
|
|
color: red;
|
|
|
|
}'''
|
|
|
|
my_css = my_selector + ' ' + my_block
|
|
|
|
res = parse(my_css)
|
2017-03-17 12:34:45 +01:00
|
|
|
self.assertEqual(res.text(), 'li.foo {\n color: red;\n}')
|
2016-08-04 00:35:53 +02:00
|
|
|
section = cast(CssSection, res.sections[0])
|
|
|
|
block = section.declaration_block
|
2017-03-17 12:34:45 +01:00
|
|
|
self.assertEqual(block.text().strip(), '{\n color: red;\n}')
|
2016-08-04 00:35:53 +02:00
|
|
|
declaration = block.declarations[0]
|
|
|
|
self.assertEqual(declaration.css_property, 'color')
|
|
|
|
self.assertEqual(declaration.css_value.text().strip(), 'red')
|
|
|
|
|
|
|
|
def test_same_line_comment(self):
|
|
|
|
# type: () -> None
|
|
|
|
my_css = '''
|
|
|
|
li.hide {
|
|
|
|
display: none; /* comment here */
|
|
|
|
/* Not to be confused
|
|
|
|
with this comment */
|
|
|
|
color: green;
|
|
|
|
}'''
|
|
|
|
res = parse(my_css)
|
|
|
|
section = cast(CssSection, res.sections[0])
|
|
|
|
block = section.declaration_block
|
|
|
|
declaration = block.declarations[0]
|
|
|
|
self.assertIn('/* comment here */', declaration.text())
|
|
|
|
|
2016-09-11 05:44:56 +02:00
|
|
|
def test_no_semicolon(self):
|
|
|
|
# type: () -> None
|
|
|
|
my_css = '''
|
|
|
|
p { color: red }
|
|
|
|
'''
|
|
|
|
|
2017-03-17 12:34:45 +01:00
|
|
|
reformatted_css = '\np {\n color: red;\n}\n'
|
2017-03-14 16:18:40 +01:00
|
|
|
|
2016-09-11 05:44:56 +02:00
|
|
|
res = parse(my_css)
|
|
|
|
|
2017-03-14 16:18:40 +01:00
|
|
|
self.assertEqual(res.text(), reformatted_css)
|
2016-09-11 05:44:56 +02:00
|
|
|
|
|
|
|
section = cast(CssSection, res.sections[0])
|
|
|
|
|
|
|
|
self.assertFalse(section.declaration_block.declarations[0].semicolon)
|
|
|
|
|
2016-09-23 02:30:31 +02:00
|
|
|
def test_empty_block(self):
|
|
|
|
# type: () -> None
|
|
|
|
my_css = '''
|
|
|
|
div {
|
|
|
|
}'''
|
|
|
|
error = 'Empty declaration'
|
2017-02-19 02:01:01 +01:00
|
|
|
with self.assertRaisesRegex(CssParserException, error):
|
2016-09-23 02:30:31 +02:00
|
|
|
parse(my_css)
|
|
|
|
|
2016-08-04 00:35:53 +02:00
|
|
|
def test_multi_line_selector(self):
|
|
|
|
# type: () -> None
|
|
|
|
my_css = '''
|
|
|
|
h1,
|
|
|
|
h2,
|
|
|
|
h3 {
|
|
|
|
top: 0
|
|
|
|
}'''
|
|
|
|
res = parse(my_css)
|
|
|
|
section = res.sections[0]
|
|
|
|
selectors = section.selector_list.selectors
|
|
|
|
self.assertEqual(len(selectors), 3)
|
|
|
|
|
|
|
|
def test_comment_at_end(self):
|
|
|
|
# type: () -> None
|
|
|
|
'''
|
|
|
|
This test verifies the current behavior, which is to
|
|
|
|
attach comments to the preceding rule, but we should
|
|
|
|
probably change it so the comments gets attached to
|
|
|
|
the next block, if possible.
|
|
|
|
'''
|
|
|
|
my_css = '''
|
|
|
|
p {
|
|
|
|
color: black;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* comment at the end of the text */
|
|
|
|
'''
|
|
|
|
res = parse(my_css)
|
|
|
|
self.assertEqual(len(res.sections), 1)
|
|
|
|
section = res.sections[0]
|
|
|
|
self.assertIn('comment at the end', section.post_fluff)
|
|
|
|
|
|
|
|
def test_media_block(self):
|
|
|
|
# type: () -> None
|
|
|
|
my_css = '''
|
|
|
|
@media (max-width: 300px) {
|
|
|
|
h5 {
|
|
|
|
margin: 0;
|
|
|
|
}
|
|
|
|
}'''
|
|
|
|
res = parse(my_css)
|
|
|
|
self.assertEqual(len(res.sections), 1)
|
2017-03-17 12:34:45 +01:00
|
|
|
self.assertEqual(res.text(), '\n @media (max-width: 300px) {\n h5 {\n margin: 0;\n }\n}')
|
|
|
|
|
|
|
|
def test_handle_prefluff(self):
|
|
|
|
# type: () -> None
|
|
|
|
PREFLUFF = ' \n '
|
|
|
|
PREFLUFF1 = ' '
|
|
|
|
PREFLUFF2 = ' /* some comment \nhere */'
|
|
|
|
PREFLUFF3 = '\n /* some comment \nhere */'
|
|
|
|
self.assertEqual(handle_prefluff(PREFLUFF), '\n')
|
|
|
|
self.assertEqual(handle_prefluff(PREFLUFF, True), '\n ')
|
|
|
|
self.assertEqual(handle_prefluff(PREFLUFF1), '')
|
|
|
|
self.assertEqual(handle_prefluff(PREFLUFF1, True), '\n ')
|
|
|
|
self.assertEqual(handle_prefluff(PREFLUFF2), '/* some comment\n here */\n')
|
|
|
|
self.assertEqual(handle_prefluff(PREFLUFF3, True), '\n /* some comment\n here */\n ')
|
|
|
|
|
|
|
|
def test_handle_postfluff(self):
|
|
|
|
# type: () -> None
|
|
|
|
POSTFLUFF = '/* Comment Here */'
|
|
|
|
POSTFLUFF1 = '/* Comment \nHere */'
|
|
|
|
POSTFLUFF2 = ' '
|
|
|
|
POSTFLUFF3 = '\n /* some comment \nhere */'
|
|
|
|
self.assertEqual(handle_postfluff(POSTFLUFF), '/* Comment Here */\n')
|
|
|
|
self.assertEqual(handle_postfluff(POSTFLUFF, space_after_first_line=True), ' /* Comment Here */\n')
|
|
|
|
self.assertEqual(handle_postfluff(POSTFLUFF, indent=True, space_after_first_line=True), ' /* Comment Here */\n')
|
|
|
|
self.assertEqual(handle_postfluff(POSTFLUFF1), '/* Comment\n Here */')
|
|
|
|
self.assertEqual(handle_postfluff(POSTFLUFF1, space_after_first_line=True), ' /* Comment\n Here */\n')
|
|
|
|
self.assertEqual(handle_postfluff(POSTFLUFF1, indent=True, space_after_first_line=True), ' /* Comment\n Here */\n')
|
|
|
|
self.assertEqual(handle_postfluff(POSTFLUFF2), '')
|
|
|
|
self.assertEqual(handle_postfluff(POSTFLUFF2, space_after_first_line=True), '')
|
|
|
|
self.assertEqual(handle_postfluff(POSTFLUFF2, indent=True, space_after_first_line=True), '\n')
|
|
|
|
self.assertEqual(handle_postfluff(POSTFLUFF3), '\n/* some comment\n here */')
|
|
|
|
self.assertEqual(handle_postfluff(POSTFLUFF3, space_after_first_line=True), '\n/* some comment\n here */\n')
|
|
|
|
self.assertEqual(handle_postfluff(POSTFLUFF3, indent=True, space_after_first_line=True), '\n /* some comment\n here */\n')
|
2016-08-04 00:35:53 +02:00
|
|
|
|
|
|
|
class ParserTestSadPath(unittest.TestCase):
|
|
|
|
'''
|
|
|
|
Use this class for tests that verify the parser will
|
|
|
|
appropriately choke on malformed CSS.
|
|
|
|
|
|
|
|
We prevent some things that are technically legal
|
|
|
|
in CSS, like having comments in the middle of list
|
|
|
|
of selectors. Some of this is just for expediency;
|
|
|
|
some of this is to enforce consistent formatting.
|
|
|
|
'''
|
2016-12-16 14:38:26 +01:00
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
# type: (*Any, **Any) -> None
|
2016-12-18 23:11:24 +01:00
|
|
|
# This method should be removed when we migrate to version 3 of Python
|
2016-12-16 14:38:26 +01:00
|
|
|
import six
|
|
|
|
if six.PY2:
|
|
|
|
self.assertRaisesRegex = self.assertRaisesRegexp # type: ignore
|
|
|
|
super(ParserTestSadPath, self).__init__(*args, **kwargs)
|
2016-08-04 00:35:53 +02:00
|
|
|
|
|
|
|
def _assert_error(self, my_css, error):
|
2016-09-12 18:03:37 +02:00
|
|
|
# type: (str, str) -> None
|
2017-02-19 02:01:01 +01:00
|
|
|
with self.assertRaisesRegex(CssParserException, error):
|
2016-08-04 00:35:53 +02:00
|
|
|
parse(my_css)
|
|
|
|
|
|
|
|
def test_unexpected_end_brace(self):
|
|
|
|
# type: () -> None
|
|
|
|
my_css = '''
|
|
|
|
@media (max-width: 975px) {
|
|
|
|
body {
|
|
|
|
color: red;
|
|
|
|
}
|
|
|
|
}} /* whoops */'''
|
|
|
|
error = 'unexpected }'
|
|
|
|
self._assert_error(my_css, error)
|
|
|
|
|
|
|
|
def test_empty_section(self):
|
|
|
|
# type: () -> None
|
|
|
|
my_css = '''
|
|
|
|
|
|
|
|
/* nothing to see here, move along */
|
|
|
|
'''
|
|
|
|
error = 'unexpected empty section'
|
|
|
|
self._assert_error(my_css, error)
|
|
|
|
|
|
|
|
def test_missing_colon(self):
|
|
|
|
# type: () -> None
|
|
|
|
my_css = '''
|
|
|
|
.hide
|
|
|
|
{
|
|
|
|
display none /* no colon here */
|
|
|
|
}'''
|
|
|
|
error = 'We expect a colon here'
|
|
|
|
self._assert_error(my_css, error)
|
|
|
|
|
|
|
|
def test_unclosed_comment(self):
|
|
|
|
# type: () -> None
|
|
|
|
my_css = ''' /* comment with no end'''
|
|
|
|
error = 'unclosed comment'
|
|
|
|
self._assert_error(my_css, error)
|
|
|
|
|
|
|
|
def test_missing_selectors(self):
|
|
|
|
# type: () -> None
|
|
|
|
my_css = '''
|
|
|
|
/* no selectors here */
|
|
|
|
{
|
|
|
|
bottom: 0
|
|
|
|
}'''
|
|
|
|
error = 'Missing selector'
|
|
|
|
self._assert_error(my_css, error)
|
|
|
|
|
2017-02-01 15:31:24 +01:00
|
|
|
def test_missing_value(self):
|
|
|
|
# type: () -> None
|
|
|
|
my_css = '''
|
|
|
|
h1
|
|
|
|
{
|
|
|
|
bottom:
|
|
|
|
}'''
|
|
|
|
error = 'Missing value'
|
|
|
|
self._assert_error(my_css, error)
|
|
|
|
|
2016-08-04 00:35:53 +02:00
|
|
|
def test_disallow_comments_in_selectors(self):
|
|
|
|
# type: () -> None
|
|
|
|
my_css = '''
|
|
|
|
h1,
|
|
|
|
h2, /* comment here not allowed by Zulip */
|
|
|
|
h3 {
|
|
|
|
top: 0
|
|
|
|
}'''
|
|
|
|
error = 'Comments in selector section are not allowed'
|
|
|
|
self._assert_error(my_css, error)
|