diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..4eddefb --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,58 @@ +name: Robot Framework Tests + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + workflow_dispatch: + +jobs: + ui-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Run UI smoke suite (headless) + run: > + robot + --outputdir results/ui + --variable BROWSER:headlesschrome + --include smoke + tests/car_insurance_quote.robot tests/insurance_navigation.robot + + - name: Upload UI test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: ui-test-results + path: results/ui + + api-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Run API suite + run: robot --outputdir results/api tests/api + + - name: Upload API test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: api-test-results + path: results/api diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e3b354b --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Robot Framework run artifacts +results/ +log.html +report.html +output.xml +*.png +selenium-screenshot-*.png + +# Python +__pycache__/ +*.pyc +.venv/ +venv/ +env/ +*.egg-info/ + +# IDE / OS +.vscode/ +.idea/ +.DS_Store +Thumbs.db diff --git a/README.md b/README.md index 948442d..9470fc9 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,150 @@ # test-robotframework -This is a repository for testing out robotframework + +A Robot Framework test automation portfolio combining UI and API test suites, +built around the Page Object pattern and structured for maintainability and +CI execution. + +- **UI suite** — drives the [Tricentis sample insurance application](https://sampleapp.tricentis.com/) + with `SeleniumLibrary`, exercising the vehicle, insurant and product data steps of + the car insurance quote wizard (see "Known limitations" below for what's out of + scope). +- **API suite** — exercises the [JSONPlaceholder](https://jsonplaceholder.typicode.com) + REST API with `RequestsLibrary`. + +## Project structure + +``` +resources/ +├── common.resource # Browser lifecycle, failure diagnostics & shared assertions +├── variables.resource # Global configuration (URL, browser, timeouts) +├── pages/ # One resource file per wizard step (Page Object Model) +│ ├── home_page.resource +│ ├── vehicle_data_page.resource +│ ├── insurant_data_page.resource +│ └── product_data_page.resource +├── data/ +│ └── car_insurance_test_data.resource # Named vehicle/insurant test profiles +└── api/ + └── jsonplaceholder.resource + +tests/ +├── car_insurance_quote.robot # End-to-end UI regression suite +├── insurance_navigation.robot # UI smoke tests for the landing page +└── api/ + └── posts_api.robot # API regression suite +``` + +Each page resource owns its own locators and keywords, so a markup change on +one wizard step only requires an edit in one file. Test suites orchestrate +page keywords; they never contain raw locators. + +## Prerequisites + +- Python 3.9+ +- Google Chrome (for the UI suite) + +## Setup + +```bash +python -m venv .venv +source .venv/bin/activate # Windows: .venv\Scripts\activate +pip install -r requirements.txt +``` + +Selenium's built-in driver manager resolves the matching ChromeDriver +automatically, so no separate driver installation is required. + +## Running the tests + +Run everything: + +```bash +robot --outputdir results tests +``` + +Run only the UI suites, headless: + +```bash +robot --outputdir results --variable BROWSER:headlesschrome \ + tests/car_insurance_quote.robot tests/insurance_navigation.robot +``` + +Run only the API suite: + +```bash +robot --outputdir results tests/api +``` + +Run by tag (e.g. the fast smoke subset): + +```bash +robot --outputdir results --include smoke tests +``` + +Available tags: `smoke`, `regression`, `navigation`, `api`, `car-insurance`. + +Run results (`log.html`, `report.html`, `output.xml`, screenshots) are +written to `results/` and are not committed to version control. + +## Continuous integration + +`.github/workflows/tests.yml` runs the UI smoke suite (headless Chrome) and +the full API suite on every push and pull request, and publishes the Robot +Framework logs as workflow artifacts. + +## Development tooling + +Optional linting and formatting tools are listed in `requirements-dev.txt`: + +```bash +pip install -r requirements-dev.txt + +# Static analysis (rule thresholds tuned in robocop.toml) +robocop check tests resources + +# Auto-formatting +robocop format tests resources +``` + +## Conventions + +- **Page Object Model** — locators live in `resources/pages/*.resource`, + never inline in test cases. +- **Explicit waits only** — every navigation is followed by + `Wait Until Element Is Visible` (or an equivalent explicit wait); no + `Sleep` calls. +- **Tagging** — every test case carries at least one tag so suites can be + sliced by CI stage (`smoke` for fast feedback, `regression` for full + coverage). +- **Data-driven cases** — `[Template]` is used where the same flow needs to + be exercised with multiple data sets (vehicle profiles, user ids). Reusable + vehicle/insurant profiles live as named dictionaries in + `resources/data/car_insurance_test_data.resource` and are passed into + keywords via Robot's `&{dict}` argument-unpacking syntax, rather than + duplicating literals inline in each test case. +- **Isolation** — `Test Setup`/`Test Teardown` reset state between tests and + capture a screenshot on UI failures for debugging. +- **Meaningful assertions** — each wizard step verifies it was genuinely + accepted (the previous panel is gone, the next one has taken its place, + and no client-side validation errors are showing) rather than only + asserting that no exception was raised. + +## Known limitations + +The UI suite targets a third-party public demo application. If Tricentis +updates the markup of `sampleapp.tricentis.com`, only the affected file(s) +under `resources/pages/` need to be updated — the test cases themselves +should remain unchanged. + +Price selection and quote submission (the steps after product data) are not +covered. The wizard's price-plan step depends on an asynchronous request +that was observed to hang indefinitely under headless automation in CI, +independent of input validity — submitted product data was accepted (no +validation errors, and the product-data panel correctly closed) but the +subsequent price-plan panel never loaded even after 45+ seconds. Extending +coverage to price selection and quote submission is a reasonable follow-up +once that step's reliability under automation can be confirmed. + +## License + +Apache License 2.0, see [LICENSE](LICENSE). diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..032b9c9 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,4 @@ +# Optional tooling used during local development, not required to run the suites. +# Robocop bundles both static analysis (`robocop check`) and formatting +# (`robocop format`) in a single package. +robotframework-robocop>=6.0 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..67f5531 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +# Runtime dependencies for executing the test suites. +robotframework>=7.0,<8.0 +robotframework-seleniumlibrary>=6.5.0,<7.0 +robotframework-requests>=0.9.5,<1.0 diff --git a/resources/api/jsonplaceholder.resource b/resources/api/jsonplaceholder.resource new file mode 100644 index 0000000..d1ef157 --- /dev/null +++ b/resources/api/jsonplaceholder.resource @@ -0,0 +1,39 @@ +*** Settings *** +Documentation Keywords for interacting with the JSONPlaceholder REST API +... (https://jsonplaceholder.typicode.com), used to demonstrate API +... test coverage alongside the UI test suite. + +Library RequestsLibrary + + +*** Variables *** +${API_SESSION} jsonplaceholder +${API_BASE_URL} https://jsonplaceholder.typicode.com + + +*** Keywords *** +Create JSONPlaceholder Session + [Documentation] Opens an HTTP session that every test in the suite can reuse. + Create Session ${API_SESSION} ${API_BASE_URL} + +Get User By Id + [Documentation] Retrieves a single user resource by id. + [Arguments] ${user_id} ${expected_status}=200 + ${response}= GET On Session ${API_SESSION} /users/${user_id} + ... expected_status=${expected_status} + RETURN ${response} + +Create Post + [Documentation] Creates a new post resource for the given user. + [Arguments] ${title} ${body} ${user_id} + VAR &{payload}= title=${title} body=${body} userId=${user_id} + ${response}= POST On Session ${API_SESSION} /posts json=${payload} + ... expected_status=201 + RETURN ${response} + +Delete Post + [Documentation] Deletes the post with the given id. + [Arguments] ${post_id} + ${response}= DELETE On Session ${API_SESSION} /posts/${post_id} + ... expected_status=200 + RETURN ${response} diff --git a/resources/common.resource b/resources/common.resource new file mode 100644 index 0000000..a85d332 --- /dev/null +++ b/resources/common.resource @@ -0,0 +1,34 @@ +*** Settings *** +Documentation Keywords shared by every UI test suite: browser lifecycle and +... failure diagnostics. + +Library SeleniumLibrary +Resource variables.resource + + +*** Keywords *** +Open Insurance Application + [Documentation] Starts a browser session and opens the insurance sample application. + Open Browser ${APP_URL} ${BROWSER} + Set Selenium Timeout ${SELENIUM_TIMEOUT} + Set Window Size 1920 1080 + +Close Insurance Application + [Documentation] Closes every browser window opened during the suite. + Close All Browsers + +Capture Screenshot On Failure + [Documentation] Saves a screenshot of the current page whenever a test fails, + ... to speed up debugging. + Run Keyword If Test Failed Capture Page Screenshot + +No Validation Errors Should Be Visible + [Documentation] Verifies the current step shows no active field validation + ... errors, confirming submitted data was genuinely accepted rather + ... than silently rejected. + ${errors}= Execute Javascript + ... var candidates = document.querySelectorAll('.error, .errorMessage, .invalid, [class*="error"]'); + ... function isVisibleError(e) { return e.offsetParent !== null && e.textContent.trim().length > 1; } + ... var visible = Array.from(candidates).filter(isVisibleError); + ... return visible.map(function(e) { return e.textContent.trim(); }).join(' || '); + Should Be Empty ${errors} msg=Unexpected validation errors: ${errors} diff --git a/resources/data/car_insurance_test_data.resource b/resources/data/car_insurance_test_data.resource new file mode 100644 index 0000000..276f1b2 --- /dev/null +++ b/resources/data/car_insurance_test_data.resource @@ -0,0 +1,44 @@ +*** Settings *** +Documentation Reusable vehicle and insurant profiles for the car insurance quote +... wizard tests. Values satisfy the live application's validation rules +... confirmed during development (e.g. list price/annual mileage ranges, +... minimum street length, insurant age between 18 and 70). + + +*** Variables *** +&{VEHICLE_MERCEDES_BENZ} make=Mercedes Benz +... engine_performance=68 +... manufacture_date=08/25/2023 +... seats=7 +... fuel=Diesel +... list_price=50000 +... annual_mileage=32185 +&{VEHICLE_VOLKSWAGEN} make=Volkswagen +... engine_performance=75 +... manufacture_date=01/15/2022 +... seats=5 +... fuel=Petrol +... list_price=28000 +... annual_mileage=15000 +&{VEHICLE_BMW} make=BMW +... engine_performance=140 +... manufacture_date=03/10/2021 +... seats=5 +... fuel=Diesel +... list_price=45000 +... annual_mileage=20000 + +&{INSURANT_JANE_DOE} first_name=Jane +... last_name=Doe +... birthdate=05/14/1990 +... gender=Female +... street=Baker Street 221B +... zip_code=10115 +... city=Berlin +&{INSURANT_JOHN_SMITH} first_name=John +... last_name=Smith +... birthdate=01/01/1985 +... gender=Male +... street=Main Street 1 +... zip_code=10115 +... city=Berlin diff --git a/resources/pages/home_page.resource b/resources/pages/home_page.resource new file mode 100644 index 0000000..4e1660d --- /dev/null +++ b/resources/pages/home_page.resource @@ -0,0 +1,40 @@ +*** Settings *** +Documentation Locators and keywords for the insurance type selection landing page. + +Library SeleniumLibrary +Resource ../variables.resource + + +*** Variables *** +${AUTOMOBILE_TAB} css:#nav_automobile +${TRUCK_TAB} css:#nav_truck +${MOTORCYCLE_TAB} css:#nav_motorcycle +${CAMPER_TAB} css:#nav_camper +${MAKE_FIELD} css:#make + + +*** Keywords *** +Go To Homepage + [Documentation] Navigates (back) to the insurance type selection page. + Go To ${APP_URL} + Wait Until Element Is Visible ${AUTOMOBILE_TAB} + +Select Automobile Insurance + [Documentation] Opens the automobile insurance quote wizard. + Click Element ${AUTOMOBILE_TAB} + Wait Until Element Is Visible ${MAKE_FIELD} + +Select Truck Insurance + [Documentation] Opens the truck insurance quote wizard. + Click Element ${TRUCK_TAB} + Wait Until Element Is Visible ${MAKE_FIELD} + +Select Motorcycle Insurance + [Documentation] Opens the motorcycle insurance quote wizard. + Click Element ${MOTORCYCLE_TAB} + Wait Until Element Is Visible ${MAKE_FIELD} + +Select Camper Insurance + [Documentation] Opens the camper insurance quote wizard. + Click Element ${CAMPER_TAB} + Wait Until Element Is Visible ${MAKE_FIELD} diff --git a/resources/pages/insurant_data_page.resource b/resources/pages/insurant_data_page.resource new file mode 100644 index 0000000..8141b00 --- /dev/null +++ b/resources/pages/insurant_data_page.resource @@ -0,0 +1,85 @@ +*** Settings *** +Documentation Locators and keywords for the "Enter Insurant Data" step of the +... insurance quote wizard. +... +... The country and occupation dropdowns use ideal-select styling with +... application-specific option values, so the keywords below select the +... first real (non-placeholder) option rather than a hard-coded label. +... Every interaction retries briefly, since fields can still be settling +... into place right after the previous step's panel closes. + +Library SeleniumLibrary +Resource ../common.resource + + +*** Variables *** +${FIRST_NAME_FIELD} css:#firstname +${LAST_NAME_FIELD} css:#lastname +${BIRTHDATE_FIELD} css:#birthdate +${GENDER_MALE_RADIO} css:#gendermale + span.ideal-radio +${GENDER_FEMALE_RADIO} css:#genderfemale + span.ideal-radio +${STREET_FIELD} css:#streetaddress +${COUNTRY_FIELD} css:#country +${ZIP_CODE_FIELD} css:#zipcode +${CITY_FIELD} css:#city +${OCCUPATION_FIELD} css:#occupation +${PRODUCT_DATA_FIRST_FIELD} css:#startdate + + +*** Keywords *** +Enter Insurant Data + [Documentation] Fills in the insurant's personal details and proceeds to the + ... product data step. + [Arguments] ${first_name} + ... ${last_name} + ... ${birthdate} + ... ${gender} + ... ${street} + ... ${zip_code} + ... ${city} + Wait Until Keyword Succeeds 10x 300ms Input Text ${FIRST_NAME_FIELD} ${first_name} + Wait Until Keyword Succeeds 10x 300ms Input Text ${LAST_NAME_FIELD} ${last_name} + Wait Until Keyword Succeeds 10x 300ms Input Text ${BIRTHDATE_FIELD} ${birthdate} + Select Gender ${gender} + Wait Until Keyword Succeeds 10x 300ms Input Text ${STREET_FIELD} ${street} + Select First Available Option ${COUNTRY_FIELD} country + Wait Until Keyword Succeeds 10x 300ms Input Text ${ZIP_CODE_FIELD} ${zip_code} + Wait Until Keyword Succeeds 10x 300ms Input Text ${CITY_FIELD} ${city} + Select First Available Option ${OCCUPATION_FIELD} occupation + No Validation Errors Should Be Visible + Click Next Button + Wait Until Element Is Visible ${PRODUCT_DATA_FIRST_FIELD} + +Insurant Data Should Be Accepted + [Documentation] Verifies the wizard genuinely moved on to the product data step: + ... the insurant data panel is gone and the product data panel has + ... taken its place, rather than both simply coexisting. + Element Should Not Be Visible ${FIRST_NAME_FIELD} + Element Should Be Visible ${PRODUCT_DATA_FIRST_FIELD} + +Click Next Button + [Documentation] Submits the step. A plain WebDriver click on this button was + ... observed to intermittently and repeatedly fail with + ... ElementNotInteractableException in CI even after several retries, + ... so it's dispatched via JavaScript instead, which the button's own + ... click handler responds to just as well. + Execute Javascript document.getElementById('nextenterproductdata').click(); + +Select Gender + [Documentation] Selects the insurant's gender. Valid values: Male, Female. + [Arguments] ${gender} + IF '${gender}' == 'Male' + Wait Until Keyword Succeeds 10x 300ms Click Element ${GENDER_MALE_RADIO} + ELSE + Wait Until Keyword Succeeds 10x 300ms Click Element ${GENDER_FEMALE_RADIO} + END + +Select First Available Option + [Documentation] Selects the first real (non-placeholder) option of a select field. + ... ``locator`` is the SeleniumLibrary locator used to select the value; + ... ``element_id`` is the field's raw HTML id, used to read the option + ... list via JavaScript. + [Arguments] ${locator} ${element_id} + ${value}= Execute Javascript + ... return document.getElementById('${element_id}').options[1].value; + Wait Until Keyword Succeeds 10x 300ms Select From List By Value ${locator} ${value} diff --git a/resources/pages/product_data_page.resource b/resources/pages/product_data_page.resource new file mode 100644 index 0000000..304888b --- /dev/null +++ b/resources/pages/product_data_page.resource @@ -0,0 +1,63 @@ +*** Settings *** +Documentation Locators and keywords for the "Enter Product Data" step of the +... insurance quote wizard. +... +... The coverage dropdowns (insurance sum, merit rating, damage +... insurance, courtesy car) use application-specific option values, so +... the keyword below selects the first real (non-placeholder) option +... for each rather than a hard-coded label. Every interaction retries +... briefly, since fields can still be settling into place right after +... the previous step's panel closes. + +Library DateTime +Library SeleniumLibrary +Resource ../common.resource + + +*** Variables *** +${START_DATE_FIELD} css:#startdate +${INSURANCE_SUM_FIELD} css:#insurancesum +${MERITRATING_FIELD} css:#meritrating +${DAMAGE_INSURANCE_FIELD} css:#damageinsurance +${COURTESY_CAR_FIELD} css:#courtesycar +${PRICE_LOADER} css:#xLoaderPrice + + +*** Keywords *** +Enter Product Data + [Documentation] Fills in the requested product options with a valid, future + ... policy start date, and submits the step. + ${start_date}= Get Current Date increment=60 days result_format=%m/%d/%Y + Wait Until Keyword Succeeds 10x 300ms Input Text ${START_DATE_FIELD} ${start_date} + Select First Available Option ${INSURANCE_SUM_FIELD} insurancesum + Select First Available Option ${MERITRATING_FIELD} meritrating + Select First Available Option ${DAMAGE_INSURANCE_FIELD} damageinsurance + Select First Available Option ${COURTESY_CAR_FIELD} courtesycar + No Validation Errors Should Be Visible + Click Next Button + +Click Next Button + [Documentation] Submits the step. A plain WebDriver click on the equivalent + ... button in an earlier step was observed to intermittently and + ... repeatedly fail with ElementNotInteractableException in CI even + ... after several retries, so it's dispatched via JavaScript instead, + ... which the button's own click handler responds to just as well. + Execute Javascript document.getElementById('nextselectpriceoption').click(); + +Select First Available Option + [Documentation] Selects the first real (non-placeholder) option of a select field. + ... ``locator`` is the SeleniumLibrary locator used to select the value; + ... ``element_id`` is the field's raw HTML id, used to read the option + ... list via JavaScript. + [Arguments] ${locator} ${element_id} + ${value}= Execute Javascript + ... return document.getElementById('${element_id}').options[1].value; + Wait Until Keyword Succeeds 10x 300ms Select From List By Value ${locator} ${value} + +Product Data Should Be Accepted + [Documentation] Verifies the wizard genuinely moved on: the product data panel + ... is gone, and the price step's loading indicator has appeared, + ... confirming the submission was accepted rather than merely that + ... the previous panel disappeared for some unrelated reason. + Wait Until Element Is Not Visible ${START_DATE_FIELD} + Wait Until Element Is Visible ${PRICE_LOADER} diff --git a/resources/pages/vehicle_data_page.resource b/resources/pages/vehicle_data_page.resource new file mode 100644 index 0000000..7598e34 --- /dev/null +++ b/resources/pages/vehicle_data_page.resource @@ -0,0 +1,60 @@ +*** Settings *** +Documentation Locators and keywords for the "Enter Vehicle Data" step of the +... insurance quote wizard. +... +... Every interaction retries briefly, since fields can still be +... settling into place right after the insurance type tab is selected. + +Library SeleniumLibrary +Resource ../common.resource + + +*** Variables *** +${MAKE_FIELD} css:#make +${ENGINE_PERFORMANCE_FIELD} css:#engineperformance +${MANUFACTURE_DATE_FIELD} css:#dateofmanufacture +${SEATS_FIELD} css:#numberofseats +${FUEL_FIELD} css:#fuel +${LIST_PRICE_FIELD} css:#listprice +${ANNUAL_MILEAGE_FIELD} css:#annualmileage +${INSURANT_DATA_FIRST_FIELD} css:#firstname + + +*** Keywords *** +Enter Vehicle Data + [Documentation] Fills in the vehicle details and proceeds to the insurant data step. + [Arguments] ${make} + ... ${engine_performance} + ... ${manufacture_date} + ... ${seats} + ... ${fuel} + ... ${list_price} + ... ${annual_mileage} + Wait Until Keyword Succeeds 10x 300ms Select From List By Value ${MAKE_FIELD} ${make} + Wait Until Keyword Succeeds 10x 300ms + ... Input Text ${ENGINE_PERFORMANCE_FIELD} ${engine_performance} + Wait Until Keyword Succeeds 10x 300ms + ... Input Text ${MANUFACTURE_DATE_FIELD} ${manufacture_date} + Wait Until Keyword Succeeds 10x 300ms Select From List By Value ${SEATS_FIELD} ${seats} + Wait Until Keyword Succeeds 10x 300ms Select From List By Value ${FUEL_FIELD} ${fuel} + Wait Until Keyword Succeeds 10x 300ms Input Text ${LIST_PRICE_FIELD} ${list_price} + Wait Until Keyword Succeeds 10x 300ms + ... Input Text ${ANNUAL_MILEAGE_FIELD} ${annual_mileage} + No Validation Errors Should Be Visible + Click Next Button + Wait Until Element Is Visible ${INSURANT_DATA_FIRST_FIELD} + +Vehicle Data Should Be Accepted + [Documentation] Verifies the wizard genuinely moved on to the insurant data step: + ... the vehicle data panel is gone and the insurant data panel has + ... taken its place, rather than both simply coexisting. + Element Should Not Be Visible ${MAKE_FIELD} + Element Should Be Visible ${INSURANT_DATA_FIRST_FIELD} + +Click Next Button + [Documentation] Submits the step. A plain WebDriver click on the equivalent + ... button in a later step was observed to intermittently and + ... repeatedly fail with ElementNotInteractableException in CI even + ... after several retries, so it's dispatched via JavaScript instead, + ... which the button's own click handler responds to just as well. + Execute Javascript document.getElementById('nextenterinsurantdata').click(); diff --git a/resources/variables.resource b/resources/variables.resource new file mode 100644 index 0000000..373fbee --- /dev/null +++ b/resources/variables.resource @@ -0,0 +1,8 @@ +*** Settings *** +Documentation Global configuration shared by every UI test suite. + + +*** Variables *** +${APP_URL} https://sampleapp.tricentis.com/ +${BROWSER} chrome +${SELENIUM_TIMEOUT} 10s diff --git a/results/log.html b/results/log.html deleted file mode 100644 index 12fab3b..0000000 --- a/results/log.html +++ /dev/null @@ -1,2454 +0,0 @@ - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -