Skip to content

Commit 8c1b9bc

Browse files
committed
[FIX] test_themes: make tests consider our "s_" classes convention
The test was incorrectly flagging inner classes that follow our naming convention (e.g. "s_floating_blocks_wrapper" for "s_floating_blocks" snippet). This made maintaining the whitelist tedious and error prone. The change enables proper enforcement of our conventions while simplifying whitelist maintenance: a snippet subclass starting with "s_" should be prefixed by the main snippet class name (e.g. "s_super_snippet_item" might be added on an inner element of "s_super_snippet"). The test just checks any element class is prefixed by any class of any parent (or itself) (for "s_" classes). Related to task-4373543 closes #1054 Related: odoo/odoo#189299 Signed-off-by: Quentin Smetz (qsm) <[email protected]>
1 parent e5b61cf commit 8c1b9bc

File tree

1 file changed

+72
-34
lines changed

1 file changed

+72
-34
lines changed

test_themes/tests/test_new_page_templates.py

Lines changed: 72 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -77,39 +77,6 @@
7777
re.compile(r'^shadow-.+'): [],
7878
# Shapes
7979
re.compile(r'^o_web_editor_[A-Z].+'): [],
80-
# Snippets
81-
# TODO our convention (badly followed) for classes which are specific to a
82-
# snippet's inner components or options is to start that class with the
83-
# class specific to the snippet itself. For instance, for a s_some_stuff
84-
# snippet, use s_some_stuff_button or s_some_stuff_small. The test here
85-
# flags as wrong such an usecase... unless you explicitly whitelist it. It
86-
# should be smarter than that and make following our convention always ok
87-
# without the need to change this test's whitelist.
88-
re.compile(r'^s_.*'): [
89-
's_alert_md',
90-
's_blockquote_with_icon', 's_blockquote',
91-
's_carousel_default', 's_carousel_rounded', 's_carousel_boxed',
92-
's_carousel_indicators_dots', 's_carousel_indicators_hidden', 's_carousel_controllers_indicators_outside',
93-
's_carousel_cards_with_img', 's_carousel_cards_card',
94-
's_quotes_carousel',
95-
's_dynamic', 's_dynamic_empty',
96-
's_dynamic_snippet_blog_posts', 's_blog_posts_effect_marley', 's_blog_post_big_picture', 's_blog_posts_post_picture_size_default',
97-
's_event_upcoming_snippet', 's_event_event_picture',
98-
's_col_no_bgcolor', 's_col_no_resize',
99-
's_image_gallery', 's_image_gallery_indicators_arrows_boxed', 's_image_gallery_indicators_arrows_rounded',
100-
's_image_gallery_indicators_dots', 's_image_gallery_indicators_squared', 's_image_gallery_indicators_rounded', 's_image_gallery_indicators_hidden', 's_image_gallery_indicators_bars', 's_image_gallery_indicators_outside','s_image_gallery_controllers_outside_arrows_right', 's_image_gallery_controllers_outside',
101-
's_newsletter_list', 's_newsletter_subscribe_form',
102-
's_parallax_is_fixed', 's_parallax_no_overflow_hidden',
103-
's_process_steps_connector_line',
104-
's_product_catalog_dish_name', 's_product_catalog_dish_dot_leaders',
105-
's_progress_bar_label_hidden', 's_progress_bar_label_inline',
106-
's_rating_no_title',
107-
's_table_of_content_vertical_navbar', 's_table_of_content_navbar_sticky', 's_table_of_content_navbar_wrap',
108-
's_timeline_card',
109-
's_website_form_custom', 's_website_form_dnone', 's_website_form_field', 's_website_form_input', 's_website_form_mark', 's_website_form_submit', 's_website_form_no_submit_label',
110-
's_donation_btn', 's_donation_custom_btn', 's_newsletter_subscribe_form_input_small',
111-
's_tabs_common', 's_tabs_nav_vertical', 's_tabs_nav_with_descriptions',
112-
],
11380
# Text
11481
re.compile(r'^text-(?!(center|end|start|bg-|lg-)).*$'): [
11582
'text-break', 'text-decoration-none', 'text-reset',
@@ -119,6 +86,37 @@
11986
# Width
12087
re.compile(r'^w-\d*$'): [],
12188
}
89+
# Special case for "s_" classes that respect our convention: classes that share
90+
# the same base and follow the naming pattern (s_some, s_some_button) are not
91+
# flagged as conflicting. Explicitly whitelist exceptions that don't follow the
92+
# pattern only.
93+
# TODO all these classes were processed but we might want to re-check them all
94+
# to minimize the list if possible.
95+
S_CLASSES_WHITELIST = [
96+
# Classes that rightfully belong here at the moment
97+
's_col_no_bgcolor', 's_col_no_resize', 's_allow_columns',
98+
's_nb_column_fixed', 's_dialog_preview',
99+
's_parallax_is_fixed', 's_parallax_bg', 's_parallax_no_overflow_hidden',
100+
's_carousel_cards_card', 's_timeline_card', 's_blog_posts', 's_events',
101+
's_appointments',
102+
103+
# Classes that should not be here... but are here by compatibility (not
104+
# following our "s_" conventions correctly).
105+
's_process_step', 's_process_step_svg_defs', 's_number', 's_tabs_common',
106+
's_process_steps_connector_line', 's_tabs_nav', 's_tabs_main',
107+
's_tabs_nav_vertical', 's_tabs_nav_with_descriptions', 's_tabs_content',
108+
's_carousel', 's_carousel_default', 's_carousel_boxed', 's_carousel_intro',
109+
's_carousel_rounded', 's_carousel_cards', 's_carousel_indicators_numbers',
110+
's_carousel_indicators_dots', 's_quotes_carousel', 's_rating_no_title',
111+
's_carousel_indicators_hidden', 's_blog_post_big_picture',
112+
's_newsletter_list', 's_event_upcoming_snippet', 's_event_event_picture',
113+
's_newsletter_subscribe_form',
114+
115+
# FIXME those classes have no reason to be here... missing data-snippet?
116+
's_hr', 's_accordion', 's_accordion_highlight', 's_media_list_item',
117+
's_media_list_img_wrapper', 's_media_list_body', 's_media_list_img',
118+
's_website_form_datetime',
119+
]
122120

123121

124122
@tagged('post_install', '-at_install')
@@ -186,7 +184,7 @@ def check(theme_name, website):
186184
html_text = self.env['ir.qweb']._render(view.id)
187185
if not html_text:
188186
continue
189-
html_tree = html.fromstring(html_text)
187+
html_tree = html.fromstring(f'<wrap>{html_text}</wrap>')
190188
blocks_el = html_tree.xpath("//*[@id='o_scroll']")
191189
if blocks_el:
192190
# Only look at blocks in website.snippets
@@ -216,6 +214,46 @@ def check(theme_name, website):
216214
% (theme_name, view.key, conflict, classes, conflicting_classes_re.pattern)
217215
)
218216

217+
# Special handling for snippet classes following
218+
# naming convention: if classes match the
219+
# 's_snippet_name_*' pattern, they are allowed.
220+
non_whitelisted_s_classes = {
221+
cl for cl in classes
222+
if cl.startswith('s_') and cl not in S_CLASSES_WHITELIST
223+
}
224+
if non_whitelisted_s_classes:
225+
all_parent_s_classes = set()
226+
parent_el = el
227+
is_in_snippet = view.key.startswith('website.configurator_')
228+
# Find all parent elements classes that start
229+
# with 's_' (including on the current element).
230+
while parent_el is not None:
231+
parent_classes = set(parent_el.attrib.get('class', '').split())
232+
all_parent_s_classes.update({cl for cl in parent_classes if cl.startswith('s_')})
233+
if parent_el.attrib.get('data-snippet'):
234+
is_in_snippet = True
235+
break
236+
parent_el = parent_el.getparent()
237+
if is_in_snippet:
238+
# Accept classes that are prefixed by such a
239+
# parent class (+ '_').
240+
non_whitelisted_s_classes = {
241+
cl for cl in non_whitelisted_s_classes
242+
if not any(cl.startswith(f'{parent_cls}_') for parent_cls in all_parent_s_classes)
243+
}
244+
is_snippet_root = el.attrib.get('data-snippet') \
245+
or el.getparent().tag == 'wrap' and view.key.startswith('website.configurator_')
246+
if len(non_whitelisted_s_classes) > (1 if is_snippet_root else 0):
247+
errors.append(
248+
"Using %r, view %r contains 's_' classes that do not respect our conventions: %r in %r"
249+
% (theme_name, view.key, non_whitelisted_s_classes, classes)
250+
)
251+
else:
252+
errors.append(
253+
"Using %r, view %r contains 's_' classes (%r) that are not in a snippet"
254+
% (theme_name, view.key, non_whitelisted_s_classes)
255+
)
256+
219257
for el in html_tree.xpath('//*[@style]'):
220258
styles = el.attrib['style'].split(';')
221259
non_empty_styles = filter(lambda style: style, styles)

0 commit comments

Comments
 (0)