CP_AUTOMATION/utils/question_answer_helper.py
2025-12-12 19:54:54 +05:30

436 lines
17 KiB
Python

"""
Question Answer Helper Utility
World-class utility for answering all 5 question types in domain assessments.
Provides intelligent, reliable, and fast question answering logic.
"""
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException, NoSuchElementException
import random
import re
class QuestionAnswerHelper:
"""Helper class for answering questions in domain assessments"""
def __init__(self, driver):
"""
Initialize Question Answer Helper
Args:
driver: WebDriver instance
"""
self.driver = driver
self.wait_timeout = 10
def get_question_id(self, question_element=None):
"""
Extract question ID from question element or current page
Args:
question_element: Optional WebElement for question container
Returns:
str: Question ID or None
"""
try:
if question_element:
test_id = question_element.get_attribute("data-testid")
else:
# Find current question on page - wait for it to appear
try:
question_element = WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "[data-testid^='domain_question__']"))
)
test_id = question_element.get_attribute("data-testid")
except:
# Try finding by question container (without __)
try:
question_element = WebDriverWait(self.driver, 5).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "[data-testid^='domain_question__'][data-testid$='']"))
)
test_id = question_element.get_attribute("data-testid")
except:
# Last resort: find any question element
question_elements = self.driver.find_elements(By.CSS_SELECTOR, "[data-testid^='domain_question__']")
if question_elements:
# Get the first one that matches the pattern (not a sub-element)
for elem in question_elements:
test_id = elem.get_attribute("data-testid")
if test_id and not any(x in test_id for x in ['__option_', '__truefalse_', '__rating_', '__textarea', '__matrix_', '__header', '__text', '__number']):
question_element = elem
break
if question_element:
test_id = question_element.get_attribute("data-testid")
if test_id:
match = re.search(r'domain_question__(\d+)(?:__|$)', test_id)
if match:
return match.group(1)
except Exception as e:
print(f"⚠️ Error getting question ID: {e}")
return None
def get_question_type(self, question_id):
"""
Determine question type by checking available answer elements
Args:
question_id: Question ID
Returns:
str: Question type (multiple_choice, true_false, rating_scale, open_ended, matrix)
"""
try:
# Wait a moment for question to fully render
import time
time.sleep(0.5)
# First, check for container elements (more reliable)
# Check for multiple choice container
if self._element_exists(f"[data-testid='domain_question__{question_id}__multiple_choice']", timeout=3):
return "multiple_choice"
# Check for true/false container
if self._element_exists(f"[data-testid='domain_question__{question_id}__true_false']", timeout=3):
return "true_false"
# Check for rating scale container
if self._element_exists(f"[data-testid='domain_question__{question_id}__rating_scale']", timeout=3):
return "rating_scale"
# Check for open ended container
if self._element_exists(f"[data-testid='domain_question__{question_id}__open_ended']", timeout=3):
return "open_ended"
# Check for matrix container
if self._element_exists(f"[data-testid='domain_question__{question_id}__matrix']", timeout=3):
return "matrix"
# Fallback: Check for individual answer elements (if containers not found)
# Check for multiple choice options (A, B, C, D, E)
for label in ['A', 'B', 'C', 'D', 'E']:
if self._element_exists(f"[data-testid='domain_question__{question_id}__option_{label}']", timeout=2):
return "multiple_choice"
# Check for true/false buttons
if self._element_exists(f"[data-testid='domain_question__{question_id}__truefalse_True']", timeout=2):
return "true_false"
if self._element_exists(f"[data-testid='domain_question__{question_id}__truefalse_False']", timeout=2):
return "true_false"
# Check for rating scale buttons
for rating in ['1', '2', '3', '4', '5']:
if self._element_exists(f"[data-testid='domain_question__{question_id}__rating_{rating}']", timeout=2):
return "rating_scale"
# Check for open ended textarea
if self._element_exists(f"[data-testid='domain_question__{question_id}__textarea']", timeout=2):
return "open_ended"
# Check for matrix cells
if self._element_exists(f"[data-testid^='domain_question__{question_id}__matrix_']", timeout=2):
return "matrix"
return "unknown"
except Exception as e:
print(f"⚠️ Error detecting question type for {question_id}: {e}")
return "unknown"
def _element_exists(self, css_selector, timeout=2):
"""Check if element exists quickly"""
try:
WebDriverWait(self.driver, timeout).until(
EC.presence_of_element_located((By.CSS_SELECTOR, css_selector))
)
return True
except:
return False
def answer_multiple_choice(self, question_id, option_label=None):
"""
Answer multiple choice question
Args:
question_id: Question ID
option_label: Option label (A, B, C, D, E) - if None, selects random
Returns:
str: Selected option label
"""
# Get available options
options = []
for label in ['A', 'B', 'C', 'D', 'E']:
locator = (By.CSS_SELECTOR, f"[data-testid='domain_question__{question_id}__option_{label}']")
try:
element = WebDriverWait(self.driver, 2).until(
EC.presence_of_element_located(locator)
)
if element.is_displayed():
options.append(label)
except:
continue
if not options:
raise Exception(f"No options found for question {question_id}")
# Select option
if option_label is None:
option_label = random.choice(options)
elif option_label not in options:
option_label = random.choice(options) # Fallback to random
locator = (By.CSS_SELECTOR, f"[data-testid='domain_question__{question_id}__option_{option_label}']")
element = WebDriverWait(self.driver, self.wait_timeout).until(
EC.element_to_be_clickable(locator)
)
element.click()
return option_label
def answer_true_false(self, question_id, value=None):
"""
Answer true/false question
Args:
question_id: Question ID
value: True or False - if None, selects random
Returns:
str: Selected value ("True" or "False")
"""
if value is None:
value = random.choice([True, False])
value_str = "True" if value else "False"
locator = (By.CSS_SELECTOR, f"[data-testid='domain_question__{question_id}__truefalse_{value_str}']")
element = WebDriverWait(self.driver, self.wait_timeout).until(
EC.element_to_be_clickable(locator)
)
element.click()
return value_str
def answer_rating_scale(self, question_id, score=None):
"""
Answer rating scale question
Args:
question_id: Question ID
score: Rating score/value - if None, selects random
Returns:
str: Selected score/value
"""
# First, find all rating options dynamically (not just '1'-'5')
# Rating options can have any value (numeric strings, labels, etc.)
rating_options = []
# Method 1: Find all elements with rating pattern
try:
all_rating_elements = self.driver.find_elements(
By.CSS_SELECTOR,
f"[data-testid^='domain_question__{question_id}__rating_']"
)
for elem in all_rating_elements:
test_id = elem.get_attribute('data-testid')
if test_id:
# Extract value from pattern: domain_question__{id}__rating_{value}
match = re.search(r'__rating_(.+)$', test_id)
if match:
value = match.group(1)
if elem.is_displayed():
rating_options.append(value)
except:
pass
# Method 2: Fallback - try common numeric values if nothing found
if not rating_options:
for s in ['1', '2', '3', '4', '5']:
locator = (By.CSS_SELECTOR, f"[data-testid='domain_question__{question_id}__rating_{s}']")
try:
element = WebDriverWait(self.driver, 2).until(
EC.presence_of_element_located(locator)
)
if element.is_displayed():
rating_options.append(s)
except:
continue
if not rating_options:
raise Exception(f"No rating options found for question {question_id}")
# Select score
if score is None:
selected_value = random.choice(rating_options)
else:
score_str = str(score)
if score_str in rating_options:
selected_value = score_str
else:
# Fallback to random if provided score not found
selected_value = random.choice(rating_options)
locator = (By.CSS_SELECTOR, f"[data-testid='domain_question__{question_id}__rating_{selected_value}']")
element = WebDriverWait(self.driver, self.wait_timeout).until(
EC.element_to_be_clickable(locator)
)
element.click()
return selected_value
def answer_open_ended(self, question_id, text=None):
"""
Answer open-ended question
Args:
question_id: Question ID
text: Answer text - if None, generates default text
Returns:
str: Entered text
"""
if text is None:
text = "This is a thoughtful response to the open-ended question. I believe this answer demonstrates understanding and reflection."
locator = (By.CSS_SELECTOR, f"[data-testid='domain_question__{question_id}__textarea']")
element = WebDriverWait(self.driver, self.wait_timeout).until(
EC.presence_of_element_located(locator)
)
element.clear()
element.send_keys(text)
return text
def answer_matrix(self, question_id, row_index=None, column_index=None):
"""
Answer matrix question
Args:
question_id: Question ID
row_index: Row index (0-based) - if None, selects random
column_index: Column index (0-based) - if None, selects random
Returns:
tuple: (row_index, column_index) selected
"""
# Get matrix dimensions
matrix_cells = self.driver.find_elements(
By.CSS_SELECTOR,
f"[data-testid^='domain_question__{question_id}__matrix_']"
)
if not matrix_cells:
raise Exception(f"No matrix cells found for question {question_id}")
# Extract available row/column indices
rows = set()
cols = set()
for cell in matrix_cells:
test_id = cell.get_attribute("data-testid")
if test_id:
match = re.search(r'matrix_(\d+)_(\d+)', test_id)
if match:
rows.add(int(match.group(1)))
cols.add(int(match.group(2)))
if not rows or not cols:
raise Exception(f"Could not determine matrix dimensions for question {question_id}")
# Select indices
if row_index is None:
row_index = random.choice(list(rows))
elif row_index not in rows:
row_index = random.choice(list(rows)) # Fallback
if column_index is None:
column_index = random.choice(list(cols))
elif column_index not in cols:
column_index = random.choice(list(cols)) # Fallback
# Click matrix cell
locator = (By.CSS_SELECTOR,
f"[data-testid='domain_question__{question_id}__matrix_{row_index}_{column_index}']")
element = WebDriverWait(self.driver, self.wait_timeout).until(
EC.element_to_be_clickable(locator)
)
element.click()
return (row_index, column_index)
def answer_question(self, question_id=None, question_type=None, **kwargs):
"""
Universal method to answer any question type
Args:
question_id: Question ID (if None, auto-detects)
question_type: Question type (if None, auto-detects)
**kwargs: Additional arguments for specific question types
- option_label: For multiple_choice
- value: For true_false
- score: For rating_scale
- text: For open_ended
- row_index, column_index: For matrix
Returns:
dict: Answer details
"""
# Auto-detect question ID if not provided
if question_id is None:
question_id = self.get_question_id()
if not question_id:
raise Exception("Could not determine question ID")
# Auto-detect question type if not provided
if question_type is None:
question_type = self.get_question_type(question_id)
if question_type == "unknown":
raise Exception(f"Could not determine question type for question {question_id}")
# Answer based on type
result = {
'question_id': question_id,
'question_type': question_type,
'answer': None
}
if question_type == "multiple_choice":
option_label = kwargs.get('option_label')
result['answer'] = self.answer_multiple_choice(question_id, option_label)
result['option_label'] = result['answer']
elif question_type == "true_false":
value = kwargs.get('value')
result['answer'] = self.answer_true_false(question_id, value)
result['value'] = result['answer']
elif question_type == "rating_scale":
score = kwargs.get('score')
result['answer'] = self.answer_rating_scale(question_id, score)
result['score'] = result['answer']
elif question_type == "open_ended":
text = kwargs.get('text')
result['answer'] = self.answer_open_ended(question_id, text)
result['text'] = result['answer']
elif question_type == "matrix":
row_index = kwargs.get('row_index')
column_index = kwargs.get('column_index')
row_idx, col_idx = self.answer_matrix(question_id, row_index, column_index)
result['answer'] = f"matrix_{row_idx}_{col_idx}"
result['row_index'] = row_idx
result['column_index'] = col_idx
else:
raise Exception(f"Unsupported question type: {question_type}")
return result