From 2c31d9fbb78064d6a35e0bd1d6f1b9f255c92032 Mon Sep 17 00:00:00 2001 From: Sam Bland Date: Thu, 27 Aug 2026 11:02:41 +0100 Subject: [PATCH 1/3] Add soup_factory fixture fixes #751 --- tests/main/view_utils.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/main/view_utils.py b/tests/main/view_utils.py index e1876ea2..88ac67b4 100644 --- a/tests/main/view_utils.py +++ b/tests/main/view_utils.py @@ -1,6 +1,7 @@ """Utility module for view tests.""" from abc import ABC, abstractmethod +from collections.abc import Callable from http import HTTPStatus import pytest @@ -54,7 +55,7 @@ class BS4Mixin(ABC): """ @abstractmethod - def _get_url(self) -> str: + def _get_url(self, **kwargs) -> str: return NotImplemented @pytest.fixture @@ -63,6 +64,19 @@ def soup(self, client) -> BeautifulSoup: response = client.get(self._get_url()) return BeautifulSoup(response.content, "html.parser") + @pytest.fixture + def soup_factory(self, client) -> Callable[..., BeautifulSoup]: + """A fixture factory for the BeautifulSoup4 object of the requested page.""" + + def get_soup(**kwargs) -> BeautifulSoup: + if kwargs: + response = client.get(self._get_url(kwargs=kwargs)) + else: + response = client.get(self._get_url()) + return BeautifulSoup(response.content, "html.parser") + + return get_soup + @pytest.fixture def auth_soup(self, client, user) -> BeautifulSoup: """A BeautifulSoup4 object of the requested page viewed by a logged-in user.""" From b2deb9fead13fefd04612be0f048d137d598c085 Mon Sep 17 00:00:00 2001 From: Sam Bland Date: Thu, 27 Aug 2026 11:02:41 +0100 Subject: [PATCH 2/3] Add admin_client and authenticated options to the soup factory --- tests/main/view_utils.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/main/view_utils.py b/tests/main/view_utils.py index 88ac67b4..bddbfe74 100644 --- a/tests/main/view_utils.py +++ b/tests/main/view_utils.py @@ -65,14 +65,17 @@ def soup(self, client) -> BeautifulSoup: return BeautifulSoup(response.content, "html.parser") @pytest.fixture - def soup_factory(self, client) -> Callable[..., BeautifulSoup]: + def soup_factory(self, client, admin_client, user) -> Callable[..., BeautifulSoup]: """A fixture factory for the BeautifulSoup4 object of the requested page.""" - def get_soup(**kwargs) -> BeautifulSoup: + def get_soup(authenticated=False, admin=False, **kwargs) -> BeautifulSoup: + _client = admin_client if admin else client + if authenticated: + _client.force_login(user) if kwargs: - response = client.get(self._get_url(kwargs=kwargs)) + response = _client.get(self._get_url(**kwargs)) else: - response = client.get(self._get_url()) + response = _client.get(self._get_url()) return BeautifulSoup(response.content, "html.parser") return get_soup From d2057b5f03ac00ee10419b0280f57faebfea52f0 Mon Sep 17 00:00:00 2001 From: Sam Bland Date: Thu, 27 Aug 2026 11:02:41 +0100 Subject: [PATCH 3/3] Implemented tests for the skill profile page view - Note we removed the use of TemplateMixin as this does not work when _get_url requires url params --- tests/main/test_main_views.py | 95 +++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 3 deletions(-) diff --git a/tests/main/test_main_views.py b/tests/main/test_main_views.py index 997a2928..cd98b52c 100644 --- a/tests/main/test_main_views.py +++ b/tests/main/test_main_views.py @@ -7,6 +7,7 @@ import json from http import HTTPStatus +from urllib import parse as parseUrl import pytest from django.db.models import QuerySet @@ -567,10 +568,98 @@ def _get_url(self): return reverse("licensing") -class TestViewSkillProfilePageView(TemplateOkMixin): +class TestViewSkillProfilePageView(BS4Mixin): """Test suite for the ViewSkillProfilePageView.""" _template_name = "main/shared-skills-profile.html" - def _get_url(self): - return reverse("view_skill_profile") + def _example_chart_data(self, user_skill): + return [ + { + "user_id": "root", + "user_data": [ + { + "skill": user_skill.skill.name, + "category": user_skill.skill.competency.competency_domain.name, + "subcategory": user_skill.skill.competency.name, + "skill_level": user_skill.skill_level.level, + } + ], + } + ] + + def _get_url(self, chart_data): + """Construct the URL for the view skill profile page with query parameters.""" + skill_levels = json.dumps(list(SkillLevel.objects.values("level", "name"))) + chart_data_str = json.dumps(chart_data) + url = f"{reverse('view_skill_profile')}" + params = parseUrl.urlencode( + { + "skill_levels": skill_levels, + "chart_data": chart_data_str, + } + ) + url = f"{url}?{params}" + return url + + def test_template_used(self, admin_client, user_skill): + """Test the correct template is used by the GET request.""" + with assertTemplateUsed(template_name=self._template_name): + response = admin_client.get( + self._get_url(self._example_chart_data(user_skill)) + ) + assert response.status_code == HTTPStatus.OK + + def test_provides_required_context(self, client, user_skill): + """Test that the view skill profile view provides the correct context.""" + url = self._get_url(self._example_chart_data(user_skill)) + response = client.get(url) + assert response.status_code == HTTPStatus.OK + assert "chart_data" in response.context + assert isinstance(response.context["chart_data"], str) + assert response.context["chart_data"] == json.dumps( + self._example_chart_data(user_skill) + ) + assert "skill_levels" in response.context + assert isinstance(response.context["skill_levels"], str) + assert response.context["skill_levels"] == json.dumps( + list(SkillLevel.objects.values("level", "name")) + ) + + def test_skill_wheel_script(self, soup_factory, user_skill): + """Test that the skill profile view contains the correct script.""" + soup = soup_factory( + authenticated=True, chart_data=self._example_chart_data(user_skill) + ) + card = soup.find("div", class_="card-body") + + assert card.find(tag_with_text_filter("h1", "Skills profile")) + assert card.find("div", id="dataviz_root") + + skill_level_list = list(SkillLevel.objects.values("level", "name")) + user_skill_dict = { + "skill": user_skill.skill.name, + "category": user_skill.skill.competency.competency_domain.name, + "subcategory": user_skill.skill.competency.name, + "skill_level": user_skill.skill_level.level, + } + chart_data = [{"user_id": "root", "user_data": [user_skill_dict]}] + + assert card.find( + tag_with_text_filter( + "script", + f"const skillLevels = {json.dumps(skill_level_list)};", + ) + ) + assert card.find( + tag_with_text_filter( + "script", + f"const charts = {json.dumps(chart_data)};", + ) + ) + assert card.find( + tag_with_text_filter( + "script", + "renderRadialBarChart(target, charts[i].user_data, skillLevels);", + ) + )