Visitar URL original
Widget templates not covered (when they are clearly used). · Issue #94 · coveragepy/django_coverage_plugin · GitHub
Skip to content

Widget templates not covered (when they are clearly used). #94

Open

Description

@PetrDlouhy

I have Widget such as:

class PlanChoiceWidget(RadioSelect):
    option_template_name = "plans/plan_choice_widget.html"

and tests such as:

class PlanChoiceWidgetTests1(TestCase):

    def setUp(self):
        self.factory = RequestFactory()
        self.plan_pricing = baker.make(
            "PlanPricing", plan__name="Foo", price=Decimal("10.00"),
            has_automatic_renewal=True
        )
        self.request = self.factory.get("")
        self.request.user = baker.make("accounts.UserProfile")

    def render_widget(self, plan):
        widget = PlanChoiceWidget()
        choice_data = {
            "name": "plan_choice",
            "value": plan.pk,
            "attrs": {"id": "id_plan_choice"},
        }
        widget.choices = [(plan.pk, plan)]
        widget.request = self.request
        return widget.render(**choice_data)

    def test_plan_choice_widget_render(self):
        rendered_html = self.render_widget(self.plan_pricing)

        self.assertInHTML(
            '<div class="pricing-plans-price__price jsPricingPlansCardPriceWhole">10</div>',
            rendered_html,
        )
        self.assertIn('data-amount="10.00"', rendered_html)
        self.assertIn('data-renewal="True"', rendered_html)

    def test_non_recurring_plan(self):
        self.plan_pricing.has_automatic_renewal = False
        self.plan_pricing.save()

        rendered_html = self.render_widget(self.plan_pricing)

        self.assertIn('<input type="radio" name="plan_choice"', rendered_html)

When I look at the coverage it shows 0% for this template although lines from plans/plan_choice_widget.html template are matched by the tests.

My .coveragerc:

[run]
omit =
   */virtualenvs/*
   */migrations/*
   */site-packages/*
   *.txt
   */settings/dev.py
plugins =
    django_coverage_plugin
branch = True
source = blenderhub
relative_files = True

I am using django-coverage-plugin version 3.1.0, coverage version 7.4.1.

More widget templates from my project are not matched.
Is this a known problem or should I gather more information?

Activity

  1. schinckel commented on May 24, 2024

    @schinckel

    I believe this is because the function that sniffs the frame for the filename fails to work on a template render - it seems that the self object in the relevant context only contains a NodeList, which does not have an origin.

  2. schinckel commented on May 24, 2024

    @schinckel

    I'll create a PR shortly, but my patch seems to work (waiting on a CI run to check).

    @@ -104,5 +104,15 @@
     def filename_for_frame(frame):
         try:
    -        return frame.f_locals["self"].origin.name
    -    except (KeyError, AttributeError):
    +        self = frame.f_locals["self"]
    +    except KeyError:
             return None
    +
    +    try:
    +        return self.origin.name
    +    except AttributeError:
    +        pass
    +
    +    try:
    +        return self[0].origin.name
    +    except (IndexError, AttributeError):
    +        pass
  3. added 3 commits that reference this issue on May 24, 2024
    9c36927
    258f45d
    97cda93
  4. SebCorbin commented on Apr 29, 2026

    @SebCorbin

    @schinckel we just reproduced this bug, any alternative?

  5. schinckel commented on Apr 29, 2026

    @schinckel

    No, sorry. I'm still not using that many widget templates, so I don't notice it. I might investigate a solution when and if I move more code from sniplates to widget templates, but that's not in the immediate future.

  6. nedbat commented on Apr 29, 2026

    @nedbat
    Member

    @SebCorbin Maybe I'm misunderstanding, but there's a diff here that maybe fixes the problem. Have you tried it?

  7. schinckel commented on Apr 29, 2026

    @schinckel

    Hahaha, I didn't even notice that I'd done that...better have another coffee (still early morning where I am).

  8. SebCorbin commented on May 7, 2026

    @SebCorbin

    So I tried the patch but it did not work, two things to note:

    1. I got this error
    Traceback (most recent call last):
      File "site-packages/django_coverage_plugin/plugin.py", line 194, in dynamic_source_filename
        filename = filename_for_frame(frame)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^
      File "site-packages/django_coverage_plugin/plugin.py", line 90, in filename_for_frame
        return self[0].origin.name
               ~~~~^^^
    TypeError: 'BlockNode' object is not subscriptable
    site-packages/coverage/collector.py:409: CoverageWarning: Disabling plug-in 'django_coverage_plugin.DjangoTemplatePlugin' due to previous exception
      self.warn(f"Disabling plug-in {plugin_name!r} due to previous exception")
    
    1. Even after adding TypeError in the list of exceptions pass-ed, no coverage was reported

    Here is how we test our widget:

    class PaymentMethodIconSelectTestCase(SimpleTestCase):
        def test_render_filters_empty_choices(self):
            """Test that render filters out empty choices from the widget."""
            choices = (
                ("", "---"),
                ("cheque", "Cheque"),
                ("bank_transfer", "Bank Transfer"),
            )
            widget = PaymentMethodIconSelect(choices=choices)
    
            rendered = widget.render("icon", "cheque")
    
            self.assertNotIn('value=""', rendered)
            self.assertIn('value="cheque"', rendered)
            self.assertIn('value="bank_transfer"', rendered)
    
            # After rendering, choices should only include non-empty values
            self.assertEqual(len(widget.choices), 2)
            self.assertEqual(widget.choices[0], ("cheque", "Cheque"))
            self.assertEqual(widget.choices[1], ("bank_transfer", "Bank Transfer"))

    and the widget:

    class PaymentMethodIconSelect(forms.RadioSelect):
        option_template_name = "admin/widgets/icon_select_option.html"
    
        def render(self, name, value, attrs=None, renderer=None):
            self.choices = [*(choice for choice in self.choices if choice[0])]
            return super().render(name, value, attrs, renderer)
    
        def create_option(self, *args, **kwargs) -> dict:
            return {
                **super().create_option(*args, **kwargs),
                "icon_url": static(f"payment-methods/{value}.svg"),
            }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions