451 lines
18 KiB
Python
451 lines
18 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:
|
|
# Machine-speed: Quick check if question is already visible (returns immediately if ready)
|
|
try:
|
|
from selenium.webdriver.common.by import By
|
|
from selenium.webdriver.support.ui import WebDriverWait
|
|
from selenium.webdriver.support import expected_conditions as EC
|
|
WebDriverWait(self.driver, 0.5).until(
|
|
EC.presence_of_element_located(
|
|
(By.CSS_SELECTOR, f"[data-testid='domain_question__{question_id}']")
|
|
)
|
|
)
|
|
except:
|
|
# Question might already be visible, continue
|
|
pass
|
|
|
|
# First, check for container elements (more reliable)
|
|
# Machine-speed: Reduced timeout from 3s to 0.5s
|
|
if self._element_exists(f"[data-testid='domain_question__{question_id}__multiple_choice']", timeout=0.5):
|
|
return "multiple_choice"
|
|
|
|
# Check for true/false container - machine-speed: 0.5s timeout
|
|
if self._element_exists(f"[data-testid='domain_question__{question_id}__true_false']", timeout=0.5):
|
|
return "true_false"
|
|
|
|
# Check for rating scale container - machine-speed: 0.5s timeout
|
|
if self._element_exists(f"[data-testid='domain_question__{question_id}__rating_scale']", timeout=0.5):
|
|
return "rating_scale"
|
|
|
|
# Check for open ended container - machine-speed: 0.5s timeout
|
|
if self._element_exists(f"[data-testid='domain_question__{question_id}__open_ended']", timeout=0.5):
|
|
return "open_ended"
|
|
|
|
# Check for matrix container - machine-speed: 0.5s timeout
|
|
if self._element_exists(f"[data-testid='domain_question__{question_id}__matrix']", timeout=0.5):
|
|
return "matrix"
|
|
|
|
# Fallback: Check for individual answer elements (if containers not found)
|
|
# Check for multiple choice options (A, B, C, D, E) - machine-speed: 0.5s timeout
|
|
for label in ['A', 'B', 'C', 'D', 'E']:
|
|
if self._element_exists(f"[data-testid='domain_question__{question_id}__option_{label}']", timeout=0.5):
|
|
return "multiple_choice"
|
|
|
|
# Check for true/false buttons - machine-speed: 0.5s timeout
|
|
if self._element_exists(f"[data-testid='domain_question__{question_id}__truefalse_True']", timeout=0.5):
|
|
return "true_false"
|
|
if self._element_exists(f"[data-testid='domain_question__{question_id}__truefalse_False']", timeout=0.5):
|
|
return "true_false"
|
|
|
|
# Check for rating scale buttons - machine-speed: 0.5s timeout
|
|
for rating in ['1', '2', '3', '4', '5']:
|
|
if self._element_exists(f"[data-testid='domain_question__{question_id}__rating_{rating}']", timeout=0.5):
|
|
return "rating_scale"
|
|
|
|
# Check for open ended textarea - machine-speed: 0.5s timeout
|
|
if self._element_exists(f"[data-testid='domain_question__{question_id}__textarea']", timeout=0.5):
|
|
return "open_ended"
|
|
|
|
# Check for matrix cells - machine-speed: 0.5s timeout
|
|
if self._element_exists(f"[data-testid^='domain_question__{question_id}__matrix_']", timeout=0.5):
|
|
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=0.5):
|
|
"""Check if element exists quickly (machine-speed optimized)"""
|
|
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 (machine-speed: reduced timeout from 2s to 0.5s)
|
|
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, 0.5).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}']")
|
|
# Machine-speed: Reduced timeout from 10s to 2s (element should be ready quickly)
|
|
element = WebDriverWait(self.driver, 2).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}']")
|
|
|
|
# Machine-speed: Reduced timeout from 10s to 2s
|
|
element = WebDriverWait(self.driver, 2).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}']")
|
|
|
|
# Machine-speed: Reduced timeout from 10s to 2s
|
|
element = WebDriverWait(self.driver, 2).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']")
|
|
|
|
# Machine-speed: Reduced timeout from 10s to 2s
|
|
element = WebDriverWait(self.driver, 2).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}']")
|
|
|
|
# Machine-speed: Reduced timeout from 10s to 2s
|
|
element = WebDriverWait(self.driver, 2).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
|
|
|