Skip to content

Commit a249d16

Browse files
test: Implemented Log execution time per prompt in Report for KM generic
2 parents adab8fe + 68d603b commit a249d16

File tree

8 files changed

+188
-49
lines changed

8 files changed

+188
-49
lines changed

tests/e2e-test/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Installing Playwright Pytest from Virtual Environment
2424

2525
Run test cases
2626

27-
- To run test cases from your 'tests' folder : "pytest --html=report.html --self-contained-html"
27+
- To run test cases from your 'tests/e2e-test' folder : "pytest --html=report.html --self-contained-html"
2828

2929
Create .env file in project root level with web app url and client credentials
3030

tests/e2e-test/pages/HomePage.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@ class HomePage(BasePage):
66
SEND_BUTTON = "//button[@title='Send Question']"
77
SHOW_CHAT_HISTORY_BUTTON = "//button[normalize-space()='Show Chat History']"
88
HIDE_CHAT_HISTORY_BUTTON = "//button[normalize-space()='Hide Chat History']"
9-
CHAT_HISTORY_NAME = "//div[contains(translate(text(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'call')]"
9+
CHAT_HISTORY_NAME = "//div[contains(@class, 'ChatHistoryListItemCell_chatTitle')]"
1010
CLEAR_CHAT_HISTORY_MENU = "//button[@id='moreButton']"
1111
CLEAR_CHAT_HISTORY = "//button[@role='menuitem']"
12+
REFERENCE_LINKS_IN_RESPONSE = "//span[@role='button' and contains(@class, 'citationContainer')]"
13+
CLOSE_BUTTON = "svg[role='button'][tabindex='0']"
14+
15+
1216

1317
def __init__(self, page):
1418
self.page = page
@@ -40,7 +44,7 @@ def show_chat_history(self):
4044
self.page.wait_for_load_state('networkidle')
4145
self.page.wait_for_timeout(2000)
4246
try:
43-
expect(self.page.locator(self.CHAT_HISTORY_NAME)).to_be_visible(timeout=7000)
47+
expect(self.page.locator(self.CHAT_HISTORY_NAME)).to_be_visible(timeout=9000)
4448
except AssertionError:
4549
raise AssertionError("Chat history name was not visible on the page within the expected time.")
4650

@@ -67,5 +71,34 @@ def close_chat_history(self):
6771
self.page.locator(self.HIDE_CHAT_HISTORY_BUTTON).click()
6872
self.page.wait_for_load_state('networkidle')
6973
self.page.wait_for_timeout(2000)
74+
75+
def click_reference_link_in_response(self):
76+
# Click on reference link response
77+
BasePage.scroll_into_view(self, self.page.locator(self.REFERENCE_LINKS_IN_RESPONSE))
78+
self.page.wait_for_timeout(2000)
79+
reference_links = self.page.locator(self.REFERENCE_LINKS_IN_RESPONSE)
80+
reference_links.nth(reference_links.count() - 1).click()
81+
# self.page.locator(self.REFERENCE_LINKS_IN_RESPONSE).click()
82+
self.page.wait_for_load_state('networkidle')
83+
self.page.wait_for_timeout(2000)
84+
85+
86+
def close_citation(self):
87+
self.page.wait_for_timeout(3000)
88+
89+
close_btn = self.page.locator(self.CLOSE_BUTTON)
90+
close_btn.wait_for(state="attached", timeout=5000)
91+
# bring it into view just in case
92+
close_btn.scroll_into_view_if_needed()
93+
# force the click, bypassing the aria-hidden check
94+
close_btn.click(force=True)
95+
self.page.wait_for_timeout(5000)
96+
97+
def has_reference_link(self):
98+
# Get all assistant messages
99+
assistant_messages = self.page.locator("div.chat-message.assistant")
100+
last_assistant = assistant_messages.nth(assistant_messages.count() - 1)
70101

71-
102+
# Use XPath properly by prefixing with 'xpath='
103+
reference_links = last_assistant.locator("xpath=.//span[@role='button' and contains(@class, 'citationContainer')]")
104+
return reference_links.count() > 0

tests/e2e-test/pytest.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ log_cli = true
33
log_cli_level = INFO
44
log_file = logs/tests.log
55
log_file_level = INFO
6-
addopts = -p no:warnings
6+
addopts = -p no:warnings --tb=short

tests/e2e-test/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ python-dotenv
44
pytest-check
55
pytest-html
66
py
7+
beautifulsoup4
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
url = 'web app url'
2-
api_url = 'api app url"
2+
api_url = "api app url"

tests/e2e-test/testdata/prompts.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"questions":[
33

44
"Total number of calls by date for last 7 days",
5+
"Generate a line chart",
56
"Show average handling time by topics in minutes",
67
"What are top 7 challenges user reported?",
78
"Give a summary of billing issues",

tests/e2e-test/tests/conftest.py

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
from dotenv import load_dotenv
88
import os
99
from py.xml import html # type: ignore
10-
10+
import io
11+
import logging
12+
from bs4 import BeautifulSoup
13+
import atexit
1114

1215
@pytest.fixture(scope="session")
1316
def login_logout():
@@ -28,25 +31,74 @@ def login_logout():
2831
# perform close the browser
2932
browser.close()
3033

34+
log_streams = {}
3135

3236
@pytest.hookimpl(tryfirst=True)
33-
def pytest_html_report_title(report):
34-
report.title = "Automation_KM_Generic"
37+
def pytest_runtest_setup(item):
38+
# Prepare StringIO for capturing logs
39+
stream = io.StringIO()
40+
handler = logging.StreamHandler(stream)
41+
handler.setLevel(logging.INFO)
3542

43+
logger = logging.getLogger()
44+
logger.addHandler(handler)
3645

37-
# Add a column for descriptions
38-
def pytest_html_results_table_header(cells):
39-
cells.insert(1, html.th("Description"))
46+
# Save handler and stream
47+
log_streams[item.nodeid] = (handler, stream)
4048

41-
def pytest_html_results_table_row(report, cells):
42-
cells.insert(1, html.td(report.description if hasattr(report, "description") else ""))
4349

44-
# Add logs and docstring to report
4550
@pytest.hookimpl(hookwrapper=True)
4651
def pytest_runtest_makereport(item, call):
4752
outcome = yield
4853
report = outcome.get_result()
49-
report.description = str(item.function.__doc__)
50-
os.makedirs("logs", exist_ok=True)
51-
extra = getattr(report, 'extra', [])
52-
report.extra = extra
54+
55+
handler, stream = log_streams.get(item.nodeid, (None, None))
56+
57+
if handler and stream:
58+
# Make sure logs are flushed
59+
handler.flush()
60+
log_output = stream.getvalue()
61+
62+
# Only remove the handler, don't close the stream yet
63+
logger = logging.getLogger()
64+
logger.removeHandler(handler)
65+
66+
# Store the log output on the report object for HTML reporting
67+
report.description = f"<pre>{log_output.strip()}</pre>"
68+
69+
# Clean up references
70+
log_streams.pop(item.nodeid, None)
71+
else:
72+
report.description = ""
73+
74+
def pytest_collection_modifyitems(items):
75+
for item in items:
76+
if hasattr(item, 'callspec'):
77+
prompt = item.callspec.params.get("prompt")
78+
if prompt:
79+
item._nodeid = prompt # This controls how the test name appears in the report
80+
81+
def rename_duration_column():
82+
report_path = os.path.abspath("report.html") # or your report filename
83+
if not os.path.exists(report_path):
84+
print("Report file not found, skipping column rename.")
85+
return
86+
87+
with open(report_path, 'r', encoding='utf-8') as f:
88+
soup = BeautifulSoup(f, 'html.parser')
89+
90+
# Find and rename the header
91+
headers = soup.select('table#results-table thead th')
92+
for th in headers:
93+
if th.text.strip() == 'Duration':
94+
th.string = 'Execution Time'
95+
#print("Renamed 'Duration' to 'Execution Time'")
96+
break
97+
else:
98+
print("'Duration' column not found in report.")
99+
100+
with open(report_path, 'w', encoding='utf-8') as f:
101+
f.write(str(soup))
102+
103+
# Register this function to run after everything is done
104+
atexit.register(rename_duration_column)
Lines changed: 82 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,91 @@
1+
import time
2+
import logging
3+
import pytest
4+
from pytest_check import check
15
from pages.HomePage import HomePage
26
from config.constants import *
3-
from pytest_check import check
4-
import logging
7+
import io
58

69
logger = logging.getLogger(__name__)
710

8-
def test_KM_Generic_Golden_path_test(login_logout):
11+
# Helper to validate the final response text
12+
def _validate_response_text(page, question):
13+
response_text = page.locator("//p")
14+
last_response = response_text.nth(response_text.count() - 1).text_content()
15+
check.not_equal(
16+
"I cannot answer this question from the data available. Please rephrase or add more details.",
17+
last_response,
18+
f"Invalid response for: {question}"
19+
)
20+
check.not_equal(
21+
"Chart cannot be generated.",
22+
last_response,
23+
f"Invalid response for: {question}"
24+
)
25+
26+
# Helper to check and close citation if it exists
27+
# def _check_and_close_citation(home):
28+
# if home.has_reference_link():
29+
# logger.info("Step: Reference link found. Opening citation.")
30+
# home.click_reference_link_in_response()
31+
# logger.info("Step: Closing citation.")
32+
# home.close_citation()
33+
34+
# Define test steps
35+
test_steps = [
36+
("Validate home page is loaded", lambda home: home.home_page_load()),
37+
("Validate delete chat history", lambda home: home.delete_chat_history()),
38+
]
39+
40+
# Add golden path question prompts
41+
for i, q in enumerate(questions, start=1):
42+
def _question_step(home, q=q): # q is default arg to avoid late binding
43+
home.enter_chat_question(q)
44+
home.click_send_button()
45+
home.validate_response_status(q)
46+
_validate_response_text(home.page, q)
47+
48+
# Include citation check directly
49+
if home.has_reference_link():
50+
logger.info(f"[{q}] Reference link found. Opening citation.")
51+
home.click_reference_link_in_response()
52+
logger.info(f"[{q}] Closing citation.")
53+
home.close_citation()
54+
55+
test_steps.append((f"Validate response for GP Prompt: {q}", _question_step))
56+
57+
# Final chat history validation
58+
test_steps.extend([
59+
("Validate chat history is saved", lambda home: home.show_chat_history()),
60+
("Validate chat history is closed", lambda home: home.close_chat_history()),
61+
])
62+
63+
# Test ID display for reporting
64+
test_ids = [f"{i+1:02d}. {desc}" for i, (desc, _) in enumerate(test_steps)]
65+
66+
@pytest.mark.parametrize("description, step", test_steps, ids=test_ids)
67+
def test_KM_Generic_Golden_Path(login_logout, description, step, request):
68+
request.node._nodeid = description
969

10-
"""Validate Golden path test case for KM Generic Accelerator """
1170
page = login_logout
1271
home_page = HomePage(page)
13-
14-
logger.info("Step 1: Validate home page is loaded.")
15-
home_page.home_page_load()
16-
17-
logger.info("Step 2: Validate delete chat history.")
18-
home_page.delete_chat_history()
19-
20-
logger.info("Step 3: Validate GP Prompts response.")
21-
for question in questions:
22-
#enter question
23-
home_page.enter_chat_question(question)
24-
# click on send button
25-
home_page.click_send_button()
26-
#validate the response status
27-
home_page.validate_response_status(question)
28-
#validate response text
29-
response_text = page.locator("//p")
30-
#assert response text
31-
check.not_equal("I cannot answer this question from the data available. Please rephrase or add more details.", response_text.nth(response_text.count() - 1).text_content(), f"Invalid response for : {question}")
32-
check.not_equal("Chart cannot be generated.", response_text.nth(response_text.count() - 1).text_content(), f"Invalid response for : {question}")
33-
34-
35-
logger.info("Step 4: Validate chat history.")
36-
home_page.show_chat_history()
72+
home_page.page = page
73+
74+
log_capture = io.StringIO()
75+
handler = logging.StreamHandler(log_capture)
76+
logger.addHandler(handler)
77+
78+
logger.info(f"Running test step: {description}")
79+
start = time.time()
80+
81+
try:
82+
step(home_page)
83+
finally:
84+
duration = time.time() - start
85+
logger.info(f"Execution Time for '{description}': {duration:.2f}s")
86+
logger.removeHandler(handler)
3787

38-
logger.info("Step 5: Validate close chat history.")
39-
home_page.close_chat_history()
88+
# Attach logs
89+
request.node._report_sections.append((
90+
"call", "log", log_capture.getvalue()
91+
))

0 commit comments

Comments
 (0)