diff --git a/docs/advanced_topics/testing.rst b/docs/advanced_topics/testing.rst index b300ece11..7e48a822e 100644 --- a/docs/advanced_topics/testing.rst +++ b/docs/advanced_topics/testing.rst @@ -44,14 +44,21 @@ WagtailPageTests .. code-block:: python + from wagtail.tests.utils.form_data import nested_form_data, streamfield + def test_can_create_content_page(self): # Get the HomePage root_page = HomePage.objects.get(pk=2) # Assert that a ContentPage can be made here, with this POST data - self.assertCanCreate(root_page, ContentPage, { + self.assertCanCreate(root_page, ContentPage, nested_form_data({ 'title': 'About us', - 'body': 'Lorem ipsum dolor sit amet') + 'body': streamfield([ + ('text', 'Lorem ipsum dolor sit amet'), + ]) + })) + + See :ref:`form_data_test_helpers` for a set of functions useful for constructing POST data. .. automethod:: assertAllowedParentPageTypes @@ -79,3 +86,17 @@ WagtailPageTests # A HomePage can have ContentPage and EventIndex children self.assertAllowedParentPageTypes( HomePage, {ContentPage, EventIndex}) + + +.. _form_data_test_helpers: + +Form data helpers +================= + +.. automodule:: wagtail.tests.utils.form_data + + .. autofunction:: nested_form_data + + .. autofunction:: streamfield + + .. autofunction:: inline_formset diff --git a/wagtail/core/tests/test_tests.py b/wagtail/core/tests/test_tests.py index 4114ebd6e..3d33cd18b 100644 --- a/wagtail/core/tests/test_tests.py +++ b/wagtail/core/tests/test_tests.py @@ -5,6 +5,7 @@ from wagtail.tests.testapp.models import ( BusinessChild, BusinessIndex, BusinessNowherePage, BusinessSubIndex, EventIndex, EventPage, SimplePage, StreamPage) from wagtail.tests.utils import WagtailPageTests, WagtailTestUtils +from wagtail.tests.utils.form_data import inline_formset, nested_form_data, streamfield class TestAssertTagInHTML(WagtailTestUtils, TestCase): @@ -137,3 +138,60 @@ class TestWagtailPageTests(WagtailPageTests): self.assertAllowedParentPageTypes(BusinessIndex, all_but_business) with self.assertRaises(AssertionError): self.assertAllowedParentPageTypes(BusinessSubIndex, {BusinessSubIndex, BusinessIndex}) + + +class TestFormDataHelpers(TestCase): + def test_nested_form_data(self): + result = nested_form_data({ + 'foo': 'bar', + 'parent': { + 'child': 'field', + }, + }) + self.assertEqual( + result, + {'foo': 'bar', 'parent-child': 'field'} + ) + + def test_streamfield(self): + result = nested_form_data({'content': streamfield([ + ('text', 'Hello, world'), + ('text', 'Goodbye, world'), + ])}) + + self.assertEqual( + result, + { + 'content-count': '2', + 'content-0-type': 'text', + 'content-0-value': 'Hello, world', + 'content-0-order': 0, + 'content-0-deleted': '', + 'content-1-type': 'text', + 'content-1-value': 'Goodbye, world', + 'content-1-order': 1, + 'content-1-deleted': '', + } + ) + + def test_inline_formset(self): + result = nested_form_data({'lines': inline_formset([ + {'text': 'Hello'}, + {'text': 'World'}, + ])}) + + self.assertEqual( + result, + { + 'lines-TOTAL_FORMS': '2', + 'lines-INITIAL_FORMS': '0', + 'lines-MIN_NUM_FORMS': '0', + 'lines-MAX_NUM_FORMS': '1000', + 'lines-0-text': 'Hello', + 'lines-0-ORDER': '0', + 'lines-0-DELETE': '', + 'lines-1-text': 'World', + 'lines-1-ORDER': '1', + 'lines-1-DELETE': '', + } + ) diff --git a/wagtail/tests/utils/form_data.py b/wagtail/tests/utils/form_data.py new file mode 100644 index 000000000..bd9b97190 --- /dev/null +++ b/wagtail/tests/utils/form_data.py @@ -0,0 +1,115 @@ +""" +The ``assertCanCreate`` method requires page data to be passed in +the same format that the page edit form would submit. For complex +page types, it can be difficult to construct this data structure by hand; +the ``wagtail.tests.utils.form_data`` module provides a set of helper +functions to assist with this. +""" + + +def _nested_form_data(data): + if isinstance(data, dict): + items = data.items() + elif isinstance(data, list): + items = enumerate(data) + + for key, value in items: + key = str(key) + if isinstance(value, (dict, list)): + for child_keys, child_value in _nested_form_data(value): + yield [key] + child_keys, child_value + else: + yield [key], value + + +def nested_form_data(data): + """ + Translates a nested dict structure into a flat form data dict + with hyphen-separated keys. + + .. code-block:: python + + nested_form_data({ + 'foo': 'bar', + 'parent': { + 'child': 'field', + }, + }) + # Returns: {'foo': 'bar', 'parent-child': 'field'} + """ + return {'-'.join(key): value for key, value in _nested_form_data(data)} + + +def streamfield(items): + """ + Takes a list of (block_type, value) tuples and turns it in to + StreamField form data. Use this within a :func:`nested_form_data` + call, with the field name as the key. + + .. code-block:: python + + nested_form_data({'content': streamfield([ + ('text', 'Hello, world'), + ])}) + # Returns: + # { + # 'content-count': '1', + # 'content-0-type': 'text', + # 'content-0-value': 'Hello, world', + # 'content-0-order': 0, + # 'content-0-deleted': '', + # } + """ + def to_block(index, item): + block, value = item + return {'type': block, 'value': value, 'deleted': '', 'order': index} + data_dict = {str(index): to_block(index, item) + for index, item in enumerate(items)} + data_dict['count'] = str(len(data_dict)) + return data_dict + + +def inline_formset(items, initial=0, min=0, max=1000): + """ + Takes a list of form data for an InlineFormset and translates + it in to valid POST data. Use this within a :func:`nested_form_data` + call, with the formset relation name as the key. + + .. code-block:: python + + nested_form_data({'lines': inline_formset([ + {'text': 'Hello'}, + {'text': 'World'}, + ])}) + # Returns: + # { + # 'lines-TOTAL_FORMS': '2', + # 'lines-INITIAL_FORMS': '0', + # 'lines-MIN_NUM_FORMS': '0', + # 'lines-MAX_NUM_FORMS': '1000', + # 'lines-0-text': 'Hello', + # 'lines-0-ORDER': '0', + # 'lines-0-DELETE': '', + # 'lines-1-text': 'World', + # 'lines-1-ORDER': '1', + # 'lines-1-DELETE': '', + # } + """ + def to_form(index, item): + defaults = { + 'ORDER': str(index), + 'DELETE': '', + } + defaults.update(item) + return defaults + + data_dict = {str(index): to_form(index, item) + for index, item in enumerate(items)} + + data_dict.update({ + 'TOTAL_FORMS': str(len(data_dict)), + 'INITIAL_FORMS': str(initial), + 'MIN_NUM_FORMS': str(min), + 'MAX_NUM_FORMS': str(max), + }) + return data_dict