161 lines
6 KiB
Python
161 lines
6 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
||
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||
|
|
|
||
|
|
"""Un SCSS personnalisé est une copie figée, comme une vue COW.
|
||
|
|
|
||
|
|
Personnaliser un site écrit du SCSS dans `ir_attachment`. Cette copie est
|
||
|
|
figée le jour où elle est écrite et continue d'employer les variables de
|
||
|
|
CETTE version. Un module peut les renommer d'un palier à l'autre : mesuré,
|
||
|
|
`website/.../primary_variables.scss` déclarait `$o-theme-font-number` en 12.0
|
||
|
|
et a remplacé tout le mécanisme en 13.0. Une personnalisation de 2020
|
||
|
|
demandait encore l'ancien nom, et le bundle s'arrêtait dessus.
|
||
|
|
|
||
|
|
Ce qui rend un tel détecteur utilisable n'est pas de trouver — c'est de ne
|
||
|
|
pas crier à tort. Sur le seul fichier mesuré, la première version rapportait
|
||
|
|
dix noms dont six n'étaient pas des manques : paramètres de mixin, variables
|
||
|
|
de boucle, arguments nommés d'`@include`. Ces tests portent surtout là-dessus.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import os
|
||
|
|
import sys
|
||
|
|
import unittest
|
||
|
|
|
||
|
|
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
|
|
sys.path.insert(0, os.path.join(REPO, "script", "odoo", "migration"))
|
||
|
|
|
||
|
|
import check_stale_scss as scss # noqa: E402
|
||
|
|
|
||
|
|
|
||
|
|
class TestWhatCountsAsAUse(unittest.TestCase):
|
||
|
|
def test_a_plain_dereference_is_a_use(self):
|
||
|
|
self.assertIn("color", scss.used_names("a { b: $color; }"))
|
||
|
|
|
||
|
|
def test_a_declaration_is_not_a_use(self):
|
||
|
|
self.assertNotIn("color", scss.used_names("$color: red;"))
|
||
|
|
|
||
|
|
def test_a_named_include_argument_is_not_a_use(self):
|
||
|
|
# LE faux positif mesuré : « @include o-position-absolute(
|
||
|
|
# $right: 50%) » se lisait comme l'usage d'un $right inexistant.
|
||
|
|
source = "@include o-position-absolute($right: 50%, $left: 50%);"
|
||
|
|
self.assertEqual(scss.used_names(source), set())
|
||
|
|
|
||
|
|
def test_a_default_value_is_not_a_use(self):
|
||
|
|
self.assertNotIn("size", scss.used_names("$size: 3 !default;"))
|
||
|
|
|
||
|
|
def test_the_name_is_not_truncated_to_dodge_the_colon(self):
|
||
|
|
# Une négation en tête de motif fait rétrograder le nom : le premier
|
||
|
|
# essai rapportait « $botto » au lieu d'écarter « $bottom ».
|
||
|
|
names = scss.used_names("a { b: $bottom-margin; }")
|
||
|
|
self.assertEqual(names, {"bottom-margin"})
|
||
|
|
|
||
|
|
def test_a_use_after_a_declaration_still_counts(self):
|
||
|
|
self.assertEqual(scss.used_names("$a: $b;"), {"b"})
|
||
|
|
|
||
|
|
|
||
|
|
class TestWhatTheFileBindsItself(unittest.TestCase):
|
||
|
|
def test_mixin_parameters_are_bound(self):
|
||
|
|
source = "@mixin thing($on, $off: 2) { a: $on; b: $off; }"
|
||
|
|
self.assertEqual(scss.bound_names(source), {"on", "off"})
|
||
|
|
|
||
|
|
def test_function_parameters_are_bound(self):
|
||
|
|
source = "@function f($value) { @return $value; }"
|
||
|
|
self.assertIn("value", scss.bound_names(source))
|
||
|
|
|
||
|
|
def test_each_loop_variables_are_bound(self):
|
||
|
|
source = "@each $key, $val in $map { a: $key; }"
|
||
|
|
bound = scss.bound_names(source)
|
||
|
|
self.assertIn("key", bound)
|
||
|
|
self.assertIn("val", bound)
|
||
|
|
|
||
|
|
def test_for_loop_variables_are_bound(self):
|
||
|
|
source = "@for $counter from 1 through 3 { a: $counter; }"
|
||
|
|
self.assertIn("counter", scss.bound_names(source))
|
||
|
|
|
||
|
|
|
||
|
|
class TestTheUrlIsSplitForTheFix(unittest.TestCase):
|
||
|
|
"""reset_asset veut le fichier ET le bundle ; l'URL porte les deux."""
|
||
|
|
|
||
|
|
def test_a_frontend_customization(self):
|
||
|
|
base, bundle = scss.split_custom_url(
|
||
|
|
"/website/static/src/scss/website.custom.web.assets_frontend.scss"
|
||
|
|
)
|
||
|
|
self.assertEqual(base, "/website/static/src/scss/website.scss")
|
||
|
|
self.assertEqual(bundle, "web.assets_frontend")
|
||
|
|
|
||
|
|
def test_a_common_customization(self):
|
||
|
|
base, bundle = scss.split_custom_url(
|
||
|
|
"/website/static/src/scss/options/colors/"
|
||
|
|
"user_theme_color_palette.custom.web.assets_common.scss"
|
||
|
|
)
|
||
|
|
self.assertTrue(base.endswith("user_theme_color_palette.scss"))
|
||
|
|
self.assertEqual(bundle, "web.assets_common")
|
||
|
|
|
||
|
|
|
||
|
|
class TestTheReport(unittest.TestCase):
|
||
|
|
def setUp(self):
|
||
|
|
from script.todo import todo_i18n
|
||
|
|
|
||
|
|
self.addCleanup(
|
||
|
|
setattr, todo_i18n, "_current_lang", todo_i18n._current_lang
|
||
|
|
)
|
||
|
|
todo_i18n._current_lang = "en"
|
||
|
|
|
||
|
|
def test_nothing_at_risk_says_so(self):
|
||
|
|
text = scss.render([], "db", "odoo13.0")
|
||
|
|
self.assertIn("✅", text)
|
||
|
|
|
||
|
|
def test_a_finding_names_the_attachment_and_the_variable(self):
|
||
|
|
text = scss.render(
|
||
|
|
[
|
||
|
|
(
|
||
|
|
1110,
|
||
|
|
"/a/b.custom.web.assets_frontend.scss",
|
||
|
|
["o-theme-font-number"],
|
||
|
|
)
|
||
|
|
],
|
||
|
|
"db",
|
||
|
|
"odoo13.0",
|
||
|
|
)
|
||
|
|
self.assertIn("1110", text)
|
||
|
|
self.assertIn("$o-theme-font-number", text)
|
||
|
|
|
||
|
|
def test_it_prints_a_command_that_can_be_pasted(self):
|
||
|
|
# Un diagnostic sans le geste qui répare oblige à rechercher les deux
|
||
|
|
# arguments de reset_asset au pire moment.
|
||
|
|
text = scss.render(
|
||
|
|
[(1110, "/a/b.custom.web.assets_frontend.scss", ["x"])],
|
||
|
|
"mydb",
|
||
|
|
"odoo13.0",
|
||
|
|
)
|
||
|
|
self.assertIn("reset_asset('/a/b.scss', 'web.assets_frontend')", text)
|
||
|
|
self.assertIn("-d mydb", text)
|
||
|
|
|
||
|
|
def test_it_warns_before_dropping(self):
|
||
|
|
text = scss.render(
|
||
|
|
[(1110, "/a/b.custom.web.assets_frontend.scss", ["x"])],
|
||
|
|
"db",
|
||
|
|
"odoo13.0",
|
||
|
|
)
|
||
|
|
self.assertIn("real customization", text)
|
||
|
|
|
||
|
|
|
||
|
|
class TestTheMigrationRunsIt(unittest.TestCase):
|
||
|
|
def test_it_is_checked_at_the_same_place_as_the_cow_views(self):
|
||
|
|
# Les deux prédictions valent avant le palier, pas après : c'est le
|
||
|
|
# seul moment où l'on peut encore arbitrer sans avoir tout rejoué.
|
||
|
|
import inspect
|
||
|
|
|
||
|
|
from script.todo.todo_upgrade import TodoUpgrade
|
||
|
|
|
||
|
|
source = inspect.getsource(TodoUpgrade.execute_odoo_upgrade)
|
||
|
|
self.assertIn("check_stale_scss.py", source)
|
||
|
|
self.assertLess(
|
||
|
|
source.index("check_stale_scss.py"),
|
||
|
|
source.index("4 - Upgrade version with OpenUpgrade"),
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
unittest.main()
|