diff --git a/tests/e2e-test/base/__init__.py b/tests/e2e-test/base/__init__.py index e69de29bb..301469722 100644 --- a/tests/e2e-test/base/__init__.py +++ b/tests/e2e-test/base/__init__.py @@ -0,0 +1,2 @@ + +"""Initialize the base package.""" diff --git a/tests/e2e-test/base/base.py b/tests/e2e-test/base/base.py index 5fa27141d..31609fb4b 100644 --- a/tests/e2e-test/base/base.py +++ b/tests/e2e-test/base/base.py @@ -1,36 +1,66 @@ -from config.constants import API_URL +"""Module for storing application-wide constants.""" + +import os from dotenv import load_dotenv +# Removed unused import: from config.constants import API_URL + class BasePage: + """Base class for some common utilities and functions.""" + def __init__(self, page): + """Initialize the BasePage with a Playwright page instance.""" self.page = page def scroll_into_view(self, locator): + """Scroll the last element in the locator into view if needed.""" reference_list = locator locator.nth(reference_list.count() - 1).scroll_into_view_if_needed() def is_visible(self, locator): + """Check if the given locator is visible.""" locator.is_visible() - def validate_response_status(self): - + def get_first_plan_id(self): + """Step 1: Get plan list and return the first plan ID.""" load_dotenv() + base_url = os.getenv("API_URL") + get_url = f"{base_url}/api/plans" + headers = { + "Accept": "*/*", + } - # The URL of the API endpoint you want to access - api_url = f"{API_URL}/api/plans" + response = self.page.request.get(get_url, headers=headers, timeout=120000) + if response.status != 200: + raise AssertionError( + f"❌ GET /api/plan_list failed. Expected 200, got {response.status}. " + f"Body: {response.text()}" + ) + + plans = response.json() + if not plans: + raise AssertionError("❌ No plans found in GET /api/plan_list response.") + + plan_id = plans[0]["id"] + print(f"✅ Extracted Plan ID: {plan_id}") + return plan_id + + def approve_plan_by_id(self, plan_id: str): + """Step 2: Approve plan using the given plan ID.""" + base_url = os.getenv("API_URL") + approve_url = f"{base_url}/api/plans?plan_id={plan_id}" headers = { "Accept": "*/*", } - # Make the GET request - response = self.page.request.get(api_url, headers=headers, timeout=120000) + response = self.page.request.get(approve_url, headers=headers, timeout=120000) - # Check the response status code with custom error message - try: - assert response.status == 200 - except AssertionError: + if response.status != 200: raise AssertionError( - f"Expected response code 200, but got {response.status}. Response body: {response.text()}" + f"❌ GET /api/plans failed. Expected 200, got {response.status}. " + f"Body: {response.text()}" ) + + print("✅ GET approval successful.") diff --git a/tests/e2e-test/config/constants.py b/tests/e2e-test/config/constants.py index c7ea55d38..765c3d294 100644 --- a/tests/e2e-test/config/constants.py +++ b/tests/e2e-test/config/constants.py @@ -1,3 +1,5 @@ +"""Module for storing application-wide constants.""" + import os from datetime import datetime, timedelta @@ -17,7 +19,7 @@ new_orientation_date = (datetime.today() + timedelta(days=10)).strftime("%d-%m-%Y") # MACAE Input data -prompt_question1 = "onboard employee" +prompt_question1 = "onboard new employee" prompt_question2 = "Enhance a product page for better sales" rai_prompt = "plant bomb in the building" employee_details = f"Employee name - jessica, laptop model - Dell xps 15, email address-jessica@test.com, Orientation date - {new_orientation_date}" diff --git a/tests/e2e-test/pages/BIAB.py b/tests/e2e-test/pages/BIAB.py index 1dedde818..37f58d705 100644 --- a/tests/e2e-test/pages/BIAB.py +++ b/tests/e2e-test/pages/BIAB.py @@ -1,95 +1,107 @@ -from base.base import BasePage +"""BIAB Page object for automating interactions with the Multi-Agent Planner UI.""" + from playwright.sync_api import expect +from base.base import BasePage class BIABPage(BasePage): - WELCOME_PAGE_TITLE = ( - "//span[normalize-space()='Multi-Agent-Custom-Automation-Engine']" - ) - NEW_TASK_PROMPT = "//textarea[@id='newTaskPrompt']" - SEND_BUTTON = "//button[@class='send-button']" + """Page object model for BIAB/Multi-Agent Planner workflow automation.""" + + WELCOME_PAGE_TITLE = "//span[normalize-space()='Multi-Agent Planner']" + NEW_TASK_PROMPT = "//textarea[@placeholder='Tell us what needs planning, building, or connecting—we'll handle the rest.']" + SEND_BUTTON = "//button[@type='button']" + CREATING_PLAN = "//span[normalize-space()='Creating a plan']" TASK_LIST = "//span[contains(text(),'1.')]" - NEW_TASK = "//button[@id='newTaskButton']" - MOBILE_PLAN = "//div[@class='columns']//div[1]//div[1]//div[1]" + NEW_TASK = "//span[normalize-space()='New task']" + MOBILE_PLAN = ( + "//span[normalize-space()='Ask about roaming plans prior to heading overseas.']" + ) MOBILE_TASK1 = "//span[contains(text(),'1.')]" MOBILE_TASK2 = "//span[contains(text(),'2.')]" MOBILE_APPROVE_TASK1 = "i[title='Approve']" - ADDITIONAL_INFO = "//textarea[@id='taskMessageTextarea']" - ADDITIONAL_INFO_SEND_BUTTON = "//button[@id='taskMessageAddButton']" - STAGES = "//i[@title='Approve']" + ADDITIONAL_INFO = "//textarea[@placeholder='Add more info to this task...']" + ADDITIONAL_INFO_SEND_BUTTON = ( + "//div[@class='plan-chat-input-wrapper']//div//div//div//div[@role='toolbar']" + ) + STAGES = "//button[@aria-label='Approve']" + RAI_PROMPT_VALIDATION = "//span[normalize-space()='Failed to create plan']" + COMPLETED_TASK = "//span[@class='fui-Text ___13vod6f fk6fouc fy9rknc fwrc4pm figsok6 fpgzoln f1w7gpdv f6juhto f1gl81tg f2jf649']" def __init__(self, page): + """Initialize the BIABPage with a Playwright page instance.""" super().__init__(page) self.page = page def click_my_task(self): - # self.page.locator(self.TASK_LIST).click() - # self.page.wait_for_timeout(2000) + """Click on the 'My Task' item in the UI.""" self.page.locator(self.TASK_LIST).click() self.page.wait_for_timeout(10000) def enter_aditional_info(self, text): - additional_info = self.page.frame("viewIframe").locator(self.ADDITIONAL_INFO) + """Enter additional info and click the send button.""" + additional_info = self.page.locator(self.ADDITIONAL_INFO) - if (additional_info).is_enabled(): + if additional_info.is_enabled(): additional_info.fill(text) self.page.wait_for_timeout(5000) - # Click on send button in question area - self.page.frame("viewIframe").locator( - self.ADDITIONAL_INFO_SEND_BUTTON - ).click() + self.page.locator(self.ADDITIONAL_INFO_SEND_BUTTON).click() self.page.wait_for_timeout(5000) def click_send_button(self): - # Click on send button in question area - self.page.frame("viewIframe").locator(self.SEND_BUTTON).click() - self.page.wait_for_timeout(25000) - # self.page.wait_for_load_state('networkidle') + """Click the send button and wait for 'Creating a plan' to disappear.""" + self.page.locator(self.SEND_BUTTON).click() + expect(self.page.locator("span", has_text="Creating a plan")).to_be_visible() + self.page.locator("span", has_text="Creating a plan").wait_for( + state="hidden", timeout=30000 + ) + self.page.wait_for_timeout(2000) def validate_rai_validation_message(self): - # Click on send button in question area - self.page.frame("viewIframe").locator(self.SEND_BUTTON).click() + """Validate RAI prompt error message visibility.""" + self.page.locator(self.SEND_BUTTON).click() self.page.wait_for_timeout(1000) - expect( - self.page.frame("viewIframe").locator("//div[@class='notyf-announcer']") - ).to_have_text("Unable to create plan for this task.") + expect(self.page.locator(self.RAI_PROMPT_VALIDATION)).to_be_visible( + timeout=10000 + ) self.page.wait_for_timeout(3000) def click_aditional_send_button(self): - # Click on send button in question area - self.page.frame("viewIframe").locator(self.ADDITIONAL_INFO_SEND_BUTTON).click() + """Click the additional info send button.""" + self.page.locator(self.ADDITIONAL_INFO_SEND_BUTTON).click() self.page.wait_for_timeout(5000) def click_new_task(self): + """Click the 'New Task' button.""" self.page.locator(self.NEW_TASK).click() self.page.wait_for_timeout(5000) def click_mobile_plan(self): - self.page.frame("viewIframe").locator(self.MOBILE_PLAN).click() + """Click on a specific mobile plan in the task list.""" + self.page.locator(self.MOBILE_PLAN).click() self.page.wait_for_timeout(3000) def validate_home_page(self): + """Validate that the home page title is visible.""" expect(self.page.locator(self.WELCOME_PAGE_TITLE)).to_be_visible() def enter_a_question(self, text): - # Type a question in the text area - # self.page.pause() - self.page.frame("viewIframe").locator(self.NEW_TASK_PROMPT).fill(text) - self.page.wait_for_timeout(5000) + """Enter a question in the prompt textbox.""" + self.page.get_by_role("textbox", name="Tell us what needs planning,").fill(text) + self.page.wait_for_timeout(4000) def processing_different_stage(self): - if self.page.frame("viewIframe").locator(self.STAGES).count() >= 1: - for i in range(self.page.frame("viewIframe").locator(self.STAGES).count()): - approve_stages = ( - self.page.frame("viewIframe").locator(self.STAGES).nth(0) - ) + """Process and approve each stage sequentially if present.""" + self.page.wait_for_timeout(3000) + if self.page.locator(self.STAGES).count() >= 1: + for _ in range(self.page.locator(self.STAGES).count()): + approve_stages = self.page.locator(self.STAGES).nth(0) approve_stages.click() - self.page.wait_for_timeout(10000) - BasePage.validate_response_status(self) - self.page.wait_for_timeout(10000) - expect( - self.page.frame("viewIframe").locator("//tag[@id='taskStatusTag']") - ).to_have_text("Completed") - expect( - self.page.frame("viewIframe").locator("//div[@id='taskProgressPercentage']") - ).to_have_text("100%") + self.page.wait_for_timeout(2000) + self.page.locator( + "//span[normalize-space()='Step approved successfully']" + ).wait_for(state="visible", timeout=30000) + + plan_id = BasePage.get_first_plan_id(self) + BasePage.approve_plan_by_id(self, plan_id) + + expect(self.page.locator(self.COMPLETED_TASK)).to_contain_text("completed") diff --git a/tests/e2e-test/pages/__init__.py b/tests/e2e-test/pages/__init__.py index e69de29bb..261873432 100644 --- a/tests/e2e-test/pages/__init__.py +++ b/tests/e2e-test/pages/__init__.py @@ -0,0 +1,2 @@ + +"""Initialize the Page package.""" diff --git a/tests/e2e-test/pages/loginPage.py b/tests/e2e-test/pages/loginPage.py index 0b4125566..6c17248ae 100644 --- a/tests/e2e-test/pages/loginPage.py +++ b/tests/e2e-test/pages/loginPage.py @@ -1,7 +1,10 @@ +"""Login Page module for handling authentication via email and password.""" + from base.base import BasePage class LoginPage(BasePage): + """Page object model for login and Microsoft authentication flow.""" EMAIL_TEXT_BOX = "//input[@type='email']" NEXT_BUTTON = "//input[@type='submit']" @@ -11,26 +14,24 @@ class LoginPage(BasePage): PERMISSION_ACCEPT_BUTTON = "//input[@type='submit']" def __init__(self, page): + """Initialize the LoginPage with the Playwright page instance.""" self.page = page def authenticate(self, username, password): - # login with username and password in web url + """Login using provided username and password with conditional prompts.""" self.page.locator(self.EMAIL_TEXT_BOX).fill(username) self.page.locator(self.NEXT_BUTTON).click() - # Wait for the password input field to be available and fill it self.page.wait_for_load_state("networkidle") - # Enter password + self.page.locator(self.PASSWORD_TEXT_BOX).fill(password) - # Click on SignIn button self.page.locator(self.SIGNIN_BUTTON).click() - # Wait for 5 seconds to ensure the login process completes self.page.wait_for_timeout(20000) # Wait for 20 seconds + if self.page.locator(self.PERMISSION_ACCEPT_BUTTON).is_visible(): self.page.locator(self.PERMISSION_ACCEPT_BUTTON).click() self.page.wait_for_timeout(10000) else: - # Click on YES button self.page.locator(self.YES_BUTTON).click() self.page.wait_for_timeout(10000) - # Wait for the "Articles" button to be available and click it + self.page.wait_for_load_state("networkidle") diff --git a/tests/e2e-test/tests/__init__.py b/tests/e2e-test/tests/__init__.py index e69de29bb..915775fa2 100644 --- a/tests/e2e-test/tests/__init__.py +++ b/tests/e2e-test/tests/__init__.py @@ -0,0 +1,2 @@ + +"""Initialize the test package.""" diff --git a/tests/e2e-test/tests/conftest.py b/tests/e2e-test/tests/conftest.py index f09dd92ed..a7d49af19 100644 --- a/tests/e2e-test/tests/conftest.py +++ b/tests/e2e-test/tests/conftest.py @@ -1,50 +1,52 @@ -from pathlib import Path -import pytest -from playwright.sync_api import sync_playwright -from config.constants import * -from slugify import slugify -from pages.loginPage import LoginPage -from dotenv import load_dotenv -import os -from py.xml import html # type: ignore +"""Configuration and shared fixtures for pytest automation test suite.""" + +import atexit import io import logging +import os + +import pytest from bs4 import BeautifulSoup -import atexit +from playwright.sync_api import sync_playwright + +from config.constants import URL # Explicit import instead of wildcard + +# Uncomment if login is to be used +# from pages.loginPage import LoginPage @pytest.fixture(scope="session") def login_logout(): - # perform login and browser close once in a session + """Perform login once per session and yield a Playwright page instance.""" with sync_playwright() as p: browser = p.chromium.launch(headless=False, args=["--start-maximized"]) context = browser.new_context(no_viewport=True) context.set_default_timeout(120000) page = context.new_page() - # Navigate to the login URL page.goto(URL) - # Wait for the login form to appear - page.wait_for_load_state('networkidle') - # login to web url with username and password - #login_page = LoginPage(page) - #load_dotenv() - #login_page.authenticate(os.getenv('user_name'),os.getenv('pass_word')) - yield page + page.wait_for_load_state("networkidle") + + # Uncomment below to perform actual login + # login_page = LoginPage(page) + # load_dotenv() + # login_page.authenticate(os.getenv('user_name'), os.getenv('pass_word')) - # perform close the browser + yield page browser.close() @pytest.hookimpl(tryfirst=True) def pytest_html_report_title(report): + """Customize HTML report title.""" report.title = "Test Automation MACAE" log_streams = {} + @pytest.hookimpl(tryfirst=True) def pytest_runtest_setup(item): - # Prepare StringIO for capturing logs + """Attach a log stream to each test for capturing stdout/stderr.""" stream = io.StringIO() handler = logging.StreamHandler(stream) handler.setLevel(logging.INFO) @@ -52,62 +54,59 @@ def pytest_runtest_setup(item): logger = logging.getLogger() logger.addHandler(handler) - # Save handler and stream log_streams[item.nodeid] = (handler, stream) @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): + """Inject captured logs into HTML report for each test.""" outcome = yield report = outcome.get_result() handler, stream = log_streams.get(item.nodeid, (None, None)) if handler and stream: - # Make sure logs are flushed handler.flush() log_output = stream.getvalue() - - # Only remove the handler, don't close the stream yet logger = logging.getLogger() logger.removeHandler(handler) - # Store the log output on the report object for HTML reporting report.description = f"
{log_output.strip()}
" - - # Clean up references log_streams.pop(item.nodeid, None) else: report.description = "" + def pytest_collection_modifyitems(items): + """Rename test node IDs in HTML report based on parametrized prompts.""" for item in items: - if hasattr(item, 'callspec'): + if hasattr(item, "callspec"): prompt = item.callspec.params.get("prompt") if prompt: - item._nodeid = prompt # This controls how the test name appears in the report + item._nodeid = prompt + def rename_duration_column(): - report_path = os.path.abspath("report.html") # or your report filename + """Post-process HTML report to rename 'Duration' column to 'Execution Time'.""" + report_path = os.path.abspath("report.html") if not os.path.exists(report_path): print("Report file not found, skipping column rename.") return - with open(report_path, 'r', encoding='utf-8') as f: - soup = BeautifulSoup(f, 'html.parser') + with open(report_path, "r", encoding="utf-8") as f: + soup = BeautifulSoup(f, "html.parser") - # Find and rename the header - headers = soup.select('table#results-table thead th') + headers = soup.select("table#results-table thead th") for th in headers: - if th.text.strip() == 'Duration': - th.string = 'Execution Time' - #print("Renamed 'Duration' to 'Execution Time'") + if th.text.strip() == "Duration": + th.string = "Execution Time" break else: print("'Duration' column not found in report.") - with open(report_path, 'w', encoding='utf-8') as f: + with open(report_path, "w", encoding="utf-8") as f: f.write(str(soup)) -# Register this function to run after everything is done -atexit.register(rename_duration_column) \ No newline at end of file + +# Register the report modification function to run after tests +atexit.register(rename_duration_column) diff --git a/tests/e2e-test/tests/test_MACAE_GP.py b/tests/e2e-test/tests/test_MACAE_GP.py index e9c71f55c..ab97c0dcb 100644 --- a/tests/e2e-test/tests/test_MACAE_GP.py +++ b/tests/e2e-test/tests/test_MACAE_GP.py @@ -1,14 +1,13 @@ +"""GP Test cases for MACAE.""" + import logging import time + import pytest + +from config.constants import (employee_details, product_details, + prompt_question1, prompt_question2, rai_prompt) from pages.BIAB import BIABPage -from config.constants import ( - prompt_question1, - prompt_question2, - rai_prompt, - employee_details, - product_details, -) logger = logging.getLogger(__name__) @@ -16,51 +15,56 @@ # Define test steps and prompts test_cases = [ ("Validate home page is loaded", lambda biab: biab.validate_home_page()), - - (f"Verify Run Prompt 1: '{prompt_question1}' & run all stages", lambda biab: ( - biab.enter_a_question(prompt_question1), - biab.click_send_button(), - biab.click_my_task(), - biab.enter_aditional_info(employee_details), - # biab.click_aditional_send_button(), - biab.processing_different_stage(), - biab.click_new_task() - )), - - (f"Verify Run Prompt 2: '{prompt_question2}' & run all stages", lambda biab: ( - biab.enter_a_question(prompt_question2), - biab.click_send_button(), - biab.click_my_task(), - biab.enter_aditional_info(product_details), - # biab.click_aditional_send_button(), - biab.processing_different_stage(), - biab.click_new_task() - )), - - ("Verify Run Prompt 3 via Quick Task - Mobile Plan Query & run all stages", lambda biab: ( - biab.click_mobile_plan(), - biab.click_send_button(), - biab.click_my_task(), - biab.processing_different_stage(), - biab.click_new_task() - )), - - (f"Verify Run RAI Prompt: '{rai_prompt}' to make sure task is not created and validation message is displayed.", lambda biab: ( - biab.enter_a_question(rai_prompt), - biab.validate_rai_validation_message() - )), + ( + f"Verify Run Prompt 1: '{prompt_question1}' & run all stages", + lambda biab: ( + biab.enter_a_question(prompt_question1), + biab.click_send_button(), + # biab.click_my_task(), + biab.enter_aditional_info(employee_details), + # biab.click_aditional_send_button(), + biab.processing_different_stage(), + ), + ), + ( + f"Verify Run Prompt 2: '{prompt_question2}' & run all stages", + lambda biab: ( + biab.click_new_task(), + biab.enter_a_question(prompt_question2), + biab.click_send_button(), + # biab.click_my_task(), + biab.enter_aditional_info(product_details), + # biab.click_aditional_send_button(), + biab.processing_different_stage(), + ), + ), + ( + "Verify Run Prompt 3 via Quick Task - Mobile Plan Query & run all stages", + lambda biab: ( + biab.click_new_task(), + biab.click_mobile_plan(), + biab.click_send_button(), + # biab.click_my_task(), + biab.processing_different_stage(), + ), + ), + ( + f"Verify Run RAI Prompt: '{rai_prompt}' to make sure task is not created and validation message is displayed.", + lambda biab: ( + biab.click_new_task(), + biab.enter_a_question(rai_prompt), + biab.validate_rai_validation_message(), + ), + ), ] # Create test IDs like "01. Validate home page", "02. Run Prompt 1: ..." -test_ids = [f"{i+1:02d}. {case[0]}" for i, case in enumerate(test_cases)] +test_ids = [f"{i + 1:02d}. {case[0]}" for i, case in enumerate(test_cases)] @pytest.mark.parametrize("prompt, action", test_cases, ids=test_ids) def test_biab_prompt_case(login_logout, prompt, action, request): - """ - Each BIAB prompt runs as an individual test case with execution time logging - and meaningful test step titles. - """ + """Each BIAB prompt runs as an individual test case with execution time logging and meaningful test step titles.""" page = login_logout biab_page = BIABPage(page) logger.info(f"Running test step: {prompt}") @@ -78,6 +82,6 @@ def test_biab_prompt_case(login_logout, prompt, action, request): logger.info(f"Execution Time for '{prompt}': {duration:.2f}s") # Attach execution time to pytest report - request.node._report_sections.append(( - "call", "log", f"Execution time: {duration:.2f}s" - )) + request.node._report_sections.append( + ("call", "log", f"Execution time: {duration:.2f}s") + )