Home > Net >  Testing a Wagtail StructBlock with a StreamBlockField
Testing a Wagtail StructBlock with a StreamBlockField

Time:12-31

I'm attempting to run some unit tests on a StructBlock, which is composed of normal block fields and a StreamBlock.

The problem I'm running into is that I can construct a test that renders the block, but I cannot test the StreamBlock validation (i.e., I can't test the block's clean())

Stack

  • Python 3.9.6
  • Django 3.2
  • Wagtail 2.13
  • pytest 6.2
  • pytest-django 4.4

Block Definitions

MyStructBlock

    class MyStructBlock(blocks.StructBlock):
        image = ImageChooserBlock()

        heading = blocks.CharBlock(required=True)
        description = blocks.RichTextBlock(features=["bold", "italic"])
        links = blocks.StreamBlock([("link", LinkBlock())], icon="link", min_num=1, max_num=6)

        class Meta:
            icon = "list-ul"
            template = "blocks/two_column_with_link_list_block.html"

LinkBlock

    class LinkBlock(blocks.StructBlock):
        link_text = blocks.CharBlock()
        internal_link = blocks.PageChooserBlock()
        internal_document = DocumentChooserBlock()
        external_link = blocks.URLBlock()

With this setup I create this kind of block_definition:

{
   'image': <Image: Thumbnail of subject>, 
   'heading': 'SomeText', 
   'description': 'Hello, Son', 
   'links':  
     [
       {'value': 
         {
          'link_text': 'Walk trip thought region.', 
          'internal_link': None, 
          'document_link': None, 
          'external_link': 'www.example.com'
         }
       },
       {'value': {...}},
       {'value': {...}},
      ]
}

Then a test like this will work just fine:

   def test_structure():
      my_struct_block = MyStructBlock()
      block_def = block_definition
      rendered_block_html = BeautifulSoup(my_struct_block.render(value=block_def)))
      assert <<THINGS ABOUT THE STRUCTURE>>

The block renders and all is well, but when I try to clean the block everything starts to go sideways.

   def test_validation():
       my_struct_block = MyStructBlock()
       block_def = block_definition
       rendered_block_html = BeautifulSoup(my_struct_block.render(my_struct_block.clean(block_def)))

Will result in something like this:

self = <wagtail.core.blocks.field_block.RichTextBlock object at 0x7f874f430580>, value = 'Hello, Son'

    def value_for_form(self, value):
        # Rich text editors take the source-HTML string as input (and takes care
        # of expanding it for the purposes of the editor)
>       return value.source
E       AttributeError: 'str' object has no attribute 'source'

.direnv/python-3.9.6/lib/python3.9/site-packages/wagtail/core/blocks/field_block.py:602: AttributeError

Which is descriptive enough -- I get that it requires a Richtext Block definition -- but why the difference?

NOTE: I have tried defining the block_definition using RichText as the value for fields that expect RichText, this fails. If I remove the RichText field, the clean proceeds to fail on the dicts I used to define the LinkBlock.

Question

Is there a canonical way to set up a test like this that can handle complex blocks, so that a common source of block definition can test render as well as clean and render? Or, does this type of block, in some way, require an integration approach where I construct a Page with the complex blocks added as stream_data and then request the Page and use the response's rendering?

CodePudding user response:

Every block type has a corresponding 'native' data type for the data it expects to work with - for the simpler blocks, this data type is what you'd expect (e.g. a string for CharBlock, an Image instance for ImageChooserBlock) but for a few of the more complex ones, there's a custom type defined:

  • for RichTextBlock, the native type is wagtail.core.rich_text.RichText (which behaves similarly to a string, but also has a source property where e.g. page IDs in page links are kept intact)
  • for StreamBlock, the native type is wagtail.core.blocks.StreamValue (a sequence type, where each item is a StreamValue with block_type and value properties).

The render method will generally be quite forgiving if you use the wrong types (such as a string for RichTextBlock or a list of dicts for StreamBlock), since it's really just invoking your own template code. The clean method will be more picky, since it's running Python logic specific to each block.

Unfortunately the correct types to use for each block aren't really formally documented, and some of them are quite fiddly to construct (e.g. a StructValue needs to be passed a reference to the corresponding StructBlock) - outside of test code, there isn't much need to create these objects from scratch, because the data will usually be coming from some outside source instead (e.g. a form submission or the database), and each block will be responsible for converting that to its native type.

With that in mind, I'd recommend that you construct your data by piggybacking on the to_python method, which converts the JSON representation as stored in the database (consisting of just simple Python data types - integers, strings, lists, dicts) into the native data types:

block_def = my_struct_block.to_python({
   'image': 123,  # the image ID
   'heading': 'SomeText', 
   'description': 'Hello, Son', 
   'links':  
     [
       {'type': 'link', 'value': 
         {
          'link_text': 'Walk trip thought region.', 
          'internal_link': None, 
          'document_link': None, 
          'external_link': 'www.example.com'
         }
       },
       {'type': 'link', 'value': {...}},
       {'type': 'link', 'value': {...}},
      ]
})

my_struct_block.clean(block_def) will hopefully then succeed.

  • Related