From 4c30fa905d9d47b3a2ee82095b1fe56cc2ec2ab5 Mon Sep 17 00:00:00 2001 From: Preston Timmons Date: Sun, 30 Aug 2015 21:13:42 -0500 Subject: Rewrote form widget tests as proper unittests. This is preparation for landing the template-based widget rendering patch and goes a long way to making these tests more useful for future development. The old doctest heritage is strong here. --- tests/forms_tests/widget_tests/__init__.py | 0 tests/forms_tests/widget_tests/base.py | 9 + .../forms_tests/widget_tests/test_checkboxinput.py | 87 ++++ .../widget_tests/test_checkboxselectmultiple.py | 115 +++++ .../widget_tests/test_clearablefileinput.py | 126 ++++++ tests/forms_tests/widget_tests/test_dateinput.py | 47 ++ .../forms_tests/widget_tests/test_datetimeinput.py | 63 +++ tests/forms_tests/widget_tests/test_fileinput.py | 16 + tests/forms_tests/widget_tests/test_hiddeninput.py | 10 + .../widget_tests/test_multiplehiddeninput.py | 75 ++++ tests/forms_tests/widget_tests/test_multiwidget.py | 163 +++++++ .../widget_tests/test_nullbooleanselect.py | 64 +++ .../forms_tests/widget_tests/test_passwordinput.py | 26 ++ tests/forms_tests/widget_tests/test_radioselect.py | 84 ++++ tests/forms_tests/widget_tests/test_select.py | 250 +++++++++++ .../widget_tests/test_selectdatewidget.py | 479 +++++++++++++++++++++ .../widget_tests/test_selectmultiple.py | 125 ++++++ .../widget_tests/test_splitdatetimewidget.py | 51 +++ .../widget_tests/test_splithiddendatetimewidget.py | 42 ++ tests/forms_tests/widget_tests/test_textarea.py | 34 ++ tests/forms_tests/widget_tests/test_textinput.py | 78 ++++ tests/forms_tests/widget_tests/test_timeinput.py | 50 +++ 22 files changed, 1994 insertions(+) create mode 100644 tests/forms_tests/widget_tests/__init__.py create mode 100644 tests/forms_tests/widget_tests/base.py create mode 100644 tests/forms_tests/widget_tests/test_checkboxinput.py create mode 100644 tests/forms_tests/widget_tests/test_checkboxselectmultiple.py create mode 100644 tests/forms_tests/widget_tests/test_clearablefileinput.py create mode 100644 tests/forms_tests/widget_tests/test_dateinput.py create mode 100644 tests/forms_tests/widget_tests/test_datetimeinput.py create mode 100644 tests/forms_tests/widget_tests/test_fileinput.py create mode 100644 tests/forms_tests/widget_tests/test_hiddeninput.py create mode 100644 tests/forms_tests/widget_tests/test_multiplehiddeninput.py create mode 100644 tests/forms_tests/widget_tests/test_multiwidget.py create mode 100644 tests/forms_tests/widget_tests/test_nullbooleanselect.py create mode 100644 tests/forms_tests/widget_tests/test_passwordinput.py create mode 100644 tests/forms_tests/widget_tests/test_radioselect.py create mode 100644 tests/forms_tests/widget_tests/test_select.py create mode 100644 tests/forms_tests/widget_tests/test_selectdatewidget.py create mode 100644 tests/forms_tests/widget_tests/test_selectmultiple.py create mode 100644 tests/forms_tests/widget_tests/test_splitdatetimewidget.py create mode 100644 tests/forms_tests/widget_tests/test_splithiddendatetimewidget.py create mode 100644 tests/forms_tests/widget_tests/test_textarea.py create mode 100644 tests/forms_tests/widget_tests/test_textinput.py create mode 100644 tests/forms_tests/widget_tests/test_timeinput.py (limited to 'tests/forms_tests/widget_tests') diff --git a/tests/forms_tests/widget_tests/__init__.py b/tests/forms_tests/widget_tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/forms_tests/widget_tests/base.py b/tests/forms_tests/widget_tests/base.py new file mode 100644 index 0000000000..3127cb85f2 --- /dev/null +++ b/tests/forms_tests/widget_tests/base.py @@ -0,0 +1,9 @@ +from django.test import SimpleTestCase + + +class WidgetTest(SimpleTestCase): + beatles = (('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo')) + + def check_html(self, widget, name, value, html='', attrs=None, **kwargs): + output = widget.render(name, value, attrs=attrs, **kwargs) + self.assertHTMLEqual(output, html) diff --git a/tests/forms_tests/widget_tests/test_checkboxinput.py b/tests/forms_tests/widget_tests/test_checkboxinput.py new file mode 100644 index 0000000000..45d8191f75 --- /dev/null +++ b/tests/forms_tests/widget_tests/test_checkboxinput.py @@ -0,0 +1,87 @@ +from django.forms import CheckboxInput + +from .base import WidgetTest + + +class CheckboxInputTest(WidgetTest): + widget = CheckboxInput() + + def test_render_empty(self): + self.check_html(self.widget, 'is_cool', '', html='') + + def test_render_none(self): + self.check_html(self.widget, 'is_cool', None, html='') + + def test_render_false(self): + self.check_html(self.widget, 'is_cool', False, html='') + + def test_render_true(self): + self.check_html( + self.widget, 'is_cool', True, + html='' + ) + + def test_render_value(self): + """ + Using any value that's not in ('', None, False, True) will check the + checkbox and set the 'value' attribute. + """ + self.check_html( + self.widget, 'is_cool', 'foo', + html='', + ) + + def test_render_int(self): + """ + Integers are handled by value, not as booleans (#17114). + """ + self.check_html( + self.widget, 'is_cool', 0, + html='', + ) + self.check_html( + self.widget, 'is_cool', 1, + html='', + ) + + def test_render_check_test(self): + """ + You can pass 'check_test' to the constructor. This is a callable that + takes the value and returns True if the box should be checked. + """ + widget = CheckboxInput(check_test=lambda value: value.startswith('hello')) + self.check_html(widget, 'greeting', '', html=( + '' + )) + self.check_html(widget, 'greeting', 'hello', html=( + '' + )) + self.check_html(widget, 'greeting', 'hello there', html=( + '' + )) + self.check_html(widget, 'greeting', 'hello & goodbye', html=( + '' + )) + + def test_render_check_exception(self): + """ + Calling check_test() shouldn't swallow exceptions (#17888). + """ + widget = CheckboxInput( + check_test=lambda value: value.startswith('hello'), + ) + + with self.assertRaises(AttributeError): + widget.render('greeting', True) + + def test_value_from_datadict(self): + """ + The CheckboxInput widget will return False if the key is not found in + the data dictionary (because HTML form submission doesn't send any + result for unchecked checkboxes). + """ + self.assertFalse(self.widget.value_from_datadict({}, {}, 'testing')) + + def test_value_from_datadict_string_int(self): + value = self.widget.value_from_datadict({'testing': '0'}, {}, 'testing') + self.assertEqual(value, True) diff --git a/tests/forms_tests/widget_tests/test_checkboxselectmultiple.py b/tests/forms_tests/widget_tests/test_checkboxselectmultiple.py new file mode 100644 index 0000000000..9e594fba37 --- /dev/null +++ b/tests/forms_tests/widget_tests/test_checkboxselectmultiple.py @@ -0,0 +1,115 @@ +from django.forms import CheckboxSelectMultiple + +from .base import WidgetTest + + +class CheckboxSelectMultipleTest(WidgetTest): + widget = CheckboxSelectMultiple() + + def test_render_value(self): + self.check_html(self.widget, 'beatles', ['J'], choices=self.beatles, html=( + """""" + )) + + def test_render_value_multiple(self): + self.check_html(self.widget, 'beatles', ['J', 'P'], choices=self.beatles, html=( + """""" + )) + + def test_render_none(self): + """ + If the value is None, none of the options are selected. + """ + self.check_html(self.widget, 'beatles', None, choices=self.beatles, html=( + """""" + )) + + def test_nested_choices(self): + nested_choices = ( + ('unknown', 'Unknown'), + ('Audio', (('vinyl', 'Vinyl'), ('cd', 'CD'))), + ('Video', (('vhs', 'VHS'), ('dvd', 'DVD'))), + ) + html = """ + + """ + self.check_html( + self.widget, 'nestchoice', ('vinyl', 'dvd'), + choices=nested_choices, attrs={'id': 'media'}, html=html, + ) + + def test_separate_ids(self): + """ + Each input gets a separate ID. + """ + choices = [('a', 'A'), ('b', 'B'), ('c', 'C')] + html = """ + + """ + self.check_html(self.widget, 'letters', ['a', 'c'], choices=choices, attrs={'id': 'abc'}, html=html) + + def test_separate_ids_constructor(self): + """ + Each input gets a separate ID when the ID is passed to the constructor. + """ + widget = CheckboxSelectMultiple(attrs={'id': 'abc'}) + choices = [('a', 'A'), ('b', 'B'), ('c', 'C')] + html = """ + + """ + self.check_html(widget, 'letters', ['a', 'c'], choices=choices, html=html) diff --git a/tests/forms_tests/widget_tests/test_clearablefileinput.py b/tests/forms_tests/widget_tests/test_clearablefileinput.py new file mode 100644 index 0000000000..5a24a85e2a --- /dev/null +++ b/tests/forms_tests/widget_tests/test_clearablefileinput.py @@ -0,0 +1,126 @@ +from django.core.files.uploadedfile import SimpleUploadedFile +from django.forms import ClearableFileInput +from django.utils import six +from django.utils.encoding import python_2_unicode_compatible + +from .base import WidgetTest + + +@python_2_unicode_compatible +class FakeFieldFile(object): + """ + Quacks like a FieldFile (has a .url and unicode representation), but + doesn't require us to care about storages etc. + """ + url = 'something' + + def __str__(self): + return self.url + + +class ClearableFileInputTest(WidgetTest): + widget = ClearableFileInput() + + def test_clear_input_renders(self): + """ + A ClearableFileInput with is_required False and rendered with an + initial value that is a file renders a clear checkbox. + """ + self.check_html(self.widget, 'myfile', FakeFieldFile(), html=( + """ + Currently: something + +
+ Change: + """ + )) + + def test_html_escaped(self): + """ + A ClearableFileInput should escape name, filename, and URL + when rendering HTML (#15182). + """ + @python_2_unicode_compatible + class StrangeFieldFile(object): + url = "something?chapter=1§=2©=3&lang=en" + + def __str__(self): + return '''something
.jpg''' + + widget = ClearableFileInput() + field = StrangeFieldFile() + output = widget.render('my
file', field) + self.assertNotIn(field.url, output) + self.assertIn('href="something?chapter=1&sect=2&copy=3&lang=en"', output) + self.assertNotIn(six.text_type(field), output) + self.assertIn('something<div onclick="alert('oops')">.jpg', output) + self.assertIn('my<div>file', output) + self.assertNotIn('my
file', output) + + def test_html_does_not_mask_exceptions(self): + """ + A ClearableFileInput should not mask exceptions produced while + checking that it has a value. + """ + @python_2_unicode_compatible + class FailingURLFieldFile(object): + @property + def url(self): + raise RuntimeError('Canary') + + def __str__(self): + return 'value' + + widget = ClearableFileInput() + field = FailingURLFieldFile() + with self.assertRaisesMessage(RuntimeError, 'Canary'): + widget.render('myfile', field) + + def test_clear_input_renders_only_if_not_required(self): + """ + A ClearableFileInput with is_required=False does not render a clear + checkbox. + """ + widget = ClearableFileInput() + widget.is_required = True + self.check_html(widget, 'myfile', FakeFieldFile(), html=( + """ + Currently: something
+ Change: + """ + )) + + def test_clear_input_renders_only_if_initial(self): + """ + A ClearableFileInput instantiated with no initial value does not render + a clear checkbox. + """ + self.check_html(self.widget, 'myfile', None, html='') + + def test_clear_input_checked_returns_false(self): + """ + ClearableFileInput.value_from_datadict returns False if the clear + checkbox is checked, if not required. + """ + value = self.widget.value_from_datadict( + data={'myfile-clear': True}, + files={}, + name='myfile', + ) + self.assertEqual(value, False) + + def test_clear_input_checked_returns_false_only_if_not_required(self): + """ + ClearableFileInput.value_from_datadict never returns False if the field + is required. + """ + widget = ClearableFileInput() + widget.is_required = True + field = SimpleUploadedFile('something.txt', b'content') + + value = widget.value_from_datadict( + data={'myfile-clear': True}, + files={'myfile': field}, + name='myfile', + ) + self.assertEqual(value, field) diff --git a/tests/forms_tests/widget_tests/test_dateinput.py b/tests/forms_tests/widget_tests/test_dateinput.py new file mode 100644 index 0000000000..3cf15fed8a --- /dev/null +++ b/tests/forms_tests/widget_tests/test_dateinput.py @@ -0,0 +1,47 @@ +from datetime import date + +from django.forms import DateInput +from django.test import override_settings +from django.utils import translation + +from .base import WidgetTest + + +class DateInputTest(WidgetTest): + widget = DateInput() + + def test_render_none(self): + self.check_html(self.widget, 'date', None, html='') + + def test_render_value(self): + d = date(2007, 9, 17) + self.assertEqual(str(d), '2007-09-17') + + self.check_html(self.widget, 'date', d, html='') + self.check_html(self.widget, 'date', date(2007, 9, 17), html=( + '' + )) + + def test_string(self): + """ + Should be able to initialize from a string value. + """ + self.check_html(self.widget, 'date', '2007-09-17', html=( + '' + )) + + def test_format(self): + """ + Use 'format' to change the way a value is displayed. + """ + d = date(2007, 9, 17) + widget = DateInput(format='%d/%m/%Y', attrs={'type': 'date'}) + self.check_html(widget, 'date', d, html='') + + @override_settings(USE_L10N=True) + @translation.override('de-at') + def test_l10n(self): + self.check_html( + self.widget, 'date', date(2007, 9, 17), + html='', + ) diff --git a/tests/forms_tests/widget_tests/test_datetimeinput.py b/tests/forms_tests/widget_tests/test_datetimeinput.py new file mode 100644 index 0000000000..50fd7f5442 --- /dev/null +++ b/tests/forms_tests/widget_tests/test_datetimeinput.py @@ -0,0 +1,63 @@ +from datetime import datetime + +from django.forms import DateTimeInput +from django.test import override_settings +from django.utils import translation + +from .base import WidgetTest + + +class DateTimeInputTest(WidgetTest): + widget = DateTimeInput() + + def test_render_none(self): + self.check_html(self.widget, 'date', None, '') + + def test_render_value(self): + """ + The microseconds are trimmed on display, by default. + """ + d = datetime(2007, 9, 17, 12, 51, 34, 482548) + self.assertEqual(str(d), '2007-09-17 12:51:34.482548') + self.check_html(self.widget, 'date', d, html=( + '' + )) + self.check_html(self.widget, 'date', datetime(2007, 9, 17, 12, 51, 34), html=( + '' + )) + self.check_html(self.widget, 'date', datetime(2007, 9, 17, 12, 51), html=( + '' + )) + + def test_render_formatted(self): + """ + Use 'format' to change the way a value is displayed. + """ + widget = DateTimeInput( + format='%d/%m/%Y %H:%M', attrs={'type': 'datetime'}, + ) + d = datetime(2007, 9, 17, 12, 51, 34, 482548) + self.check_html(widget, 'date', d, html='') + + @override_settings(USE_L10N=True) + @translation.override('de-at') + def test_l10n(self): + d = datetime(2007, 9, 17, 12, 51, 34, 482548) + self.check_html(self.widget, 'date', d, html=( + '' + )) + + @override_settings(USE_L10N=True) + @translation.override('de-at') + def test_locale_aware(self): + d = datetime(2007, 9, 17, 12, 51, 34, 482548) + with self.settings(USE_L10N=False): + self.check_html( + self.widget, 'date', d, + html='', + ) + with translation.override('es'): + self.check_html( + self.widget, 'date', d, + html='', + ) diff --git a/tests/forms_tests/widget_tests/test_fileinput.py b/tests/forms_tests/widget_tests/test_fileinput.py new file mode 100644 index 0000000000..eb1c9d81ed --- /dev/null +++ b/tests/forms_tests/widget_tests/test_fileinput.py @@ -0,0 +1,16 @@ +from django.forms import FileInput + +from .base import WidgetTest + + +class FileInputTest(WidgetTest): + widget = FileInput() + + def test_render(self): + """ + FileInput widgets never render the value attribute. The old value + isn't useful if a form is updated or an error occurred. + """ + self.check_html(self.widget, 'email', 'test@example.com', html='') + self.check_html(self.widget, 'email', '', html='') + self.check_html(self.widget, 'email', None, html='') diff --git a/tests/forms_tests/widget_tests/test_hiddeninput.py b/tests/forms_tests/widget_tests/test_hiddeninput.py new file mode 100644 index 0000000000..039e89d5cc --- /dev/null +++ b/tests/forms_tests/widget_tests/test_hiddeninput.py @@ -0,0 +1,10 @@ +from django.forms import HiddenInput + +from .base import WidgetTest + + +class HiddenInputTest(WidgetTest): + widget = HiddenInput() + + def test_render(self): + self.check_html(self.widget, 'email', '', html='') diff --git a/tests/forms_tests/widget_tests/test_multiplehiddeninput.py b/tests/forms_tests/widget_tests/test_multiplehiddeninput.py new file mode 100644 index 0000000000..d1c1ae6fa5 --- /dev/null +++ b/tests/forms_tests/widget_tests/test_multiplehiddeninput.py @@ -0,0 +1,75 @@ +from django.forms import MultipleHiddenInput + +from .base import WidgetTest + + +class MultipleHiddenInputTest(WidgetTest): + widget = MultipleHiddenInput() + + def test_render_single(self): + self.check_html( + self.widget, 'email', ['test@example.com'], + html='', + ) + + def test_render_multiple(self): + self.check_html( + self.widget, 'email', ['test@example.com', 'foo@example.com'], + html=( + '\n' + '' + ), + ) + + def test_render_attrs(self): + self.check_html( + self.widget, 'email', ['test@example.com'], attrs={'class': 'fun'}, + html='', + ) + + def test_render_attrs_multiple(self): + self.check_html( + self.widget, 'email', ['test@example.com', 'foo@example.com'], attrs={'class': 'fun'}, + html=( + '\n' + '' + ), + ) + + def test_render_attrs_constructor(self): + widget = MultipleHiddenInput(attrs={'class': 'fun'}) + self.check_html(widget, 'email', [], '') + self.check_html( + widget, 'email', ['foo@example.com'], + html='', + ) + self.check_html( + widget, 'email', ['foo@example.com', 'test@example.com'], + html=( + '\n' + '' + ), + ) + self.check_html( + widget, 'email', ['foo@example.com'], attrs={'class': 'special'}, + html='', + ) + + def test_render_empty(self): + self.check_html(self.widget, 'email', [], '') + + def test_render_none(self): + self.check_html(self.widget, 'email', None, '') + + def test_render_increment_id(self): + """ + Each input should get a separate ID. + """ + self.check_html( + self.widget, 'letters', ['a', 'b', 'c'], attrs={'id': 'hideme'}, + html=( + '\n' + '\n' + '' + ), + ) diff --git a/tests/forms_tests/widget_tests/test_multiwidget.py b/tests/forms_tests/widget_tests/test_multiwidget.py new file mode 100644 index 0000000000..bb6f8bfc4f --- /dev/null +++ b/tests/forms_tests/widget_tests/test_multiwidget.py @@ -0,0 +1,163 @@ +import copy +from datetime import datetime + +from django.forms import ( + CharField, FileInput, MultipleChoiceField, MultiValueField, MultiWidget, + RadioSelect, SelectMultiple, SplitDateTimeField, SplitDateTimeWidget, + TextInput, +) + +from .base import WidgetTest + + +class MyMultiWidget(MultiWidget): + def decompress(self, value): + if value: + return value.split('__') + return ['', ''] + + +class ComplexMultiWidget(MultiWidget): + def __init__(self, attrs=None): + widgets = ( + TextInput(), + SelectMultiple(choices=WidgetTest.beatles), + SplitDateTimeWidget(), + ) + super(ComplexMultiWidget, self).__init__(widgets, attrs) + + def decompress(self, value): + if value: + data = value.split(',') + return [ + data[0], list(data[1]), datetime.strptime(data[2], "%Y-%m-%d %H:%M:%S") + ] + return [None, None, None] + + def format_output(self, rendered_widgets): + return '\n'.join(rendered_widgets) + + +class ComplexField(MultiValueField): + def __init__(self, required=True, widget=None, label=None, initial=None): + fields = ( + CharField(), + MultipleChoiceField(choices=WidgetTest.beatles), + SplitDateTimeField(), + ) + super(ComplexField, self).__init__( + fields, required, widget, label, initial, + ) + + def compress(self, data_list): + if data_list: + return '%s,%s,%s' % ( + data_list[0], ''.join(data_list[1]), data_list[2], + ) + return None + + +class DeepCopyWidget(MultiWidget): + """ + Used to test MultiWidget.__deepcopy__(). + """ + def __init__(self, choices=[]): + widgets = [ + RadioSelect(choices=choices), + TextInput, + ] + super(DeepCopyWidget, self).__init__(widgets) + + def _set_choices(self, choices): + """ + When choices are set for this widget, we want to pass those along to + the Select widget. + """ + self.widgets[0].choices = choices + + def _get_choices(self): + """ + The choices for this widget are the Select widget's choices. + """ + return self.widgets[0].choices + choices = property(_get_choices, _set_choices) + + +class MultiWidgetTest(WidgetTest): + + def test_text_inputs(self): + widget = MyMultiWidget( + widgets=( + TextInput(attrs={'class': 'big'}), + TextInput(attrs={'class': 'small'}), + ) + ) + self.check_html(widget, 'name', ['john', 'lennon'], html=( + '' + '' + )) + self.check_html(widget, 'name', 'john__lennon', html=( + '' + '' + )) + self.check_html(widget, 'name', 'john__lennon', attrs={'id': 'foo'}, html=( + '' + '' + )) + + def test_constructor_attrs(self): + widget = MyMultiWidget( + widgets=( + TextInput(attrs={'class': 'big'}), + TextInput(attrs={'class': 'small'}), + ), + attrs={'id': 'bar'}, + ) + self.check_html(widget, 'name', ['john', 'lennon'], html=( + '' + '' + )) + + def test_needs_multipart_true(self): + """ + needs_multipart_form should be True if any widgets need it. + """ + widget = MyMultiWidget(widgets=(TextInput(), FileInput())) + self.assertTrue(widget.needs_multipart_form) + + def test_needs_multipart_false(self): + """ + needs_multipart_form should be False if no widgets need it. + """ + widget = MyMultiWidget(widgets=(TextInput(), TextInput())) + self.assertFalse(widget.needs_multipart_form) + + def test_nested_multiwidget(self): + """ + MultiWidgets can be composed of other MultiWidgets. + """ + widget = ComplexMultiWidget() + self.check_html(widget, 'name', 'some text,JP,2007-04-25 06:24:00', html=( + """ + + + + + """ + )) + + def test_deepcopy(self): + """ + MultiWidget should define __deepcopy__() (#12048). + """ + w1 = DeepCopyWidget(choices=[1, 2, 3]) + w2 = copy.deepcopy(w1) + w2.choices = [4, 5, 6] + # w2 ought to be independent of w1, since MultiWidget ought + # to make a copy of its sub-widgets when it is copied. + self.assertEqual(w1.choices, [1, 2, 3]) diff --git a/tests/forms_tests/widget_tests/test_nullbooleanselect.py b/tests/forms_tests/widget_tests/test_nullbooleanselect.py new file mode 100644 index 0000000000..42b1dbf7c1 --- /dev/null +++ b/tests/forms_tests/widget_tests/test_nullbooleanselect.py @@ -0,0 +1,64 @@ +from django.forms import NullBooleanSelect +from django.test import override_settings +from django.utils import translation + +from .base import WidgetTest + + +class NullBooleanSelectTest(WidgetTest): + widget = NullBooleanSelect() + + def test_render_true(self): + self.check_html(self.widget, 'is_cool', True, html=( + """""" + )) + + def test_render_false(self): + self.check_html(self.widget, 'is_cool', False, html=( + """""" + )) + + def test_render_none(self): + self.check_html(self.widget, 'is_cool', None, html=( + """""" + )) + + def test_render_value(self): + self.check_html(self.widget, 'is_cool', '2', html=( + """""" + )) + + @override_settings(USE_L10N=True) + def test_l10n(self): + """ + Ensure that the NullBooleanSelect widget's options are lazily + localized (#17190). + """ + widget = NullBooleanSelect() + + with translation.override('de-at'): + self.check_html(widget, 'id_bool', True, html=( + """ + + """ + )) diff --git a/tests/forms_tests/widget_tests/test_passwordinput.py b/tests/forms_tests/widget_tests/test_passwordinput.py new file mode 100644 index 0000000000..4cb7c4ef47 --- /dev/null +++ b/tests/forms_tests/widget_tests/test_passwordinput.py @@ -0,0 +1,26 @@ +from django.forms import PasswordInput + +from .base import WidgetTest + + +class PasswordInputTest(WidgetTest): + widget = PasswordInput() + + def test_render(self): + self.check_html(self.widget, 'password', '', html='') + + def test_render_ignore_value(self): + self.check_html(self.widget, 'password', 'secret', html='') + + def test_render_value_true(self): + """ + The render_value argument lets you specify whether the widget should + render its value. For security reasons, this is off by default. + """ + widget = PasswordInput(render_value=True) + self.check_html(widget, 'password', '', html='') + self.check_html(widget, 'password', None, html='') + self.check_html( + widget, 'password', 'test@example.com', + html='', + ) diff --git a/tests/forms_tests/widget_tests/test_radioselect.py b/tests/forms_tests/widget_tests/test_radioselect.py new file mode 100644 index 0000000000..e37dcf7725 --- /dev/null +++ b/tests/forms_tests/widget_tests/test_radioselect.py @@ -0,0 +1,84 @@ +from django.forms import RadioSelect + +from .base import WidgetTest + + +class RadioSelectTest(WidgetTest): + widget = RadioSelect() + + def test_render(self): + self.check_html(self.widget, 'beatle', 'J', choices=self.beatles, html=( + """
    +
  • +
  • +
  • +
  • +
""" + )) + + def test_nested_choices(self): + nested_choices = ( + ('unknown', 'Unknown'), + ('Audio', (('vinyl', 'Vinyl'), ('cd', 'CD'))), + ('Video', (('vhs', 'VHS'), ('dvd', 'DVD'))), + ) + html = """ +
    +
  • + +
  • +
  • Audio
      +
    • + +
    • +
    • +
  • +
  • Video
      +
    • +
    • + +
    • +
  • +
+ """ + self.check_html( + self.widget, 'nestchoice', 'dvd', choices=nested_choices, + attrs={'id': 'media'}, html=html, + ) + + def test_constructor_attrs(self): + """ + Attributes provided at instantiation are passed to the constituent + inputs. + """ + widget = RadioSelect(attrs={'id': 'foo'}) + html = """ +
    +
  • + +
  • +
  • +
  • +
  • +
+ """ + self.check_html(widget, 'beatle', 'J', choices=self.beatles, html=html) + + def test_render_attrs(self): + """ + Attributes provided at render-time are passed to the constituent + inputs. + """ + html = """ +
    +
  • + +
  • +
  • +
  • +
  • +
+ """ + self.check_html(self.widget, 'beatle', 'J', choices=self.beatles, attrs={'id': 'bar'}, html=html) diff --git a/tests/forms_tests/widget_tests/test_select.py b/tests/forms_tests/widget_tests/test_select.py new file mode 100644 index 0000000000..86ef355c62 --- /dev/null +++ b/tests/forms_tests/widget_tests/test_select.py @@ -0,0 +1,250 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import copy + +from django.forms import Select +from django.utils.safestring import mark_safe + +from .base import WidgetTest + + +class SelectTest(WidgetTest): + widget = Select() + nested_widget = Select(choices=( + ('outer1', 'Outer 1'), + ('Group "1"', (('inner1', 'Inner 1'), ('inner2', 'Inner 2'))), + )) + + def test_render(self): + self.check_html(self.widget, 'beatle', 'J', choices=self.beatles, html=( + """""" + )) + + def test_render_none(self): + """ + If the value is None, none of the options are selected. + """ + self.check_html(self.widget, 'beatle', None, choices=self.beatles, html=( + """""" + )) + + def test_render_label_value(self): + """ + If the value corresponds to a label (but not to an option value), none + of the options are selected. + """ + self.check_html(self.widget, 'beatle', 'John', choices=self.beatles, html=( + """""" + )) + + def test_render_selected(self): + """ + Only one option can be selected (#8103). + """ + choices = [('0', '0'), ('1', '1'), ('2', '2'), ('3', '3'), ('0', 'extra')] + + self.check_html(self.widget, 'choices', '0', choices=choices, html=( + """""" + )) + + def test_constructor_attrs(self): + """ + Select options shouldn't inherit the parent widget attrs. + """ + widget = Select( + attrs={'class': 'super', 'id': 'super'}, + choices=[(1, 1), (2, 2), (3, 3)], + ) + self.check_html(widget, 'num', 2, html=( + """""" + )) + + def test_compare_to_str(self): + """ + The value is compared to its str(). + """ + self.check_html( + self.widget, 'num', 2, + choices=[('1', '1'), ('2', '2'), ('3', '3')], + html=( + """""" + ), + ) + self.check_html( + self.widget, 'num', '2', + choices=[(1, 1), (2, 2), (3, 3)], + html=( + """""" + ), + ) + self.check_html( + self.widget, 'num', 2, + choices=[(1, 1), (2, 2), (3, 3)], + html=( + """""" + ), + ) + + def test_choices_constuctor(self): + widget = Select(choices=[(1, 1), (2, 2), (3, 3)]) + self.check_html(widget, 'num', 2, html=( + """""" + )) + + def test_choices_constructor_generator(self): + """ + If choices is passed to the constructor and is a generator, it can be + iterated over multiple times without getting consumed. + """ + def get_choices(): + for i in range(5): + yield (i, i) + + widget = Select(choices=get_choices()) + self.check_html(widget, 'num', 2, html=( + """""" + )) + self.check_html(widget, 'num', 3, html=( + """""" + )) + + def test_choices_constuctor_and_render(self): + """ + If 'choices' is passed to both the constructor and render(), then + they'll both be in the output. + """ + widget = Select(choices=[(1, 1), (2, 2), (3, 3)]) + self.check_html(widget, 'num', 2, choices=[(4, 4), (5, 5)], html=( + """""" + )) + + def test_choices_escaping(self): + choices = (('bad', 'you & me'), ('good', mark_safe('you > me'))) + self.check_html(self.widget, 'escape', None, choices=choices, html=( + """""" + )) + + def test_choices_unicode(self): + self.check_html( + self.widget, 'email', 'ŠĐĆŽćžšđ', + choices=[('ŠĐĆŽćžšđ', 'ŠĐabcĆŽćžšđ'), ('ćžšđ', 'abcćžšđ')], + html=( + """""" + ), + ) + + def test_choices_optgroup(self): + """ + Choices can be nested one level in order to create HTML optgroups. + """ + self.check_html(self.nested_widget, 'nestchoice', None, html=( + """""" + )) + + def test_choices_select_outer(self): + self.check_html(self.nested_widget, 'nestchoice', 'outer1', html=( + """""" + )) + + def test_choices_select_inner(self): + self.check_html(self.nested_widget, 'nestchoice', 'inner1', html=( + """""" + )) + + def test_deepcopy(self): + """ + __deepcopy__() should copy all attributes properly (#25085). + """ + widget = Select() + obj = copy.deepcopy(widget) + self.assertIsNot(widget, obj) + self.assertEqual(widget.choices, obj.choices) + self.assertIsNot(widget.choices, obj.choices) + self.assertEqual(widget.attrs, obj.attrs) + self.assertIsNot(widget.attrs, obj.attrs) diff --git a/tests/forms_tests/widget_tests/test_selectdatewidget.py b/tests/forms_tests/widget_tests/test_selectdatewidget.py new file mode 100644 index 0000000000..e95c083532 --- /dev/null +++ b/tests/forms_tests/widget_tests/test_selectdatewidget.py @@ -0,0 +1,479 @@ +from datetime import date + +from django.forms import DateField, Form, SelectDateWidget +from django.test import override_settings +from django.utils import translation +from django.utils.dates import MONTHS_AP + +from .base import WidgetTest + + +class SelectDateWidgetTest(WidgetTest): + maxDiff = None + widget = SelectDateWidget( + years=('2007', '2008', '2009', '2010', '2011', '2012', '2013', '2014', '2015', '2016'), + ) + + def test_render_empty(self): + self.check_html(self.widget, 'mydate', '', html=( + """ + + + + + + """ + )) + + def test_render_none(self): + """ + Rendering the None or '' values should yield the same output. + """ + self.assertHTMLEqual( + self.widget.render('mydate', None), + self.widget.render('mydate', ''), + ) + + def test_render_string(self): + self.check_html(self.widget, 'mydate', '2010-04-15', html=( + """ + + + + + + """ + )) + + def test_render_datetime(self): + self.assertHTMLEqual( + self.widget.render('mydate', date(2010, 4, 15)), + self.widget.render('mydate', '2010-04-15'), + ) + + def test_render_invalid_date(self): + """ + Invalid dates should still render the failed date. + """ + self.check_html(self.widget, 'mydate', '2010-02-31', html=( + """ + + + + + + """ + )) + + def test_custom_months(self): + widget = SelectDateWidget(months=MONTHS_AP, years=('2013',)) + self.check_html(widget, 'mydate', '', html=( + """ + + + + + + """ + )) + + def test_selectdate_required(self): + class GetNotRequiredDate(Form): + mydate = DateField(widget=SelectDateWidget, required=False) + + class GetRequiredDate(Form): + mydate = DateField(widget=SelectDateWidget, required=True) + + self.assertFalse(GetNotRequiredDate().fields['mydate'].widget.is_required) + self.assertTrue(GetRequiredDate().fields['mydate'].widget.is_required) + + def test_selectdate_empty_label(self): + w = SelectDateWidget(years=('2014',), empty_label='empty_label') + + # Rendering the default state with empty_label setted as string. + self.assertInHTML('', w.render('mydate', ''), count=3) + + w = SelectDateWidget(years=('2014',), empty_label=('empty_year', 'empty_month', 'empty_day')) + + # Rendering the default state with empty_label tuple. + self.assertHTMLEqual( + w.render('mydate', ''), + """ + + + + + + """, + ) + + self.assertRaisesMessage(ValueError, 'empty_label list/tuple must have 3 elements.', + SelectDateWidget, years=('2014',), empty_label=('not enough', 'values')) + + @override_settings(USE_L10N=True) + @translation.override('nl') + def test_l10n(self): + w = SelectDateWidget( + years=('2007', '2008', '2009', '2010', '2011', '2012', '2013', '2014', '2015', '2016') + ) + self.assertEqual( + w.value_from_datadict({'date_year': '2010', 'date_month': '8', 'date_day': '13'}, {}, 'date'), + '13-08-2010', + ) + + self.assertHTMLEqual( + w.render('date', '13-08-2010'), + """ + + + + + + """, + ) + + # Even with an invalid date, the widget should reflect the entered value (#17401). + self.assertEqual(w.render('mydate', '2010-02-30').count('selected="selected"'), 3) + + # Years before 1900 should work. + w = SelectDateWidget(years=('1899',)) + self.assertEqual( + w.value_from_datadict({'date_year': '1899', 'date_month': '8', 'date_day': '13'}, {}, 'date'), + '13-08-1899', + ) diff --git a/tests/forms_tests/widget_tests/test_selectmultiple.py b/tests/forms_tests/widget_tests/test_selectmultiple.py new file mode 100644 index 0000000000..502aba3bf4 --- /dev/null +++ b/tests/forms_tests/widget_tests/test_selectmultiple.py @@ -0,0 +1,125 @@ +from django.forms import SelectMultiple + +from .base import WidgetTest + + +class SelectMultipleTest(WidgetTest): + widget = SelectMultiple() + numeric_choices = (('0', '0'), ('1', '1'), ('2', '2'), ('3', '3'), ('0', 'extra')) + + def test_render_selected(self): + self.check_html(self.widget, 'beatles', ['J'], choices=self.beatles, html=( + """""" + )) + + def test_render_multiple_selected(self): + self.check_html(self.widget, 'beatles', ['J', 'P'], choices=self.beatles, html=( + """""" + )) + + def test_render_none(self): + """ + If the value is None, none of the options are selected. + """ + self.check_html(self.widget, 'beatles', None, choices=self.beatles, html=( + """""" + )) + + def test_render_value_label(self): + """ + If the value corresponds to a label (but not to an option value), none + of the options are selected. + """ + self.check_html(self.widget, 'beatles', ['John'], choices=self.beatles, html=( + """""" + )) + + def test_multiple_options_same_value(self): + """ + Multiple options with the same value can be selected (#8103). + """ + self.check_html(self.widget, 'choices', ['0'], choices=self.numeric_choices, html=( + """""" + )) + + def test_multiple_values_invalid(self): + """ + If multiple values are given, but some of them are not valid, the valid + ones are selected. + """ + self.check_html(self.widget, 'beatles', ['J', 'G', 'foo'], choices=self.beatles, html=( + """""" + )) + + def test_compare_string(self): + choices = [('1', '1'), ('2', '2'), ('3', '3')] + + self.check_html(self.widget, 'nums', [2], choices=choices, html=( + """""" + )) + + self.check_html(self.widget, 'nums', ['2'], choices=choices, html=( + """""" + )) + + self.check_html(self.widget, 'nums', [2], choices=choices, html=( + """""" + )) + + def test_optgroup_select_multiple(self): + widget = SelectMultiple(choices=( + ('outer1', 'Outer 1'), + ('Group "1"', (('inner1', 'Inner 1'), ('inner2', 'Inner 2'))), + )) + self.check_html(widget, 'nestchoice', ['outer1', 'inner2'], html=( + """""" + )) diff --git a/tests/forms_tests/widget_tests/test_splitdatetimewidget.py b/tests/forms_tests/widget_tests/test_splitdatetimewidget.py new file mode 100644 index 0000000000..172bcbbe8d --- /dev/null +++ b/tests/forms_tests/widget_tests/test_splitdatetimewidget.py @@ -0,0 +1,51 @@ +from datetime import date, datetime, time + +from django.forms import SplitDateTimeWidget + +from .base import WidgetTest + + +class SplitDateTimeWidgetTest(WidgetTest): + widget = SplitDateTimeWidget() + + def test_render_empty(self): + self.check_html(self.widget, 'date', '', html=( + '' + )) + + def test_render_none(self): + self.check_html(self.widget, 'date', None, html=( + '' + )) + + def test_render_datetime(self): + self.check_html(self.widget, 'date', datetime(2006, 1, 10, 7, 30), html=( + '' + '' + )) + + def test_render_date_and_time(self): + self.check_html(self.widget, 'date', [date(2006, 1, 10), time(7, 30)], html=( + '' + '' + )) + + def test_constructor_attrs(self): + widget = SplitDateTimeWidget(attrs={'class': 'pretty'}) + self.check_html(widget, 'date', datetime(2006, 1, 10, 7, 30), html=( + '' + '' + )) + + def test_formatting(self): + """ + Use 'date_format' and 'time_format' to change the way a value is + displayed. + """ + widget = SplitDateTimeWidget( + date_format='%d/%m/%Y', time_format='%H:%M', + ) + self.check_html(widget, 'date', datetime(2006, 1, 10, 7, 30), html=( + '' + '' + )) diff --git a/tests/forms_tests/widget_tests/test_splithiddendatetimewidget.py b/tests/forms_tests/widget_tests/test_splithiddendatetimewidget.py new file mode 100644 index 0000000000..07ee15690c --- /dev/null +++ b/tests/forms_tests/widget_tests/test_splithiddendatetimewidget.py @@ -0,0 +1,42 @@ +from datetime import datetime + +from django.forms import SplitHiddenDateTimeWidget +from django.test import override_settings +from django.utils import translation + +from .base import WidgetTest + + +class SplitHiddenDateTimeWidgetTest(WidgetTest): + widget = SplitHiddenDateTimeWidget() + + def test_render_empty(self): + self.check_html(self.widget, 'date', '', html=( + '' + )) + + def test_render_value(self): + d = datetime(2007, 9, 17, 12, 51, 34, 482548) + self.check_html(self.widget, 'date', d, html=( + '' + '' + )) + self.check_html(self.widget, 'date', datetime(2007, 9, 17, 12, 51, 34), html=( + '' + '' + )) + self.check_html(self.widget, 'date', datetime(2007, 9, 17, 12, 51), html=( + '' + '' + )) + + @override_settings(USE_L10N=True) + @translation.override('de-at') + def test_l10n(self): + d = datetime(2007, 9, 17, 12, 51) + self.check_html(self.widget, 'date', d, html=( + """ + + + """ + )) diff --git a/tests/forms_tests/widget_tests/test_textarea.py b/tests/forms_tests/widget_tests/test_textarea.py new file mode 100644 index 0000000000..490848fab3 --- /dev/null +++ b/tests/forms_tests/widget_tests/test_textarea.py @@ -0,0 +1,34 @@ +from django.forms import Textarea +from django.utils.safestring import mark_safe + +from .base import WidgetTest + + +class TextareaTest(WidgetTest): + widget = Textarea() + + def test_render(self): + self.check_html(self.widget, 'msg', 'value', html=( + '' + )) + + def test_render_required(self): + widget = Textarea() + widget.is_required = True + self.check_html(widget, 'msg', 'value', html='') + + def test_render_empty(self): + self.check_html(self.widget, 'msg', '', html='') + + def test_render_none(self): + self.check_html(self.widget, 'msg', None, html='') + + def test_escaping(self): + self.check_html(self.widget, 'msg', 'some "quoted" & ampersanded value', html=( + '' + )) + + def test_mark_safe(self): + self.check_html(self.widget, 'msg', mark_safe('pre "quoted" value'), html=( + '' + )) diff --git a/tests/forms_tests/widget_tests/test_textinput.py b/tests/forms_tests/widget_tests/test_textinput.py new file mode 100644 index 0000000000..33e455a160 --- /dev/null +++ b/tests/forms_tests/widget_tests/test_textinput.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.forms import TextInput +from django.utils.safestring import mark_safe + +from .base import WidgetTest + + +class TextInputTest(WidgetTest): + widget = TextInput() + + def test_render(self): + self.check_html(self.widget, 'email', '', html='') + + def test_render_none(self): + self.check_html(self.widget, 'email', None, html='') + + def test_render_value(self): + self.check_html(self.widget, 'email', 'test@example.com', html=( + '' + )) + + def test_render_boolean(self): + """ + Boolean values are rendered to their string forms ("True" and + "False"). + """ + self.check_html(self.widget, 'get_spam', False, html=( + '' + )) + self.check_html(self.widget, 'get_spam', True, html=( + '' + )) + + def test_render_quoted(self): + self.check_html( + self.widget, 'email', 'some "quoted" & ampersanded value', + html='', + ) + + def test_render_custom_attrs(self): + self.check_html( + self.widget, 'email', 'test@example.com', attrs={'class': 'fun'}, + html='', + ) + + def test_render_unicode(self): + self.check_html( + self.widget, 'email', 'ŠĐĆŽćžšđ', attrs={'class': 'fun'}, + html=( + '' + ), + ) + + def test_constructor_attrs(self): + widget = TextInput(attrs={'class': 'fun', 'type': 'email'}) + self.check_html(widget, 'email', '', html='') + self.check_html( + widget, 'email', 'foo@example.com', + html='', + ) + + def test_attrs_precedence(self): + """ + `attrs` passed to render() get precedence over those passed to the + constructor + """ + widget = TextInput(attrs={'class': 'pretty'}) + self.check_html( + widget, 'email', '', attrs={'class': 'special'}, + html='', + ) + + def test_attrs_safestring(self): + widget = TextInput(attrs={'onBlur': mark_safe("function('foo')")}) + self.check_html(widget, 'email', '', html='') diff --git a/tests/forms_tests/widget_tests/test_timeinput.py b/tests/forms_tests/widget_tests/test_timeinput.py new file mode 100644 index 0000000000..96fb04e24c --- /dev/null +++ b/tests/forms_tests/widget_tests/test_timeinput.py @@ -0,0 +1,50 @@ +from datetime import time + +from django.forms import TimeInput +from django.test import override_settings +from django.utils import translation + +from .base import WidgetTest + + +class TimeInputTest(WidgetTest): + widget = TimeInput() + + def test_render_none(self): + self.check_html(self.widget, 'time', None, html='') + + def test_render_value(self): + """ + The microseconds are trimmed on display, by default. + """ + t = time(12, 51, 34, 482548) + self.assertEqual(str(t), '12:51:34.482548') + self.check_html(self.widget, 'time', t, html='') + self.check_html(self.widget, 'time', time(12, 51, 34), html=( + '' + )) + self.check_html(self.widget, 'time', time(12, 51), html=( + '' + )) + + def test_string(self): + """ + We should be able to initialize from a unicode value. + """ + self.check_html(self.widget, 'time', '13:12:11', html=( + '' + )) + + def test_format(self): + """ + Use 'format' to change the way a value is displayed. + """ + t = time(12, 51, 34, 482548) + widget = TimeInput(format='%H:%M', attrs={'type': 'time'}) + self.check_html(widget, 'time', t, html='') + + @override_settings(USE_L10N=True) + @translation.override('de-at') + def test_l10n(self): + t = time(12, 51, 34, 482548) + self.check_html(self.widget, 'time', t, html='') -- cgit v1.3