559 lines
19 KiB
Python
559 lines
19 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
from odoo.tests.common import TransactionCase
|
|
from hypothesis import given, strategies as st, settings
|
|
|
|
|
|
class TestAriaLabels(TransactionCase):
|
|
"""
|
|
Test cases for ARIA label accessibility
|
|
|
|
Property 21: ARIA labels present for accessibility
|
|
For any star in the rating form, it should have an appropriate
|
|
ARIA label for screen reader compatibility.
|
|
|
|
Validates: Requirements 8.3
|
|
"""
|
|
|
|
def setUp(self):
|
|
super(TestAriaLabels, self).setUp()
|
|
# The star rating widget has 5 stars
|
|
self.max_stars = 5
|
|
self.min_stars = 1
|
|
|
|
def _get_aria_label_for_star(self, star_number):
|
|
"""
|
|
Get the ARIA label for a specific star.
|
|
|
|
This mirrors the logic in rating_stars.js getAriaLabel():
|
|
- For star 1: "Rate 1 star out of 5"
|
|
- For stars 2-5: "Rate N stars out of 5"
|
|
|
|
Args:
|
|
star_number: The star number (1-5)
|
|
|
|
Returns:
|
|
The ARIA label string for that star
|
|
"""
|
|
if star_number == 1:
|
|
return f"Rate 1 star out of {self.max_stars}"
|
|
return f"Rate {star_number} stars out of {self.max_stars}"
|
|
|
|
def _get_container_aria_label(self, selected_value):
|
|
"""
|
|
Get the ARIA label for the star container.
|
|
|
|
This mirrors the logic in rating_stars.xml template:
|
|
- Container has role="slider"
|
|
- aria-label: "Rating: N out of 5 stars"
|
|
|
|
Args:
|
|
selected_value: The currently selected rating value (0-5)
|
|
|
|
Returns:
|
|
The ARIA label string for the container
|
|
"""
|
|
return f"Rating: {selected_value} out of {self.max_stars} stars"
|
|
|
|
def _verify_aria_label_property(self, star_number):
|
|
"""
|
|
Verify that a star has an appropriate ARIA label.
|
|
|
|
The property states: For any star in the rating form, it should have
|
|
an appropriate ARIA label for screen reader compatibility.
|
|
|
|
Args:
|
|
star_number: The star number to verify (1-5)
|
|
"""
|
|
# Get the ARIA label for this star
|
|
aria_label = self._get_aria_label_for_star(star_number)
|
|
|
|
# Property 1: ARIA label should exist (not None or empty)
|
|
self.assertIsNotNone(
|
|
aria_label,
|
|
f"Star {star_number} should have an ARIA label"
|
|
)
|
|
self.assertTrue(
|
|
len(aria_label) > 0,
|
|
f"Star {star_number} ARIA label should not be empty"
|
|
)
|
|
|
|
# Property 2: ARIA label should contain the star number
|
|
self.assertIn(
|
|
str(star_number),
|
|
aria_label,
|
|
f"ARIA label should contain star number {star_number}"
|
|
)
|
|
|
|
# Property 3: ARIA label should contain "Rate" to indicate action
|
|
self.assertIn(
|
|
"Rate",
|
|
aria_label,
|
|
f"ARIA label should contain 'Rate' to indicate action"
|
|
)
|
|
|
|
# Property 4: ARIA label should contain "out of" to indicate scale
|
|
self.assertIn(
|
|
"out of",
|
|
aria_label,
|
|
f"ARIA label should contain 'out of' to indicate scale"
|
|
)
|
|
|
|
# Property 5: ARIA label should contain max stars
|
|
self.assertIn(
|
|
str(self.max_stars),
|
|
aria_label,
|
|
f"ARIA label should contain max stars {self.max_stars}"
|
|
)
|
|
|
|
# Property 6: ARIA label should use correct singular/plural form
|
|
if star_number == 1:
|
|
# Should say "1 star" (singular)
|
|
self.assertIn(
|
|
"1 star",
|
|
aria_label,
|
|
f"ARIA label for star 1 should use singular 'star'"
|
|
)
|
|
self.assertNotIn(
|
|
"1 stars",
|
|
aria_label,
|
|
f"ARIA label for star 1 should not use plural 'stars'"
|
|
)
|
|
else:
|
|
# Should say "N stars" (plural)
|
|
self.assertIn(
|
|
f"{star_number} stars",
|
|
aria_label,
|
|
f"ARIA label for star {star_number} should use plural 'stars'"
|
|
)
|
|
|
|
return aria_label
|
|
|
|
def _verify_container_aria_attributes(self, selected_value):
|
|
"""
|
|
Verify that the container has appropriate ARIA attributes.
|
|
|
|
The container should have:
|
|
- role="slider"
|
|
- aria-label describing current rating
|
|
- aria-valuemin="1"
|
|
- aria-valuemax="5"
|
|
- aria-valuenow=selected_value
|
|
- aria-readonly (when readonly)
|
|
|
|
Args:
|
|
selected_value: The currently selected rating value (0-5)
|
|
"""
|
|
# Get container ARIA label
|
|
container_label = self._get_container_aria_label(selected_value)
|
|
|
|
# Property 1: Container should have ARIA label
|
|
self.assertIsNotNone(
|
|
container_label,
|
|
"Container should have an ARIA label"
|
|
)
|
|
self.assertTrue(
|
|
len(container_label) > 0,
|
|
"Container ARIA label should not be empty"
|
|
)
|
|
|
|
# Property 2: Container label should contain "Rating"
|
|
self.assertIn(
|
|
"Rating",
|
|
container_label,
|
|
"Container ARIA label should contain 'Rating'"
|
|
)
|
|
|
|
# Property 3: Container label should contain selected value
|
|
self.assertIn(
|
|
str(selected_value),
|
|
container_label,
|
|
f"Container ARIA label should contain selected value {selected_value}"
|
|
)
|
|
|
|
# Property 4: Container label should contain max stars
|
|
self.assertIn(
|
|
str(self.max_stars),
|
|
container_label,
|
|
f"Container ARIA label should contain max stars {self.max_stars}"
|
|
)
|
|
|
|
# Property 5: Container label should contain "out of"
|
|
self.assertIn(
|
|
"out of",
|
|
container_label,
|
|
"Container ARIA label should contain 'out of'"
|
|
)
|
|
|
|
return container_label
|
|
|
|
# Feature: helpdesk-rating-five-stars, Property 21: ARIA labels present for accessibility
|
|
@given(star_number=st.integers(min_value=1, max_value=5))
|
|
@settings(max_examples=100, deadline=None)
|
|
def test_property_aria_labels_present(self, star_number):
|
|
"""
|
|
Property 21: ARIA labels present for accessibility
|
|
|
|
For any star in the rating form (1-5), the star should have an
|
|
appropriate ARIA label for screen reader compatibility.
|
|
|
|
This tests that:
|
|
1. Each star has a non-empty ARIA label
|
|
2. ARIA label contains the star number
|
|
3. ARIA label indicates the action ("Rate")
|
|
4. ARIA label indicates the scale ("out of 5")
|
|
5. ARIA label uses correct singular/plural form
|
|
|
|
Validates: Requirements 8.3
|
|
"""
|
|
self._verify_aria_label_property(star_number)
|
|
|
|
# Feature: helpdesk-rating-five-stars, Property 21: ARIA labels present for accessibility
|
|
@given(selected_value=st.integers(min_value=0, max_value=5))
|
|
@settings(max_examples=100, deadline=None)
|
|
def test_property_container_aria_attributes(self, selected_value):
|
|
"""
|
|
Property 21: ARIA labels present for accessibility (Container)
|
|
|
|
For any selected rating value (0-5), the container should have
|
|
appropriate ARIA attributes for screen reader compatibility.
|
|
|
|
This tests that:
|
|
1. Container has an ARIA label describing current rating
|
|
2. ARIA label contains "Rating"
|
|
3. ARIA label contains the selected value
|
|
4. ARIA label indicates the scale
|
|
|
|
Validates: Requirements 8.3
|
|
"""
|
|
self._verify_container_aria_attributes(selected_value)
|
|
|
|
def test_aria_label_for_each_star(self):
|
|
"""
|
|
Test that each star (1-5) has a proper ARIA label
|
|
"""
|
|
for star_number in range(1, self.max_stars + 1):
|
|
aria_label = self._get_aria_label_for_star(star_number)
|
|
|
|
# Verify label exists
|
|
self.assertIsNotNone(aria_label)
|
|
self.assertTrue(len(aria_label) > 0)
|
|
|
|
# Verify label contains key information
|
|
self.assertIn(str(star_number), aria_label)
|
|
self.assertIn("Rate", aria_label)
|
|
self.assertIn("out of", aria_label)
|
|
self.assertIn(str(self.max_stars), aria_label)
|
|
|
|
def test_aria_label_singular_plural(self):
|
|
"""
|
|
Test that ARIA labels use correct singular/plural form
|
|
"""
|
|
# Star 1 should use singular "star"
|
|
label_1 = self._get_aria_label_for_star(1)
|
|
self.assertIn("1 star", label_1)
|
|
self.assertNotIn("1 stars", label_1)
|
|
|
|
# Stars 2-5 should use plural "stars"
|
|
for star_number in range(2, self.max_stars + 1):
|
|
label = self._get_aria_label_for_star(star_number)
|
|
self.assertIn(f"{star_number} stars", label)
|
|
self.assertNotIn(f"{star_number} star out", label)
|
|
|
|
def test_aria_label_format(self):
|
|
"""
|
|
Test the exact format of ARIA labels
|
|
"""
|
|
# Test star 1
|
|
label_1 = self._get_aria_label_for_star(1)
|
|
self.assertEqual(
|
|
label_1,
|
|
"Rate 1 star out of 5",
|
|
"Star 1 ARIA label should match expected format"
|
|
)
|
|
|
|
# Test star 2
|
|
label_2 = self._get_aria_label_for_star(2)
|
|
self.assertEqual(
|
|
label_2,
|
|
"Rate 2 stars out of 5",
|
|
"Star 2 ARIA label should match expected format"
|
|
)
|
|
|
|
# Test star 3
|
|
label_3 = self._get_aria_label_for_star(3)
|
|
self.assertEqual(
|
|
label_3,
|
|
"Rate 3 stars out of 5",
|
|
"Star 3 ARIA label should match expected format"
|
|
)
|
|
|
|
# Test star 4
|
|
label_4 = self._get_aria_label_for_star(4)
|
|
self.assertEqual(
|
|
label_4,
|
|
"Rate 4 stars out of 5",
|
|
"Star 4 ARIA label should match expected format"
|
|
)
|
|
|
|
# Test star 5
|
|
label_5 = self._get_aria_label_for_star(5)
|
|
self.assertEqual(
|
|
label_5,
|
|
"Rate 5 stars out of 5",
|
|
"Star 5 ARIA label should match expected format"
|
|
)
|
|
|
|
def test_container_aria_label_format(self):
|
|
"""
|
|
Test the exact format of container ARIA label
|
|
"""
|
|
# Test with different selected values
|
|
for value in range(0, self.max_stars + 1):
|
|
container_label = self._get_container_aria_label(value)
|
|
expected = f"Rating: {value} out of 5 stars"
|
|
self.assertEqual(
|
|
container_label,
|
|
expected,
|
|
f"Container ARIA label for value {value} should match expected format"
|
|
)
|
|
|
|
def test_aria_labels_are_unique(self):
|
|
"""
|
|
Test that each star has a unique ARIA label
|
|
"""
|
|
labels = []
|
|
for star_number in range(1, self.max_stars + 1):
|
|
label = self._get_aria_label_for_star(star_number)
|
|
labels.append(label)
|
|
|
|
# All labels should be unique
|
|
unique_labels = set(labels)
|
|
self.assertEqual(
|
|
len(unique_labels),
|
|
len(labels),
|
|
"Each star should have a unique ARIA label"
|
|
)
|
|
|
|
def test_aria_labels_are_descriptive(self):
|
|
"""
|
|
Test that ARIA labels are descriptive enough for screen readers
|
|
"""
|
|
for star_number in range(1, self.max_stars + 1):
|
|
label = self._get_aria_label_for_star(star_number)
|
|
|
|
# Label should be at least 10 characters (descriptive enough)
|
|
self.assertGreaterEqual(
|
|
len(label),
|
|
10,
|
|
f"ARIA label for star {star_number} should be descriptive (at least 10 chars)"
|
|
)
|
|
|
|
# Label should contain spaces (not just concatenated words)
|
|
self.assertIn(
|
|
" ",
|
|
label,
|
|
f"ARIA label for star {star_number} should contain spaces"
|
|
)
|
|
|
|
def test_aria_labels_consistency(self):
|
|
"""
|
|
Test that ARIA labels are consistent across multiple calls
|
|
"""
|
|
for star_number in range(1, self.max_stars + 1):
|
|
# Get label multiple times
|
|
label1 = self._get_aria_label_for_star(star_number)
|
|
label2 = self._get_aria_label_for_star(star_number)
|
|
label3 = self._get_aria_label_for_star(star_number)
|
|
|
|
# All should be identical
|
|
self.assertEqual(
|
|
label1,
|
|
label2,
|
|
f"ARIA label for star {star_number} should be consistent"
|
|
)
|
|
self.assertEqual(
|
|
label2,
|
|
label3,
|
|
f"ARIA label for star {star_number} should be consistent"
|
|
)
|
|
|
|
def test_aria_labels_no_special_characters(self):
|
|
"""
|
|
Test that ARIA labels don't contain problematic special characters
|
|
"""
|
|
for star_number in range(1, self.max_stars + 1):
|
|
label = self._get_aria_label_for_star(star_number)
|
|
|
|
# Should not contain HTML tags
|
|
self.assertNotIn("<", label)
|
|
self.assertNotIn(">", label)
|
|
|
|
# Should not contain quotes that could break attributes
|
|
self.assertNotIn('"', label)
|
|
|
|
# Should not contain newlines
|
|
self.assertNotIn("\n", label)
|
|
self.assertNotIn("\r", label)
|
|
|
|
def test_aria_labels_screen_reader_friendly(self):
|
|
"""
|
|
Test that ARIA labels are screen reader friendly
|
|
"""
|
|
for star_number in range(1, self.max_stars + 1):
|
|
label = self._get_aria_label_for_star(star_number)
|
|
|
|
# Should start with an action verb for clarity
|
|
self.assertTrue(
|
|
label.startswith("Rate"),
|
|
f"ARIA label should start with action verb 'Rate'"
|
|
)
|
|
|
|
# Should be in sentence case (not all caps)
|
|
self.assertNotEqual(
|
|
label,
|
|
label.upper(),
|
|
"ARIA label should not be all uppercase"
|
|
)
|
|
|
|
def test_container_aria_attributes_for_all_values(self):
|
|
"""
|
|
Test container ARIA attributes for all possible rating values
|
|
"""
|
|
for value in range(0, self.max_stars + 1):
|
|
container_label = self._get_container_aria_label(value)
|
|
|
|
# Verify label exists and is descriptive
|
|
self.assertIsNotNone(container_label)
|
|
self.assertTrue(len(container_label) > 0)
|
|
|
|
# Verify label contains key information
|
|
self.assertIn("Rating", container_label)
|
|
self.assertIn(str(value), container_label)
|
|
self.assertIn("out of", container_label)
|
|
self.assertIn(str(self.max_stars), container_label)
|
|
|
|
def test_aria_labels_internationalization_ready(self):
|
|
"""
|
|
Test that ARIA labels are structured for easy internationalization
|
|
"""
|
|
# The current implementation uses English strings
|
|
# This test verifies the structure is consistent and could be translated
|
|
|
|
for star_number in range(1, self.max_stars + 1):
|
|
label = self._get_aria_label_for_star(star_number)
|
|
|
|
# Label should follow a consistent pattern
|
|
# "Rate X star(s) out of Y"
|
|
parts = label.split()
|
|
|
|
# Should have at least 5 words
|
|
self.assertGreaterEqual(
|
|
len(parts),
|
|
5,
|
|
f"ARIA label should have consistent structure with multiple words"
|
|
)
|
|
|
|
# First word should be "Rate"
|
|
self.assertEqual(
|
|
parts[0],
|
|
"Rate",
|
|
"ARIA label should start with 'Rate'"
|
|
)
|
|
|
|
def test_aria_labels_wcag_compliance(self):
|
|
"""
|
|
Test that ARIA labels meet WCAG 2.1 AA accessibility standards
|
|
"""
|
|
# WCAG requires that interactive elements have accessible names
|
|
# and that the names are descriptive
|
|
|
|
for star_number in range(1, self.max_stars + 1):
|
|
label = self._get_aria_label_for_star(star_number)
|
|
|
|
# 1. Label must exist (WCAG 4.1.2)
|
|
self.assertIsNotNone(label)
|
|
self.assertTrue(len(label) > 0)
|
|
|
|
# 2. Label must be descriptive (WCAG 2.4.6)
|
|
# Should describe both the action and the result
|
|
self.assertIn("Rate", label) # Action
|
|
self.assertIn(str(star_number), label) # Result
|
|
|
|
# 3. Label must provide context (WCAG 3.3.2)
|
|
# Should indicate the scale
|
|
self.assertIn("out of", label)
|
|
self.assertIn(str(self.max_stars), label)
|
|
|
|
def test_aria_labels_all_stars_have_labels(self):
|
|
"""
|
|
Test that all 5 stars have ARIA labels (no missing labels)
|
|
"""
|
|
labels = []
|
|
|
|
for star_number in range(1, self.max_stars + 1):
|
|
label = self._get_aria_label_for_star(star_number)
|
|
labels.append(label)
|
|
|
|
# Should have exactly 5 labels
|
|
self.assertEqual(
|
|
len(labels),
|
|
self.max_stars,
|
|
f"Should have {self.max_stars} ARIA labels"
|
|
)
|
|
|
|
# All labels should be non-empty
|
|
for i, label in enumerate(labels, start=1):
|
|
self.assertTrue(
|
|
len(label) > 0,
|
|
f"Star {i} should have a non-empty ARIA label"
|
|
)
|
|
|
|
def test_aria_labels_boundary_values(self):
|
|
"""
|
|
Test ARIA labels for boundary values (first and last star)
|
|
"""
|
|
# First star (1)
|
|
label_first = self._get_aria_label_for_star(1)
|
|
self.assertEqual(label_first, "Rate 1 star out of 5")
|
|
|
|
# Last star (5)
|
|
label_last = self._get_aria_label_for_star(self.max_stars)
|
|
self.assertEqual(label_last, f"Rate {self.max_stars} stars out of 5")
|
|
|
|
def test_container_aria_label_boundary_values(self):
|
|
"""
|
|
Test container ARIA label for boundary values
|
|
"""
|
|
# No rating (0)
|
|
label_zero = self._get_container_aria_label(0)
|
|
self.assertEqual(label_zero, "Rating: 0 out of 5 stars")
|
|
|
|
# Maximum rating (5)
|
|
label_max = self._get_container_aria_label(self.max_stars)
|
|
self.assertEqual(label_max, f"Rating: {self.max_stars} out of 5 stars")
|
|
|
|
def test_aria_labels_provide_complete_information(self):
|
|
"""
|
|
Test that ARIA labels provide complete information for screen reader users
|
|
"""
|
|
for star_number in range(1, self.max_stars + 1):
|
|
label = self._get_aria_label_for_star(star_number)
|
|
|
|
# A screen reader user should understand:
|
|
# 1. What action they can take ("Rate")
|
|
self.assertIn("Rate", label)
|
|
|
|
# 2. What value they're selecting (the star number)
|
|
self.assertIn(str(star_number), label)
|
|
|
|
# 3. What the scale is ("out of 5")
|
|
self.assertIn("out of", label)
|
|
self.assertIn(str(self.max_stars), label)
|
|
|
|
# 4. The unit of measurement ("star" or "stars")
|
|
self.assertTrue(
|
|
"star" in label or "stars" in label,
|
|
"ARIA label should contain 'star' or 'stars'"
|
|
)
|