Skip to content

Commit 54a2d3d

Browse files
bso-odooxlu-odoo
authored andcommitted
[IMP] test_themes: add tests to ensure theme compatibility with website
This commit adds a test that makes sure templates can be rendered on all themes and that they neither contain duplicate classes nor contradicting ones. Covered templates: - snippet blocks - configurator snippet customizations - new page template snippet customizations task-3562147 closes odoo#744 X-original-commit: 758e2ab Related: odoo/odoo#141074 Signed-off-by: Romain Derie (rde) <[email protected]>
1 parent 8de909e commit 54a2d3d

File tree

2 files changed

+209
-0
lines changed

2 files changed

+209
-0
lines changed

test_themes/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
# Part of Odoo. See LICENSE file for full copyright and licensing details.
33

44
from . import test_crawl
5+
from . import test_new_page_templates
56
from . import test_theme_upgrade
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
# -*- coding: utf-8 -*-
2+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
3+
4+
from lxml import html
5+
6+
import logging
7+
import re
8+
9+
from odoo.addons.website.tools import MockRequest
10+
from odoo.tests import tagged, TransactionCase
11+
from odoo.tools import escape_psql
12+
13+
_logger = logging.getLogger(__name__)
14+
15+
CONFLICTUAL_CLASSES = [
16+
['btn-outline-primary', 'btn-primary', 'btn-secondary'],
17+
['btn-block', 'btn-outline-primary'],
18+
['container', 'container-fluid', 'o_container_small'],
19+
['d-block', 'd-flex', 'd-inline-block', 'd-none'],
20+
['d-block', 'd-lg-block', 'd-md-block'],
21+
['d-flex', 'd-md-flex'],
22+
['flex-column', 'flex-column-reverse', 'flex-row', 'flex-row-reverse'],
23+
['g-0', 'g-col-lg-2', 'g-col-lg-3', 'g-col-lg-4', 'g-col-lg-5', 'g-col-lg-6'],
24+
['g-0', 'g-height-5', 'g-height-8', 'g-height-10'],
25+
['h-100', 'o_half_screen_height', 'o_full_screen_height'],
26+
['justify-content-center', 'justify-content-start'],
27+
['nav-link', 'nav-pills', 'nav-tabs'],
28+
['o_cc1', 'o_cc2', 'o_cc3', 'o_cc4', 'o_cc5'],
29+
['o_spc-medium', 'o_spc-none', 'o_spc-small'],
30+
['oi-arrows-h', 'oi-arrows-v', 'oi-chevron-left', 'oi-chevron-right', 'oi-search'],
31+
['position-absolute', 'position-relative'],
32+
['s_carousel_bordered', 's_carousel_default', 's_carousel_rounded'],
33+
['s_image_gallery_indicators_arrows_boxed', 's_image_gallery_indicators_arrows_rounded'],
34+
['text-center', 'text-end', 'text-start'],
35+
]
36+
37+
# For each RE, associates a whitelist
38+
CONFLICTUAL_CLASSES_RE = {
39+
# Align
40+
re.compile(r'^align-(?!(self|items)-).+'): [],
41+
re.compile(r'^align-self-.+'): [],
42+
re.compile(r'^align-items-.+'): [],
43+
# BG
44+
re.compile(r'^bg(-|_)'): [
45+
'bg_option_menu_gradient',
46+
],
47+
# Col
48+
re.compile(r'^col-\d+$'): [],
49+
re.compile(r'^col-lg-.+'): [],
50+
re.compile(r'^offset-\d+$'): [],
51+
re.compile(r'^offset-lg-.+'): [],
52+
# Display
53+
re.compile(r'^display-\d$'): [],
54+
re.compile(r'^display-\d-fs$'): [],
55+
# Margin, padding
56+
re.compile(r'^m-(\d|auto)$'): [],
57+
re.compile(r'^m(x|s)-\d$'): [],
58+
re.compile(r'^m(x|e)-\d$'): [],
59+
re.compile(r'^m(y|t)-\d$'): [],
60+
re.compile(r'^m(y|b)-\d$'): [],
61+
re.compile(r'^(p(x|s)?-?\d+|padding-.+)$'): [],
62+
re.compile(r'^(p(x|e)?-?\d+|padding-.+)$'): [],
63+
re.compile(r'^(p(y|t)?-?\d+|padding-.+)$'): [],
64+
# p0+pb32 appears in Bewise and Graphene
65+
re.compile(r'^(p(y|b)?-?\d+|padding-.+)$'): ['p0'],
66+
# Font awesome
67+
re.compile(r'^fa-\dx$'): [],
68+
re.compile(r'^fa-...+'): [],
69+
# Rounded
70+
re.compile(r'^rounded-.+'): [],
71+
# Shadow
72+
re.compile(r'^shadow-.+'): [],
73+
# Shapes
74+
re.compile(r'^o_web_editor_[A-Z].+'): [],
75+
# Snippets
76+
re.compile(r'^s_.*'): [
77+
's_alert_md',
78+
's_blockquote_classic',
79+
's_carousel_bordered', 's_carousel_default', 's_carousel_rounded',
80+
's_dynamic', 's_dynamic_empty', 's_dynamic_snippet_blog_posts',
81+
's_blog_posts_effect_marley', 's_blog_post_big_picture',
82+
's_col_no_bgcolor', 's_col_no_resize',
83+
's_event_upcoming_snippet',
84+
's_image_gallery_cover', 's_image_gallery_indicators_arrows_boxed', 's_image_gallery_indicators_arrows_rounded',
85+
's_image_gallery_indicators_dots', 's_image_gallery_indicators_rounded', 's_image_gallery_show_indicators',
86+
's_newsletter_list', 's_newsletter_subscribe_form',
87+
's_parallax_is_fixed', 's_parallax_no_overflow_hidden',
88+
's_process_steps_connector_line',
89+
's_product_catalog_dish_name', 's_product_catalog_dish_dot_leaders',
90+
's_table_of_content_vertical_navbar', 's_table_of_content_navbar_sticky', 's_table_of_content_navbar_wrap',
91+
's_timeline_card',
92+
's_website_form_custom', 's_website_form_dnone', 's_website_form_field', 's_website_form_input', 's_website_form_mark',
93+
],
94+
# Text
95+
re.compile(r'^text-(?!(center|end|start|bg-|lg-)).*$'): [
96+
'text-break',
97+
],
98+
re.compile(r'^text-bg-.*$'): [],
99+
re.compile(r'^text-lg-.*$'): [],
100+
# Width
101+
re.compile(r'^w-\d*$'): [],
102+
}
103+
104+
105+
@tagged('post_install', '-at_install')
106+
class TestNewPageTemplates(TransactionCase):
107+
108+
def test_template_names(self):
109+
websites_themes = self.env['website'].get_test_themes_websites()
110+
for website in websites_themes:
111+
views = self.env['ir.ui.view'].search([
112+
('key', 'like', f'{website.theme_id.name}.new_page_template%_s_'),
113+
])
114+
if website.theme_id.name != 'theme_default':
115+
self.assertGreater(len(views), 10, "Test should have encountered some views in theme %r" % website.name)
116+
for view in views:
117+
self.assertEqual(view.mode, 'extension', "Theme's new page template customization %r should never be primary" % view.key)
118+
name = view.key.split('.')[1]
119+
parent_name = view.inherit_id.key.split('.')[1]
120+
self.assertEqual(name, parent_name, "Theme's new page template customization %r should use the same name as their parent %r" % (view.key, view.inherit_id.key))
121+
122+
def test_render_templates(self):
123+
errors = []
124+
view_ids = set()
125+
websites_themes = self.env['website'].get_test_themes_websites()
126+
for website in websites_themes:
127+
with MockRequest(self.env, website=website):
128+
views = self.env['ir.ui.view'].search([
129+
'|', '|',
130+
('key', 'like', f'{website.theme_id.name}.s_'),
131+
('key', 'like', f'{website.theme_id.name}.configurator'),
132+
('key', 'like', f'{website.theme_id.name}.new_page'),
133+
])
134+
view_ids.update(views.ids)
135+
for view in views:
136+
try:
137+
self.env['ir.qweb']._render(view.id)
138+
except Exception:
139+
errors.append("View %s cannot be rendered" % view.key)
140+
_logger.info("Tested %s views", len(view_ids))
141+
self.assertGreater(len(view_ids), 1250, "Test should have encountered a lot of views")
142+
self.assertFalse(errors, "No error should have been collected")
143+
144+
def test_render_applied_templates(self):
145+
View = self.env['ir.ui.view']
146+
errors = []
147+
classes_inventory = set()
148+
view_count = 0
149+
150+
def check(theme_name, website):
151+
with MockRequest(self.env, website=website):
152+
views = View.search([
153+
'|', '|',
154+
('key', 'in', [
155+
'website.snippets',
156+
'website.new_page_template_groups',
157+
]),
158+
('key', 'like', escape_psql('website.configurator_')),
159+
('key', 'like', escape_psql('website.new_page_template_sections_')),
160+
])
161+
for view in views:
162+
try:
163+
# TODO: Improve the perfs of the next line
164+
# Doesn't seem to be a way to avoid one RECURSIVE
165+
# SQL Query from `_get_inheriting_views` per view
166+
html_text = self.env['ir.qweb']._render(view.id)
167+
if not html_text:
168+
continue
169+
html_tree = html.fromstring(html_text)
170+
blocks_el = html_tree.xpath("//*[@id='o_scroll']")
171+
if blocks_el:
172+
# Only look at blocks in website.snippets
173+
html_tree = blocks_el[0]
174+
for el in html_tree.xpath('//*[@class]'):
175+
classes = el.attrib['class'].split(' ')
176+
classes_inventory.update(classes)
177+
if len(classes) != len(set(classes)):
178+
errors.append("Using %r, view %r contains duplicate classes: %r" % (theme_name, view.key, classes))
179+
for conflicting_classes in CONFLICTUAL_CLASSES:
180+
conflict = set(classes).intersection(conflicting_classes)
181+
if len(conflict) > 1:
182+
errors.append("Using %r, view %r contains conflicting classes: %r in %r" % (theme_name, view.key, conflict, classes))
183+
for conflicting_classes_re in CONFLICTUAL_CLASSES_RE:
184+
conflict = {cl for cl in filter(conflicting_classes_re.findall, set(classes))}
185+
white_list = CONFLICTUAL_CLASSES_RE[conflicting_classes_re]
186+
conflict.difference_update(white_list)
187+
if len(conflict) > 1:
188+
errors.append("Using %r, view %r contains conflicting classes: %r in %r (according to pattern %r)" % (theme_name, view.key, conflict, classes, conflicting_classes_re.pattern))
189+
except Exception:
190+
_logger.error("Using %r, view %r cannot be rendered", theme_name, view.key)
191+
errors.append("Using %r, view %r cannot be rendered" % (theme_name, view.key))
192+
return len(views)
193+
194+
view_count += check('no theme', self.env.ref('website.default_website'))
195+
websites_themes = self.env['website'].get_test_themes_websites()
196+
for website in websites_themes:
197+
view_count += check(website.name, website)
198+
_logger.info("Tested %s views", view_count)
199+
self.assertGreater(view_count, 2900, "Test should have checked many views")
200+
# Use this information to potentially update known possible conflicts.
201+
for known_classes in CONFLICTUAL_CLASSES:
202+
classes_inventory.difference_update(known_classes)
203+
for known_classes in CONFLICTUAL_CLASSES_RE.values():
204+
classes_inventory.difference_update(known_classes)
205+
for known_classes_re in CONFLICTUAL_CLASSES_RE:
206+
classes_inventory = [cl for cl in filter(lambda cl: not known_classes_re.findall(cl), classes_inventory)]
207+
_logger.info("Unknown classes encountered: %r", sorted(list(classes_inventory)))
208+
self.assertFalse(errors, "No error should have been collected")

0 commit comments

Comments
 (0)