From 50999d41ef3426486aba59596c6b44fb223ef86d Mon Sep 17 00:00:00 2001
From: Ratheesh kumar R <108045773+ratheesh-aot@users.noreply.github.com>
Date: Wed, 20 Mar 2024 12:13:43 -0700
Subject: [PATCH] [TO MAIN] DESENG-513 - Add poll results to results tab
(#2422)
* DESENG-513: Poll results view
* DESENG-513: Lint fixes
* DESENG-513: Updated changelog
* DESENG-513: Votes text update
* Removed comments
* DESENG-513: Fixing review comments
---
CHANGELOG.MD | 5 +
met-api/src/met_api/resources/widget_poll.py | 12 ++
.../met_api/services/poll_response_service.py | 72 ++++++++++-
met-api/tests/unit/api/test_widget_poll.py | 49 +++++++-
.../Results/EngagementResults.tsx | 4 +
.../PollResults/EngagementPollContext.tsx | 118 ++++++++++++++++++
.../Results/PollResults/PollResult.tsx | 37 ++++++
.../Results/PollResults/PollResultsView.tsx | 13 ++
.../Results/PollResults/ResultView.tsx | 74 +++++++++++
met-web/src/models/pollWidget.tsx | 14 +++
.../widgetService/PollService/index.tsx | 13 +-
.../widgets/PollWidgetResultsView.test.tsx | 98 +++++++++++++++
12 files changed, 503 insertions(+), 6 deletions(-)
create mode 100644 met-web/src/components/engagement/form/EngagementFormTabs/Results/PollResults/EngagementPollContext.tsx
create mode 100644 met-web/src/components/engagement/form/EngagementFormTabs/Results/PollResults/PollResult.tsx
create mode 100644 met-web/src/components/engagement/form/EngagementFormTabs/Results/PollResults/PollResultsView.tsx
create mode 100644 met-web/src/components/engagement/form/EngagementFormTabs/Results/PollResults/ResultView.tsx
create mode 100644 met-web/tests/unit/components/widgets/PollWidgetResultsView.test.tsx
diff --git a/CHANGELOG.MD b/CHANGELOG.MD
index f02e9400a..5c49f3af8 100644
--- a/CHANGELOG.MD
+++ b/CHANGELOG.MD
@@ -1,5 +1,10 @@
## March 19, 2024
+- **Task**: Add poll results to results tab [DESENG-513](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-513)
+ - Added poll results to results tab.
+ - Added poll results API.
+ - Added Unit tests.
+
- **Task**: Change static english text to be able to support string translations [DESENG-467](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-467)
- Implemented a language selector in the public header.
- Incorporated logic to dynamically adjust the unauthenticated route based on the selected language and load the appropriate translation file.
diff --git a/met-api/src/met_api/resources/widget_poll.py b/met-api/src/met_api/resources/widget_poll.py
index 7eb24550b..785c88183 100644
--- a/met-api/src/met_api/resources/widget_poll.py
+++ b/met-api/src/met_api/resources/widget_poll.py
@@ -10,6 +10,7 @@
from met_api.schemas import utils as schema_utils
from met_api.schemas.widget_poll import WidgetPollSchema
from met_api.services.widget_poll_service import WidgetPollService
+from met_api.services.poll_response_service import PollResponseService
from met_api.utils.util import allowedorigins, cors_preflight
from met_api.utils.ip_util import hash_ip
@@ -165,3 +166,14 @@ def post(widget_id, poll_widget_id):
except BusinessException as err:
return err.error, err.status_code
+
+ @staticmethod
+ @cross_origin(origins=allowedorigins())
+ @_jwt.requires_auth
+ def get(poll_widget_id, **_):
+ """Get poll responses for a given widget."""
+ try:
+ poll_results = PollResponseService().get_poll_details_with_response_counts(poll_widget_id)
+ return jsonify(poll_results), HTTPStatus.OK
+ except BusinessException as err:
+ return err.error, err.status_code
diff --git a/met-api/src/met_api/services/poll_response_service.py b/met-api/src/met_api/services/poll_response_service.py
index 4dcdaf554..dd070f2ad 100644
--- a/met-api/src/met_api/services/poll_response_service.py
+++ b/met-api/src/met_api/services/poll_response_service.py
@@ -1,9 +1,15 @@
"""Service for Poll Response management."""
+
from http import HTTPStatus
+from sqlalchemy import func
from sqlalchemy.exc import SQLAlchemyError
from met_api.exceptions.business_exception import BusinessException
from met_api.models.poll_responses import PollResponse as PollResponseModel
from met_api.services.poll_answers_service import PollAnswerService
+from met_api.models import Poll, PollAnswer, db
+from met_api.services import authorization
+from met_api.constants.membership_type import MembershipType
+from met_api.utils.roles import Role
class PollResponseService:
@@ -31,8 +37,7 @@ def create_response(response_data: dict) -> PollResponseModel:
return poll_response
except SQLAlchemyError as e:
# Log the exception or handle it as needed
- raise BusinessException(f'Error creating poll response: {e}',
- HTTPStatus.INTERNAL_SERVER_ERROR) from e
+ raise BusinessException(f'Error creating poll response: {e}', HTTPStatus.INTERNAL_SERVER_ERROR) from e
@staticmethod
def get_poll_count(poll_id: int, ip_addr: str = None) -> int:
@@ -46,5 +51,64 @@ def get_poll_count(poll_id: int, ip_addr: str = None) -> int:
return len(responses)
except SQLAlchemyError as e:
# Log the exception or handle it as needed
- raise BusinessException(f'Error creating poll response: {e}',
- HTTPStatus.INTERNAL_SERVER_ERROR) from e
+ raise BusinessException(f'Error creating poll response: {e}', HTTPStatus.INTERNAL_SERVER_ERROR) from e
+
+ @staticmethod
+ def _check_authorization(engagement_id):
+ """Check user authorization."""
+ authorization.check_auth(
+ one_of_roles=(
+ MembershipType.TEAM_MEMBER.name,
+ Role.EDIT_ENGAGEMENT.value,
+ ),
+ engagement_id=engagement_id,
+ )
+
+ @staticmethod
+ def get_poll_details_with_response_counts(poll_id):
+ """
+ Get poll details along with response counts for each answer for a specific poll.
+
+ :param poll_id: The ID of the poll.
+ :return: Poll details and response counts for each answer in a structured format.
+ """
+ poll = Poll.query.get(poll_id)
+ if not poll:
+ raise BusinessException('Poll not found', HTTPStatus.NOT_FOUND)
+ # Check authorization
+ PollResponseService._check_authorization(poll.engagement_id)
+
+ # Query to join PollAnswer and PollResponse and count responses for each answer
+ poll_data = (
+ db.session.query(
+ PollAnswer.id.label('answer_id'),
+ PollAnswer.answer_text,
+ func.count(PollResponseModel.selected_answer_id).label('response_count')
+ )
+ .select_from(PollAnswer)
+ .outerjoin(PollResponseModel, PollAnswer.id == PollResponseModel.selected_answer_id)
+ .filter(PollAnswer.poll_id == poll_id)
+ .group_by(PollAnswer.id, PollAnswer.answer_text)
+ .all()
+ )
+
+ # Calculate total responses
+ total_responses = sum(response_count for _, _, response_count in poll_data)
+
+ # Construct response dictionary
+ response = {
+ 'poll_id': poll_id,
+ 'title': poll.title,
+ 'description': poll.description,
+ 'total_response': total_responses,
+ 'answers': [
+ {
+ 'answer_id': answer_id,
+ 'answer_text': answer_text,
+ 'total_response': response_count,
+ 'percentage': (response_count / total_responses * 100) if total_responses > 0 else 0
+ } for answer_id, answer_text, response_count in poll_data
+ ]
+ }
+
+ return response
diff --git a/met-api/tests/unit/api/test_widget_poll.py b/met-api/tests/unit/api/test_widget_poll.py
index b38e5d8da..50ded71a4 100644
--- a/met-api/tests/unit/api/test_widget_poll.py
+++ b/met-api/tests/unit/api/test_widget_poll.py
@@ -26,7 +26,8 @@
from met_api.utils.enums import ContentType
from tests.utilities.factory_scenarios import TestJwtClaims, TestPollAnswerInfo, TestWidgetPollInfo
from tests.utilities.factory_utils import (
- factory_auth_header, factory_engagement_model, factory_poll_answer_model, factory_poll_model, factory_widget_model)
+ factory_auth_header, factory_engagement_model, factory_poll_answer_model, factory_poll_model,
+ factory_poll_response_model, factory_widget_model)
fake = Faker()
@@ -211,3 +212,49 @@ def test_record_poll_response(client, session, jwt):
)
assert rv.status_code == HTTPStatus.BAD_REQUEST
+
+
+def test_get_poll_response(client, jwt, session, setup_admin_user_and_claims):
+ """Assert that a response for a poll widget can be retrieved."""
+ # Test setup: create a poll widget and a response model
+ _, claims = setup_admin_user_and_claims
+ headers = factory_auth_header(jwt=jwt, claims=claims)
+ engagement = factory_engagement_model()
+ widget = factory_widget_model({'engagement_id': engagement.id})
+ poll = factory_poll_model(widget, TestWidgetPollInfo.poll1)
+ answer1 = factory_poll_answer_model(poll, TestPollAnswerInfo.answer1)
+ answer2 = factory_poll_answer_model(poll, TestPollAnswerInfo.answer2)
+ answer3 = factory_poll_answer_model(poll, TestPollAnswerInfo.answer3)
+
+ # Recording 2 votes for answer1
+ factory_poll_response_model(poll, answer1)
+ factory_poll_response_model(poll, answer1)
+
+ # Recording 2 votes for answer2
+ factory_poll_response_model(poll, answer2)
+ factory_poll_response_model(poll, answer2)
+
+ # Sending GET request
+ rv = client.get(
+ f'/api/widgets/{widget.id}/polls/{poll.id}/responses',
+ headers=headers,
+ content_type=ContentType.JSON.value,
+ )
+
+ # Checking response
+ assert rv.status_code == HTTPStatus.OK
+ # Testing Poll title and total_response
+ assert rv.json.get('title') == poll.title
+ assert rv.json.get('total_response') == 4
+ # Testing First answer
+ assert rv.json.get('answers')[0].get('answer_id') == answer1.id
+ assert rv.json.get('answers')[0].get('total_response') == 2
+ assert rv.json.get('answers')[0].get('percentage') == 50
+ # Testing Second answer
+ assert rv.json.get('answers')[1].get('answer_id') == answer2.id
+ assert rv.json.get('answers')[1].get('total_response') == 2
+ assert rv.json.get('answers')[1].get('percentage') == 50
+ # Testing Third answer
+ assert rv.json.get('answers')[2].get('answer_id') == answer3.id
+ assert rv.json.get('answers')[2].get('total_response') == 0
+ assert rv.json.get('answers')[2].get('percentage') == 0
diff --git a/met-web/src/components/engagement/form/EngagementFormTabs/Results/EngagementResults.tsx b/met-web/src/components/engagement/form/EngagementFormTabs/Results/EngagementResults.tsx
index 390a8512a..a3a017edf 100644
--- a/met-web/src/components/engagement/form/EngagementFormTabs/Results/EngagementResults.tsx
+++ b/met-web/src/components/engagement/form/EngagementFormTabs/Results/EngagementResults.tsx
@@ -3,6 +3,7 @@ import { Grid, Box } from '@mui/material';
import { MetPaper, PrimaryButton, SecondaryButton } from 'components/common';
import { EngagementTabsContext } from '../EngagementTabsContext';
import { ActionContext } from '../../ActionContext';
+import PollResultsView from './PollResults/PollResultsView';
const EngagementResults = () => {
const { isSaving } = useContext(ActionContext);
@@ -19,6 +20,9 @@ const EngagementResults = () => {
spacing={2}
sx={{ padding: '2em' }}
>
+
+
+
({
+ widget: null,
+ isWidgetsLoading: true,
+ pollWidget: null,
+ isLoadingPollWidget: true,
+ pollResults: null,
+ isPollResultsLoading: true,
+});
+
+export const EngagementPollContextProvider = ({ children }: { children: JSX.Element | JSX.Element[] }) => {
+ const { savedEngagement } = useContext(ActionContext);
+ const [widgets, setWidgets] = useState(null);
+ const [widget, setWidget] = useState(null);
+ const [isWidgetsLoading, setIsWidgetsLoading] = useState(true);
+ const [pollWidget, setPollWidget] = useState(null);
+ const [isLoadingPollWidget, setIsLoadingPollWidget] = useState(true);
+ const [pollResults, setPollResults] = useState(null);
+ const [isPollResultsLoading, setIsPollResultsLoading] = useState(true);
+ const dispatch = useAppDispatch();
+
+ const loadWidgets = async () => {
+ if (!savedEngagement.id) {
+ setIsWidgetsLoading(false);
+ return;
+ }
+
+ try {
+ const widgetsList = await getWidgets(savedEngagement.id);
+ setWidgets(widgetsList);
+ setIsWidgetsLoading(false);
+ } catch (err) {
+ setIsWidgetsLoading(false);
+ dispatch(openNotification({ severity: 'error', text: 'Error fetching engagement widgets' }));
+ } finally {
+ setIsWidgetsLoading(false);
+ }
+ };
+
+ const loadPollWidget = async () => {
+ const widget = widgets?.find((w) => w.widget_type_id === WidgetType.Poll) ?? null;
+ setWidget(widget);
+ if (!widget) {
+ setIsLoadingPollWidget(false);
+ return;
+ }
+ try {
+ const result = await fetchPollWidgets(widget.id);
+ setPollWidget(result.at(-1));
+ setIsLoadingPollWidget(false);
+ } catch (error) {
+ dispatch(openNotification({ severity: 'error', text: 'An error occurred while trying to load Poll data' }));
+ setIsLoadingPollWidget(false);
+ } finally {
+ setIsLoadingPollWidget(false);
+ }
+ };
+
+ const getPollResults = async () => {
+ try {
+ if (!widget || !pollWidget) {
+ return;
+ }
+ const data = await fetchPollResults(widget.id, pollWidget.id);
+ setPollResults(data);
+ setIsPollResultsLoading(false);
+ } catch (error) {
+ dispatch(openNotification({ severity: 'error', text: 'Error fetching poll results' }));
+ setIsPollResultsLoading(false);
+ } finally {
+ setIsPollResultsLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ loadWidgets();
+ }, [savedEngagement.id]);
+
+ useEffect(() => {
+ loadPollWidget();
+ }, [widgets]);
+
+ useEffect(() => {
+ getPollResults();
+ }, [widget, pollWidget]);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/met-web/src/components/engagement/form/EngagementFormTabs/Results/PollResults/PollResult.tsx b/met-web/src/components/engagement/form/EngagementFormTabs/Results/PollResults/PollResult.tsx
new file mode 100644
index 000000000..917281e09
--- /dev/null
+++ b/met-web/src/components/engagement/form/EngagementFormTabs/Results/PollResults/PollResult.tsx
@@ -0,0 +1,37 @@
+import React, { useContext } from 'react';
+import { Grid } from '@mui/material';
+import { EngagementPollContext } from './EngagementPollContext';
+import ResultView from './ResultView';
+import { MidScreenLoader } from 'components/common';
+
+export const PollResult = () => {
+ const { widget, isWidgetsLoading, isLoadingPollWidget, pollWidget, pollResults, isPollResultsLoading } =
+ useContext(EngagementPollContext);
+
+ // Show a loader while the data is being loaded
+ if (isLoadingPollWidget || isWidgetsLoading || isPollResultsLoading) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ // Check if both widget and pollWidget are available
+ if (
+ widget &&
+ pollWidget &&
+ widget.id !== undefined &&
+ pollWidget.id !== undefined &&
+ pollResults?.poll_id !== undefined
+ ) {
+ return ;
+ }
+
+ // Display a message or handle the null case when widgetId or pollId is not available
+ return null;
+};
+
+export default PollResult;
diff --git a/met-web/src/components/engagement/form/EngagementFormTabs/Results/PollResults/PollResultsView.tsx b/met-web/src/components/engagement/form/EngagementFormTabs/Results/PollResults/PollResultsView.tsx
new file mode 100644
index 000000000..a2b31cfd9
--- /dev/null
+++ b/met-web/src/components/engagement/form/EngagementFormTabs/Results/PollResults/PollResultsView.tsx
@@ -0,0 +1,13 @@
+import React from 'react';
+import { EngagementPollContextProvider } from './EngagementPollContext';
+import PollResult from './PollResult';
+
+export const PollResultsView = () => {
+ return (
+
+
+
+ );
+};
+
+export default PollResultsView;
diff --git a/met-web/src/components/engagement/form/EngagementFormTabs/Results/PollResults/ResultView.tsx b/met-web/src/components/engagement/form/EngagementFormTabs/Results/PollResults/ResultView.tsx
new file mode 100644
index 000000000..5717c80a6
--- /dev/null
+++ b/met-web/src/components/engagement/form/EngagementFormTabs/Results/PollResults/ResultView.tsx
@@ -0,0 +1,74 @@
+import React from 'react';
+import { PollResultResponse } from 'models/pollWidget';
+import { Box, Typography, LinearProgress, Stack } from '@mui/material';
+import { Widget } from 'models/widget';
+import { Grid } from '@mui/material';
+import { MetHeader4 } from '../../../../../common';
+import Divider from '@mui/material/Divider';
+
+interface PollResultViewProps {
+ widget: Widget;
+ pollResult: PollResultResponse;
+}
+
+interface PollOptionProps {
+ label: string;
+ votes: number;
+ percentage: number;
+}
+
+function PollOption({ label, votes, percentage }: PollOptionProps) {
+ return (
+
+ {label}
+
+
+ {`${percentage}%`}
+
+
+ {votes > 1 ? `${votes} votes` : `${votes} vote`}
+
+
+
+ );
+}
+
+const ResultView: React.FC = ({ pollResult, widget }) => {
+ if (!pollResult) {
+ return No Poll Result
;
+ }
+
+ return (
+
+
+ {widget.title} - Results
+
+
+
+
+ {pollResult.title}
+
+
+ {pollResult.description}
+
+
+
+ {pollResult.answers.map((answer, index) => (
+
+ ))}
+
+
+ Total Votes: {pollResult.total_response}
+
+
+
+
+ );
+};
+
+export default ResultView;
diff --git a/met-web/src/models/pollWidget.tsx b/met-web/src/models/pollWidget.tsx
index 69a047875..919da2c7b 100644
--- a/met-web/src/models/pollWidget.tsx
+++ b/met-web/src/models/pollWidget.tsx
@@ -10,7 +10,21 @@ export interface PollWidget {
export interface PollAnswer {
id: number;
answer_text: string;
+ percentage?: number;
}
export interface PollResponse {
selected_answer_id: string;
}
+
+export interface PollResultResponse {
+ answers: Array<{
+ answer_id: number;
+ answer_text: string;
+ percentage: number;
+ total_response: number;
+ }>;
+ description: string;
+ poll_id: number;
+ title: string;
+ total_response: number;
+}
diff --git a/met-web/src/services/widgetService/PollService/index.tsx b/met-web/src/services/widgetService/PollService/index.tsx
index 88ead8ade..2cf5ee0ec 100644
--- a/met-web/src/services/widgetService/PollService/index.tsx
+++ b/met-web/src/services/widgetService/PollService/index.tsx
@@ -1,7 +1,7 @@
import http from 'apiManager/httpRequestHandler';
import Endpoints from 'apiManager/endpoints';
import { replaceAllInURL, replaceUrl } from 'helper';
-import { PollWidget, PollAnswer, PollResponse } from 'models/pollWidget';
+import { PollWidget, PollAnswer, PollResponse, PollResultResponse } from 'models/pollWidget';
interface PostPollRequest {
widget_id: number;
@@ -73,3 +73,14 @@ export const postPollResponse = async (
return Promise.reject(err);
}
};
+
+export const fetchPollResults = async (widget_id: number, poll_id: number): Promise => {
+ try {
+ let url = replaceUrl(Endpoints.PollWidgets.RECORD_RESPONSE, 'widget_id', String(widget_id));
+ url = replaceUrl(url, 'poll_id', String(poll_id));
+ const response = await http.GetRequest(url);
+ return response.data || Promise.reject('Failed to fetch Poll Results');
+ } catch (err) {
+ return Promise.reject(err);
+ }
+};
diff --git a/met-web/tests/unit/components/widgets/PollWidgetResultsView.test.tsx b/met-web/tests/unit/components/widgets/PollWidgetResultsView.test.tsx
new file mode 100644
index 000000000..c0f00e228
--- /dev/null
+++ b/met-web/tests/unit/components/widgets/PollWidgetResultsView.test.tsx
@@ -0,0 +1,98 @@
+import { render, waitFor, screen, fireEvent } from '@testing-library/react';
+import React from 'react';
+import '@testing-library/jest-dom';
+import EngagementForm from '../../../../src/components/engagement/form';
+import * as widgetService from 'services/widgetService';
+import * as pollService from 'services/widgetService/PollService';
+import { draftEngagement, pollWidget } from '../factory';
+import { USER_ROLES } from 'services/userService/constants';
+import { setupWidgetTestEnvMock, setupWidgetTestEnvSpy } from './setupWidgetTestEnv';
+
+jest.mock('components/map', () => () => );
+jest.mock('axios');
+jest.mock('react-redux', () => ({
+ ...jest.requireActual('react-redux'),
+ useSelector: jest.fn(() => {
+ return {
+ roles: [USER_ROLES.VIEW_PRIVATE_ENGAGEMENTS, USER_ROLES.EDIT_ENGAGEMENT, USER_ROLES.CREATE_ENGAGEMENT],
+ assignedEngagements: [draftEngagement.id],
+ };
+ }),
+}));
+const mockCreateWidget = jest.fn(() => Promise.resolve(pollWidget));
+
+jest.mock('apiManager/apiSlices/widgets', () => ({
+ ...jest.requireActual('apiManager/apiSlices/widgets'),
+ useCreateWidgetMutation: () => [mockCreateWidget],
+ useCreateWidgetItemsMutation: () => [mockCreateWidget],
+ useUpdateWidgetMutation: () => [jest.fn(() => Promise.resolve(pollWidget))],
+ useDeleteWidgetMutation: () => [jest.fn(() => Promise.resolve())],
+ useSortWidgetsMutation: () => [jest.fn(() => Promise.resolve())],
+}));
+
+describe('Poll Widget tests', () => {
+ const mockPoll = {
+ id: 1,
+ widget_id: pollWidget.id,
+ engagement_id: draftEngagement.id,
+ title: 'Test Poll',
+ description: 'Description',
+ status: 'active',
+ answers: [
+ { id: 1, answer_text: 'Option 1' },
+ { id: 2, answer_text: 'Option 2' },
+ { id: 3, answer_text: 'Option 3' },
+ ],
+ };
+
+ const mockPollResult = {
+ poll_id: mockPoll.id,
+ title: mockPoll.title,
+ description: mockPoll.description,
+ total_response: 4,
+ answers: [
+ { answer_id: 1, answer_text: 'Option 1', total_response: 2, percentage: 50 },
+ { answer_id: 2, answer_text: 'Option 2', total_response: 2, percentage: 50 },
+ { answer_id: 3, answer_text: 'Option 3', total_response: 0, percentage: 0 },
+ ],
+ };
+
+ beforeAll(() => {
+ setupWidgetTestEnvMock();
+ setupWidgetTestEnvSpy();
+ jest.spyOn(widgetService, 'getWidgets').mockReturnValue(Promise.resolve([pollWidget]));
+ jest.spyOn(pollService, 'fetchPollWidgets').mockReturnValue(Promise.resolve([mockPoll]));
+ jest.spyOn(pollService, 'fetchPollResults').mockReturnValue(Promise.resolve(mockPollResult));
+ });
+
+ async function renderPollWidgetView() {
+ await waitFor(() => expect(screen.getByText('Results')).toBeInTheDocument());
+ fireEvent.click(screen.getByText('Results'));
+ expect(screen.getByText('Poll')).toBeVisible();
+ await waitFor(() => expect(screen.getByText('Test Poll')).toBeInTheDocument());
+ }
+
+ test('Poll widget results is rendered', async () => {
+ render();
+ await renderPollWidgetView();
+ expect(screen.getByText(mockPollResult.description)).toBeVisible();
+ expect(screen.getByText('Total Votes: 4')).toBeVisible();
+ });
+
+ test('Poll widget answer options and its percentage is rendered', async () => {
+ render();
+ await renderPollWidgetView();
+ expect(screen.getByText(mockPollResult.answers[0].answer_text)).toBeVisible();
+ expect(screen.getByText(mockPollResult.answers[1].answer_text)).toBeVisible();
+ const twoVotes = screen.getAllByText('2 votes');
+ expect(twoVotes.length).toBe(2);
+ expect(twoVotes[0]).toBeVisible();
+ expect(twoVotes[1]).toBeVisible();
+ expect(screen.getByText('0 votes')).toBeVisible();
+ expect(screen.getByText('0%')).toBeVisible();
+ const fiftyPercent = screen.getAllByText('50%');
+ expect(fiftyPercent.length).toBe(2);
+ expect(fiftyPercent[0]).toBeVisible();
+ expect(fiftyPercent[1]).toBeVisible();
+ });
+});