import random import textwrap from odoo.http import _request_stack from odoo.tests.common import TransactionCase, tagged from odoo.tools import DotDict from odoo.addons.web_studio.controllers.main import WebStudioController @tagged('web_studio_normalization') class TestViewNormalization(TransactionCase): maxDiff = None def setUp(self): super(TestViewNormalization, self).setUp() random.seed('https://youtu.be/tFjNH9l6-sQ') self.session = DotDict({'debug': ''}) _request_stack.push(self) self.base_view = self.env.ref('base.view_partner_form') self.gantt_view = self.env['ir.ui.view'].create({ 'arch_base': """ """, 'model': 'res.partner', 'type': 'gantt', }) self.view = self.base_view.create({ 'arch_base': """

""", 'model': 'res.partner'}) self.studio_controller = WebStudioController() def _test_view_normalization(self, original, expected, view='form'): if view == 'gantt': view = self.gantt_view else: view = self.view original = original and textwrap.dedent(original) self.studio_controller._set_studio_view(view, original) studio_view = self.studio_controller._get_studio_view(view) studio_view = studio_view.with_context(load_all_views=True) normalized = studio_view.normalize() self.studio_controller._set_studio_view(view, normalized) self.env[self.view.model].with_context(studio=True, load_all_views=True).get_view(view.id, view.type) normalized = normalized and normalized.strip() expected = expected and textwrap.dedent(expected).strip() self.assertEqual(normalized, expected) # Flatten all xpath that target nodes added by studio itself def test_view_normalization_00(self): self._test_view_normalization(""" """, """ """) # Delete children of deleted nodes and reanchor siblings def test_view_normalization_01(self): self._test_view_normalization(""" """, """ """) # When there is no more sibling, we need to reanchor on the parent def test_view_normalization_02(self): self._test_view_normalization(""" """, """ """) # When a field is deleted, other xpath that targets it need to be reanchored. def test_view_normalization_03(self): self._test_view_normalization(""" """, """ """) # If there is nothing left in the studio view, delete it. def test_view_normalization_04(self): expected = '' self._test_view_normalization(""" """, expected) studio_view = self.studio_controller._set_studio_view(self.view, expected) studio_view = self.studio_controller._get_studio_view(self.view) self.assertEqual(len(studio_view), 0) # An after can become a replace if the following sibling has been removed. def test_view_normalization_05(self): self._test_view_normalization(""" """, """ """) # Multiple additions of fields should not appear if it was deleted def test_view_normalization_06(self): self._test_view_normalization(""" """, """ """) # Consecutive xpaths around a field that was moved away can be merged. def test_view_normalization_07(self): self._test_view_normalization(""" """, """ """) # A field that was added, then moved then deleted should not appear. def test_view_normalization_08(self): self._test_view_normalization(""" """, """ """) # Fields that were added then removed should not appear in the view at all, # and every other xpath that was using it should be reanchored elsewhere. def test_view_normalization_09(self): self._test_view_normalization(""" """, """ """) # When two fields are added after a given field, the second one will appear # before the first one. def test_view_normalization_10(self): self._test_view_normalization(""" """, """ """) # When we add a field after another one and replace the sibling of this one, # everything could be done in a single replace on the sibling node. def test_view_normalization_11(self): self._test_view_normalization(""" """, """ """) # When closest previous node has no name, the closest next node should be # used instead, provided it has a name. Also, attributes need to be handled # in a single xpath and alphabetically sorted. def test_view_normalization_12(self): self._test_view_normalization(""" PAGE 1 ZZZ PAGE 1 HELP """, """ PAGE 1 HELP PAGE 1 ZZZ """) # Changing an already existing attribute will generate a remove line for # the previous value and an addition line for the new value. The removing # line should not close the attributes xpath, both attributes need to be # redefined in a single xpath. def test_view_normalization_13(self): self._test_view_normalization(""" PAGE 1 PAGE 1 HELP """, """ PAGE 1 HELP PAGE 1 """) def test_view_normalization_14(self): # There is already a chatter on res.partner.form view, which is why # the resulting xpath is /div instead of /sheet. self._test_view_normalization("""
""", """
""") # Don't break on text with newlines def test_view_normalization_15(self): # New lines in text used to create a new line in the diff, desynchronizing # the diff lines and the tree elements iterator self._test_view_normalization("""

THIS IS A MULTILINE TITLE

""", """

THIS IS A MULTILINE TITLE

""") # Test anchoring next to studio fields def test_view_normalization_16(self): self._test_view_normalization(""" """, """ """) # Test replace of last element in arch def test_view_normalization_17(self): self._test_view_normalization(""" """, """ """) # Replace an existing element then add it back in but somewhere before # its original position def test_view_normalization_18(self): self._test_view_normalization(""" """, """ """) # Delete an existing element, then replace another element with the deleted # element further down def test_view_normalization_19(self): self._test_view_normalization(""" """, """ """) # Delete an existing element, then replace another element before the # original element with the latter def test_view_normalization_20(self): self._test_view_normalization(""" """, """ """) # template fields are appended to the templates, not to the kanban itself def test_view_normalization_21(self): self._test_view_normalization(""" """, """ """) # adding kanban and template fields while using absolute xpaths def test_view_normalization_22(self): self._test_view_normalization(""" lang """, """ lang """) # Correctly calculate the expr on flat views def test_view_normalization_23(self): self._test_view_normalization(""" date """, """ date """, 'gantt') # test that unnamed groups/pages are given a pseudo-random name attribute def test_view_normalization_24(self): random.seed("https://i.redd.it/pnyr50lf0jh01.png") self._test_view_normalization("""

hello world!

foo bar baz

spam eggs bacon

""", """

hello world!

foo bar baz

spam eggs bacon

""") random.seed() # test that unnamed pages are given a pseudo-random name attribute def test_view_normalization_25(self): self._test_view_normalization("""

hello world!

foo bar baz

spam eggs bacon

""", """

hello world!

foo bar baz

spam eggs bacon

""") # Adjacent addition/removal changes ends with correct xpath def test_view_normalization_26(self): self._test_view_normalization(""" """, """ """) # Test descendants equivalent nodes does not change children def test_view_normalization_27(self): self._test_view_normalization("""
""", """
""") # Test descendants equivalent nodes does not change children with unwrapped field def test_view_normalization_28(self): self._test_view_normalization("""
""", """
""") # Move an existing element (after its position) def test_view_normalization_29_1(self): self._test_view_normalization(""" """, """ """) # Move an existing element (before its position) def test_view_normalization_29_2(self): self._test_view_normalization(""" """, """ """) # Move two existing elements (after its position) def test_view_normalization_30_1(self): self._test_view_normalization(""" """, """ """) # Move two existing elements (before its position) def test_view_normalization_30_2(self): self._test_view_normalization(""" """, """ """) # Move two consequentive existing elements def test_view_normalization_30_3(self): self._test_view_normalization(""" """, """ """) # xpath based on a moved element def test_view_normalization_31(self): self._test_view_normalization(""" """, """ """) # Move an existing element and its attributes def test_view_normalization_32(self): self._test_view_normalization(""" Kikou """, """ Kikou """) # Move fields and one of them needs to generate an absolute xpath def test_view_normalization_33(self): self._test_view_normalization(""" """, """ """) def test_view_normalization_34(self): self._test_view_normalization(""" """, """ """) def test_view_normalization_29(self): self.view = self.base_view.create({ 'arch_base': '''

''', 'model': 'res.partner', 'type': 'form'}) self._test_view_normalization( '''
''', '''
''') def test_view_normalization_30(self): self.view = self.base_view.create({ 'arch_base': '''

''', 'model': 'res.partner', 'type': 'form'}) self._test_view_normalization( '''
''', '''
''') def test_view_normalization_31_2(self): self.view = self.base_view.create({ 'arch_base': '''

''', 'model': 'res.partner', 'type': 'form'}) # x_path_[1-3] target
# x_path_4 targets
self._test_view_normalization( '''
''', '''
''') # Removed line can be adjacent to added xpath with [not(@name)] def test_view_normalization_35(self): self.view = self.base_view.create({ 'arch_base': '''
hello
world
!
''', 'model': 'res.partner', 'type': 'form'}) # end result: orator:/hello/cruel/world/!? self._test_view_normalization( '''
cruel
orator:
!?
''', '''
!?
orator:
cruel
''') # Added line adjacent to comment should be ignored when xpathing def test_view_normalization_36(self): self.view = self.base_view.create({ 'arch_base': '''
''', 'model': 'res.partner', 'type': 'form'}) # end result: orator:/hello/cruel/world/!? self._test_view_normalization( '''
''', '''
''') # Identical fields in same xpath do not have mistaken identity def test_view_normalization_37(self): self.view = self.base_view.create({ 'arch_base': ''' ''', 'model': 'res.partner', 'type': 'form'}) self._test_view_normalization( '''