Skip to content

Developer Guide: Frontend Testing with Robot Framework + Selenium

This guide covers the installation, configuration, and usage of Robot Framework with SeleniumLibrary for testing the TQPro frontend applications, with specific focus on the tqweb-adm module.

Overview

Why Robot Framework?

Robot Framework is a keyword-driven test automation framework that provides:

  • Readable syntax - Tests are written in plain English-like keywords
  • No Java coding required - Uses Python-based libraries under the hood
  • Rich ecosystem - SeleniumLibrary, Browser Library, API testing, and more
  • Built-in reporting - Generates HTML reports and logs automatically
  • Easy maintenance - Keyword abstraction makes tests resilient to UI changes

Testing Scope

Frontend tests in TQPro verify:

Layer What We Test Approach
Component Individual UI elements Keyword tests
Integration API calls via tlinq() Selenium + real backend
E2E Complete user workflows Selenium + real backend

Prerequisites

Before setting up Robot Framework testing:

  1. Python 3.8+ - Required for Robot Framework
  2. Chrome/Firefox browser - Target browser for testing
  3. Running TQPro backend - API server on port 11080
  4. Web server - Serving tqweb-adm (nginx or local dev server)
  5. TLINQ_HOME - Environment variable pointing to config/ directory

Installation

Step 1: Create Python Virtual Environment

# Navigate to project root
cd /path/to/tqpro

# Create virtual environment
python -m venv .venv

# Activate virtual environment
# On Linux/macOS:
source .venv/bin/activate
# On Windows:
.venv\Scripts\activate

Step 2: Install Robot Framework and Libraries

Create requirements-test.txt in the project root:

robotframework==7.1.1
robotframework-seleniumlibrary==6.6.1
robotframework-browser==18.9.1
webdrivermanager==0.10.0
selenium==4.25.0

Install dependencies:

pip install -r requirements-test.txt

# Initialize Browser Library (optional, for Playwright-based testing)
rfbrowser init

Step 3: Install WebDriver

SeleniumLibrary requires browser drivers. Use webdrivermanager to install:

# Install ChromeDriver
webdrivermanager chrome --linkpath /usr/local/bin

# Or install GeckoDriver for Firefox
webdrivermanager firefox --linkpath /usr/local/bin

Alternatively, let SeleniumLibrary manage drivers automatically (Selenium 4.6+).

Step 4: Create Test Directory Structure

tqpro/
├── requirements-test.txt
└── tests/
    └── frontend/
        ├── resources/
        │   ├── common.resource
        │   ├── cruise_keywords.resource
        │   └── variables.robot
        ├── page_objects/
        │   ├── base_page.resource
        │   ├── cruise_mgmt_page.resource
        │   └── login_page.resource
        ├── cruise/
        │   ├── company_tests.robot
        │   ├── itinerary_tests.robot
        │   ├── cruise_tests.robot
        │   └── settings_tests.robot
        └── smoke/
            └── smoke_tests.robot

Configuration

Variables File

Create tests/frontend/resources/variables.robot:

*** Variables ***
# URLs
${BASE_URL}                 http://localhost:8080/tqweb-adm
${API_URL}                  http://localhost:11080/tlinq-api

# Browser Configuration
${BROWSER}                  chrome
${HEADLESS}                 ${FALSE}

# Timeouts
${IMPLICIT_WAIT}            10s
${EXPLICIT_WAIT}            30s
${PAGE_LOAD_TIMEOUT}        60s

# Test Data Prefixes
${TEST_PREFIX}              RF_TEST_

# Test User
# For dev-mode testing (auth-mode=oauth2-proxy, dev-mode=true):
${TEST_USER}                testuser
${TEST_ROLES}               admin,agent
# For OIDC testing (auth-mode=native-oidc or hybrid):
# Tests must handle the Keycloak login page redirect.
# See "Testing with OIDC Authentication" section below.

# Cruise Test Data
${TEST_COMPANY_NAME}        ${TEST_PREFIX}Cruise Company
${TEST_COMPANY_CODE}        RFTC
${TEST_ITINERARY_NAME}      ${TEST_PREFIX}Mediterranean Cruise
${TEST_ITINERARY_CODE}      RFMC
${TEST_ITINERARY_DURATION}  7

Common Resource File

Create tests/frontend/resources/common.resource:

*** Settings ***
Library    SeleniumLibrary
Library    Collections
Library    String
Resource   variables.robot

*** Keywords ***
Open Browser To Application
    [Arguments]    ${page}=index.html
    ${options}=    Evaluate    sys.modules['selenium.webdriver'].ChromeOptions()    sys
    Run Keyword If    ${HEADLESS}    Call Method    ${options}    add_argument    --headless=new
    Call Method    ${options}    add_argument    --window-size=1920,1080
    Call Method    ${options}    add_argument    --no-sandbox
    Call Method    ${options}    add_argument    --disable-dev-shm-usage
    Open Browser    ${BASE_URL}/${page}    ${BROWSER}    options=${options}
    Set Selenium Implicit Wait    ${IMPLICIT_WAIT}
    Set Selenium Timeout    ${EXPLICIT_WAIT}
    Maximize Browser Window

Close Application
    Close All Browsers

Wait For Page Load
    Wait Until Element Is Visible    css:body    timeout=${PAGE_LOAD_TIMEOUT}

Wait For Ajax Complete
    Wait For Condition    return jQuery.active == 0    timeout=${EXPLICIT_WAIT}

Wait For Notification
    [Arguments]    ${expected_text}=${EMPTY}
    Wait Until Element Is Visible    css:.notifyjs-wrapper    timeout=10s
    Run Keyword If    '${expected_text}' != '${EMPTY}'
    ...    Element Should Contain    css:.notifyjs-wrapper    ${expected_text}

Wait For Foundation Dialog
    [Arguments]    ${dialog_id}
    Wait Until Element Is Visible    id:${dialog_id}    timeout=${EXPLICIT_WAIT}
    Wait Until Element Has Attribute    id:${dialog_id}    aria-hidden    false

Close Foundation Dialog
    [Arguments]    ${dialog_id}
    Click Element    css:#${dialog_id} [data-close]
    Wait Until Element Is Not Visible    id:${dialog_id}    timeout=${EXPLICIT_WAIT}

Element Should Have Class
    [Arguments]    ${locator}    ${class_name}
    ${classes}=    Get Element Attribute    ${locator}    class
    Should Contain    ${classes}    ${class_name}

Capture Screenshot On Failure
    Run Keyword If Test Failed    Capture Page Screenshot

Page Object Resources

Base Page Resource

Create tests/frontend/page_objects/base_page.resource:

*** Settings ***
Library    SeleniumLibrary
Resource   ../resources/common.resource

*** Keywords ***
Input Text And Clear
    [Arguments]    ${locator}    ${text}
    Clear Element Text    ${locator}
    Input Text    ${locator}    ${text}

Select Dropdown Value
    [Arguments]    ${locator}    ${value}
    Click Element    ${locator}
    Wait Until Element Is Visible    css:${locator} option[value="${value}"]
    Select From List By Value    ${locator}    ${value}

Click And Wait For Ajax
    [Arguments]    ${locator}
    Click Element    ${locator}
    Wait For Ajax Complete

Scroll To Element
    [Arguments]    ${locator}
    Scroll Element Into View    ${locator}
    Sleep    0.3s    # Brief pause for smooth scrolling

Get Table Row Count
    [Arguments]    ${table_locator}
    ${rows}=    Get Element Count    css:${table_locator} tbody tr
    RETURN    ${rows}

Table Should Contain Row With Text
    [Arguments]    ${table_locator}    ${text}
    Element Should Contain    css:${table_locator}    ${text}

Cruise Management Page Resource

Create tests/frontend/page_objects/cruise_mgmt_page.resource:

*** Settings ***
Library    SeleniumLibrary
Resource   base_page.resource
Resource   ../resources/variables.robot

*** Variables ***
# Tree Panel Locators
${COMPANY_TREE}                 id:company_tree
${BTN_ADD_COMPANY}              id:btn_new_company
${BTN_SETTINGS}                 id:btn_settings
${BTN_TOGGLE_INACTIVE}          id:btn_toggle_inactive

# Company Dialog Locators
${DLG_COMPANY}                  id:dlg_company
${INPUT_COMPANY_NAME}           id:company_name
${INPUT_COMPANY_CODE}           id:company_code
${CHECKBOX_COMPANY_ACTIVE}      id:company_active
${BTN_COMPANY_SAVE}             id:company_save

# Itinerary Dialog Locators
${DLG_ITINERARY}                id:dlg_itinerary
${INPUT_ITINERARY_NAME}         id:itinerary_name
${INPUT_ITINERARY_CODE}         id:itinerary_code
${INPUT_ITINERARY_DURATION}     id:itinerary_duration
${SELECT_ITINERARY_SHIP}        id:itinerary_ship
${BTN_ITINERARY_SAVE}           id:itinerary_save

# Cruise Bar Locators
${CRUISE_BAR}                   id:cruise_bar
${BTN_ADD_CRUISE}               id:btn_add_cruise

# Settings Dialog Locators
${DLG_SETTINGS}                 id:dlg_settings
${SETTINGS_TABS}                id:settings_tabs

# Workspace Locators
${WORKSPACE_PANEL}              id:workspace_panel
${ITINERARY_DETAIL}             id:itinerary_detail
${INPUT_DETAIL_ITIN_NAME}       id:detail_itinerary_name

*** Keywords ***
# Navigation
Open Cruise Management Page
    Open Browser To Application    cruisemgmt.html
    Wait For Cruise Page Load

Wait For Cruise Page Load
    Wait Until Element Is Visible    ${COMPANY_TREE}    timeout=${PAGE_LOAD_TIMEOUT}
    Wait For Ajax Complete

# Company Operations
Click Add Company Button
    Click Element    ${BTN_ADD_COMPANY}
    Wait For Foundation Dialog    dlg_company

Fill Company Form
    [Arguments]    ${name}    ${code}    ${active}=${TRUE}
    Input Text And Clear    ${INPUT_COMPANY_NAME}    ${name}
    Input Text And Clear    ${INPUT_COMPANY_CODE}    ${code}
    ${is_checked}=    Run Keyword And Return Status
    ...    Checkbox Should Be Selected    ${CHECKBOX_COMPANY_ACTIVE}
    Run Keyword If    ${active} != ${is_checked}
    ...    Click Element    ${CHECKBOX_COMPANY_ACTIVE}

Save Company
    Click And Wait For Ajax    ${BTN_COMPANY_SAVE}
    Wait Until Element Is Not Visible    ${DLG_COMPANY}    timeout=${EXPLICIT_WAIT}

Create Company
    [Arguments]    ${name}    ${code}    ${active}=${TRUE}
    Click Add Company Button
    Fill Company Form    ${name}    ${code}    ${active}
    Save Company

Select Company In Tree
    [Arguments]    ${company_name}
    ${locator}=    Set Variable    xpath://div[contains(@class,'company-header')]//span[contains(text(),'${company_name}')]
    Wait Until Element Is Visible    ${locator}
    Click Element    ${locator}
    Wait For Ajax Complete

Company Should Be In Tree
    [Arguments]    ${company_name}
    Element Should Be Visible
    ...    xpath://div[contains(@class,'company-header')]//span[contains(text(),'${company_name}')]

Company Should Not Be In Tree
    [Arguments]    ${company_name}
    Element Should Not Be Visible
    ...    xpath://div[contains(@class,'company-header')]//span[contains(text(),'${company_name}')]

Click Edit Company Button
    [Arguments]    ${company_name}
    ${locator}=    Set Variable
    ...    xpath://div[contains(@class,'company-header')][.//span[contains(text(),'${company_name}')]]//button[contains(@class,'btn-edit-company')]
    Click Element    ${locator}
    Wait For Foundation Dialog    dlg_company

Click Delete Company Button
    [Arguments]    ${company_name}
    ${locator}=    Set Variable
    ...    xpath://div[contains(@class,'company-header')][.//span[contains(text(),'${company_name}')]]//button[contains(@class,'btn-delete-company')]
    Click Element    ${locator}
    Handle Alert    action=ACCEPT

# Itinerary Operations
Click Add Itinerary Button
    [Arguments]    ${company_name}
    ${locator}=    Set Variable
    ...    xpath://div[contains(@class,'company-header')][.//span[contains(text(),'${company_name}')]]//button[contains(@class,'btn-add-itinerary')]
    Click Element    ${locator}
    Wait For Foundation Dialog    dlg_itinerary

Fill Itinerary Form
    [Arguments]    ${name}    ${code}    ${duration}
    Input Text And Clear    ${INPUT_ITINERARY_NAME}    ${name}
    Input Text And Clear    ${INPUT_ITINERARY_CODE}    ${code}
    Input Text And Clear    ${INPUT_ITINERARY_DURATION}    ${duration}

Save Itinerary
    Click And Wait For Ajax    ${BTN_ITINERARY_SAVE}
    Wait Until Element Is Not Visible    ${DLG_ITINERARY}    timeout=${EXPLICIT_WAIT}

Create Itinerary
    [Arguments]    ${company_name}    ${itinerary_name}    ${code}    ${duration}
    Click Add Itinerary Button    ${company_name}
    Fill Itinerary Form    ${itinerary_name}    ${code}    ${duration}
    Save Itinerary

Select Itinerary In Tree
    [Arguments]    ${itinerary_name}
    ${locator}=    Set Variable    xpath://div[contains(@class,'itinerary-item')]//span[contains(text(),'${itinerary_name}')]
    Wait Until Element Is Visible    ${locator}
    Click Element    ${locator}
    Wait For Ajax Complete

Itinerary Should Be In Tree
    [Arguments]    ${company_name}    ${itinerary_name}
    Element Should Be Visible
    ...    xpath://li[contains(@class,'company-node')][.//span[contains(text(),'${company_name}')]]//div[contains(@class,'itinerary-item')]//span[contains(text(),'${itinerary_name}')]

Get Itinerary Detail Name
    ${value}=    Get Value    ${INPUT_DETAIL_ITIN_NAME}
    RETURN    ${value}

# Settings Operations
Open Settings Dialog
    Click Element    ${BTN_SETTINGS}
    Wait For Foundation Dialog    dlg_settings

Select Settings Tab
    [Arguments]    ${tab_name}
    ${locator}=    Set Variable    xpath://div[@id='settings_tabs']//a[contains(text(),'${tab_name}')]
    Click Element    ${locator}
    Sleep    0.3s    # Allow tab content to load

Close Settings Dialog
    Close Foundation Dialog    dlg_settings

# Cruise Operations
Click Add Cruise Button
    Click Element    ${BTN_ADD_CRUISE}
    # Wait for cruise dialog

Get Cruise Chip Count
    ${count}=    Get Element Count    css:#cruise_bar .cruise-chip
    RETURN    ${count}

Select Cruise By Date
    [Arguments]    ${date_label}
    ${locator}=    Set Variable    xpath://div[@id='cruise_bar']//div[contains(@class,'cruise-chip')][contains(text(),'${date_label}')]
    Click Element    ${locator}
    Wait For Ajax Complete

# Toggle Operations
Toggle Inactive Items
    Click Element    ${BTN_TOGGLE_INACTIVE}
    Wait For Ajax Complete

# Workspace Verification
Workspace Panel Should Be Visible
    Element Should Be Visible    ${WORKSPACE_PANEL}

Itinerary Detail Should Show
    [Arguments]    ${expected_name}
    ${actual_name}=    Get Itinerary Detail Name
    Should Be Equal    ${actual_name}    ${expected_name}

Test Suites

Smoke Tests

Create tests/frontend/smoke/smoke_tests.robot:

*** Settings ***
Documentation    Smoke tests for Cruise Management frontend
Resource         ../page_objects/cruise_mgmt_page.resource
Suite Setup      Open Cruise Management Page
Suite Teardown   Close Application
Test Teardown    Capture Screenshot On Failure

*** Test Cases ***
Cruise Management Page Loads Successfully
    [Documentation]    Verify the cruise management page loads without errors
    [Tags]    smoke    cruise
    Workspace Panel Should Be Visible
    Element Should Be Visible    ${COMPANY_TREE}
    Element Should Be Visible    ${BTN_ADD_COMPANY}
    Element Should Be Visible    ${BTN_SETTINGS}

Settings Dialog Opens And Contains All Tabs
    [Documentation]    Verify settings dialog opens and shows all configuration tabs
    [Tags]    smoke    cruise    settings
    Open Settings Dialog
    Element Should Be Visible    ${SETTINGS_TABS}
    # Verify all tabs are present
    Element Should Be Visible    xpath://div[@id='settings_tabs']//a[contains(text(),'Areas')]
    Element Should Be Visible    xpath://div[@id='settings_tabs']//a[contains(text(),'Ports')]
    Element Should Be Visible    xpath://div[@id='settings_tabs']//a[contains(text(),'Cabin Types')]
    Element Should Be Visible    xpath://div[@id='settings_tabs']//a[contains(text(),'Charge Types')]
    Element Should Be Visible    xpath://div[@id='settings_tabs']//a[contains(text(),'Ships')]
    Close Settings Dialog

Add Company Dialog Opens
    [Documentation]    Verify add company dialog can be opened
    [Tags]    smoke    cruise    company
    Click Add Company Button
    Element Should Be Visible    ${INPUT_COMPANY_NAME}
    Element Should Be Visible    ${INPUT_COMPANY_CODE}
    Element Should Be Visible    ${CHECKBOX_COMPANY_ACTIVE}
    Element Should Be Visible    ${BTN_COMPANY_SAVE}
    Close Foundation Dialog    dlg_company

Company Tests

Create tests/frontend/cruise/company_tests.robot:

*** Settings ***
Documentation    Tests for Cruise Company CRUD operations
Resource         ../page_objects/cruise_mgmt_page.resource
Suite Setup      Open Cruise Management Page
Suite Teardown   Close Application
Test Setup       Log    Starting test: ${TEST_NAME}
Test Teardown    Run Keywords    Capture Screenshot On Failure    AND    Cleanup Test Company

*** Variables ***
${CURRENT_TEST_COMPANY}    ${EMPTY}

*** Test Cases ***
Create New Company Successfully
    [Documentation]    Verify a new cruise company can be created
    [Tags]    cruise    company    create
    ${company_name}=    Set Variable    ${TEST_PREFIX}New Company
    Set Suite Variable    ${CURRENT_TEST_COMPANY}    ${company_name}
    Create Company    ${company_name}    NWC1    ${TRUE}
    Company Should Be In Tree    ${company_name}
    Wait For Notification    success

Create Company With Minimum Fields
    [Documentation]    Verify company can be created with only required fields
    [Tags]    cruise    company    create
    ${company_name}=    Set Variable    ${TEST_PREFIX}Minimal Co
    Set Suite Variable    ${CURRENT_TEST_COMPANY}    ${company_name}
    Click Add Company Button
    Fill Company Form    ${company_name}    MIN1
    Save Company
    Company Should Be In Tree    ${company_name}

Edit Existing Company
    [Documentation]    Verify an existing company can be edited
    [Tags]    cruise    company    update
    # First create a company
    ${original_name}=    Set Variable    ${TEST_PREFIX}Original
    Create Company    ${original_name}    ORI1

    # Edit the company
    ${updated_name}=    Set Variable    ${TEST_PREFIX}Updated
    Set Suite Variable    ${CURRENT_TEST_COMPANY}    ${updated_name}
    Click Edit Company Button    ${original_name}
    Fill Company Form    ${updated_name}    UPD1
    Save Company

    # Verify changes
    Company Should Not Be In Tree    ${original_name}
    Company Should Be In Tree    ${updated_name}

Delete Company
    [Documentation]    Verify a company can be deleted
    [Tags]    cruise    company    delete
    ${company_name}=    Set Variable    ${TEST_PREFIX}ToDelete
    Create Company    ${company_name}    DEL1
    Company Should Be In Tree    ${company_name}

    Click Delete Company Button    ${company_name}
    Company Should Not Be In Tree    ${company_name}
    Set Suite Variable    ${CURRENT_TEST_COMPANY}    ${EMPTY}

Select Company Highlights In Tree
    [Documentation]    Verify selecting a company highlights it in the tree
    [Tags]    cruise    company    select
    ${company_name}=    Set Variable    ${TEST_PREFIX}Selectable
    Set Suite Variable    ${CURRENT_TEST_COMPANY}    ${company_name}
    Create Company    ${company_name}    SEL1

    Select Company In Tree    ${company_name}
    ${locator}=    Set Variable    xpath://div[contains(@class,'company-header')][.//span[contains(text(),'${company_name}')]]
    Element Should Have Class    ${locator}    selected

*** Keywords ***
Cleanup Test Company
    [Documentation]    Clean up any test company created during the test
    Run Keyword If    '${CURRENT_TEST_COMPANY}' != '${EMPTY}'
    ...    Run Keyword And Ignore Error    Delete Company If Exists    ${CURRENT_TEST_COMPANY}

Delete Company If Exists
    [Arguments]    ${company_name}
    ${exists}=    Run Keyword And Return Status    Company Should Be In Tree    ${company_name}
    Run Keyword If    ${exists}    Click Delete Company Button    ${company_name}

Itinerary Tests

Create tests/frontend/cruise/itinerary_tests.robot:

*** Settings ***
Documentation    Tests for Cruise Itinerary CRUD operations
Resource         ../page_objects/cruise_mgmt_page.resource
Suite Setup      Setup Test Company
Suite Teardown   Run Keywords    Cleanup Test Company    AND    Close Application
Test Teardown    Capture Screenshot On Failure

*** Variables ***
${SUITE_COMPANY}    ${TEST_PREFIX}Itinerary Test Company

*** Test Cases ***
Create New Itinerary Successfully
    [Documentation]    Verify a new itinerary can be created under a company
    [Tags]    cruise    itinerary    create
    ${itinerary_name}=    Set Variable    ${TEST_PREFIX}Caribbean 7-Day
    Create Itinerary    ${SUITE_COMPANY}    ${itinerary_name}    CAR7    7
    Itinerary Should Be In Tree    ${SUITE_COMPANY}    ${itinerary_name}

Create Multiple Itineraries For Same Company
    [Documentation]    Verify multiple itineraries can be created for one company
    [Tags]    cruise    itinerary    create
    Create Itinerary    ${SUITE_COMPANY}    ${TEST_PREFIX}Med Cruise    MED5    5
    Create Itinerary    ${SUITE_COMPANY}    ${TEST_PREFIX}Baltic Tour    BAL10    10
    Itinerary Should Be In Tree    ${SUITE_COMPANY}    ${TEST_PREFIX}Med Cruise
    Itinerary Should Be In Tree    ${SUITE_COMPANY}    ${TEST_PREFIX}Baltic Tour

Select Itinerary Loads Detail Panel
    [Documentation]    Verify selecting an itinerary loads its details in the workspace
    [Tags]    cruise    itinerary    select
    ${itinerary_name}=    Set Variable    ${TEST_PREFIX}Detail Test
    Create Itinerary    ${SUITE_COMPANY}    ${itinerary_name}    DTL1    3

    Select Company In Tree    ${SUITE_COMPANY}
    Select Itinerary In Tree    ${itinerary_name}

    Itinerary Detail Should Show    ${itinerary_name}

*** Keywords ***
Setup Test Company
    Open Cruise Management Page
    Create Company    ${SUITE_COMPANY}    ITCO    ${TRUE}

Cleanup Test Company
    Run Keyword And Ignore Error    Click Delete Company Button    ${SUITE_COMPANY}

Settings Tests

Create tests/frontend/cruise/settings_tests.robot:

*** Settings ***
Documentation    Tests for Cruise Settings (dimensional data management)
Resource         ../page_objects/cruise_mgmt_page.resource
Suite Setup      Open Cruise Management Page
Suite Teardown   Close Application
Test Teardown    Run Keywords    Capture Screenshot On Failure    AND    Close Settings If Open

*** Test Cases ***
Navigate Between Settings Tabs
    [Documentation]    Verify all settings tabs are accessible
    [Tags]    cruise    settings    navigation
    Open Settings Dialog

    Select Settings Tab    Areas
    Wait Until Element Is Visible    id:areas_table

    Select Settings Tab    Ports
    Wait Until Element Is Visible    id:ports_table

    Select Settings Tab    Cabin Types
    Wait Until Element Is Visible    id:cabin_types_table

    Select Settings Tab    Charge Types
    Wait Until Element Is Visible    id:charge_types_table

    Select Settings Tab    Ships
    Wait Until Element Is Visible    id:ships_table

    Close Settings Dialog

Add New Area
    [Documentation]    Verify a new geographic area can be added
    [Tags]    cruise    settings    areas    create
    Open Settings Dialog
    Select Settings Tab    Areas

    Click Element    id:btn_add_area
    Wait Until Element Is Visible    id:area_name
    Input Text    id:area_name    ${TEST_PREFIX}Caribbean
    Input Text    id:area_code    TCAR
    Click Element    id:area_save
    Wait For Ajax Complete

    Table Should Contain Row With Text    id:areas_table    ${TEST_PREFIX}Caribbean
    Close Settings Dialog

Add New Port
    [Documentation]    Verify a new port can be added
    [Tags]    cruise    settings    ports    create
    Open Settings Dialog
    Select Settings Tab    Ports

    Click Element    id:btn_add_port
    Wait Until Element Is Visible    id:port_name
    Input Text    id:port_name    ${TEST_PREFIX}Miami
    Input Text    id:port_code    TMIA
    # Select area if required
    Click Element    id:port_save
    Wait For Ajax Complete

    Table Should Contain Row With Text    id:ports_table    ${TEST_PREFIX}Miami
    Close Settings Dialog

Add New Cabin Type
    [Documentation]    Verify a new cabin type can be added
    [Tags]    cruise    settings    cabins    create
    Open Settings Dialog
    Select Settings Tab    Cabin Types

    Click Element    id:btn_add_cabin_type
    Wait Until Element Is Visible    id:cabin_type_name
    Input Text    id:cabin_type_name    ${TEST_PREFIX}Balcony Suite
    Input Text    id:cabin_type_code    TBAL
    Click Element    id:cabin_type_save
    Wait For Ajax Complete

    Table Should Contain Row With Text    id:cabin_types_table    ${TEST_PREFIX}Balcony Suite
    Close Settings Dialog

*** Keywords ***
Close Settings If Open
    ${is_open}=    Run Keyword And Return Status
    ...    Element Should Be Visible    ${DLG_SETTINGS}
    Run Keyword If    ${is_open}    Close Settings Dialog

Running Tests

Command Line Execution

Run all frontend tests:

# Activate virtual environment first
source .venv/bin/activate  # or .venv\Scripts\activate on Windows

# Run all tests
robot --outputdir results tests/frontend/

# Run with specific browser
robot --variable BROWSER:firefox --outputdir results tests/frontend/

# Run in headless mode
robot --variable HEADLESS:True --outputdir results tests/frontend/

# Run specific test suite
robot --outputdir results tests/frontend/cruise/company_tests.robot

# Run tests by tag
robot --include smoke --outputdir results tests/frontend/
robot --include cruise --exclude slow --outputdir results tests/frontend/

# Run single test case
robot --test "Create New Company Successfully" --outputdir results tests/frontend/

Custom Configuration

Run with custom URLs:

robot \
    --variable BASE_URL:http://staging.example.com/tqweb-adm \
    --variable API_URL:http://staging.example.com/tlinq-api \
    --outputdir results \
    tests/frontend/

Parallel Execution

Install pabot for parallel execution:

pip install robotframework-pabot

# Run tests in parallel (4 processes)
pabot --processes 4 --outputdir results tests/frontend/

# Run test suites in parallel
pabot --processes 4 --testlevelsplit --outputdir results tests/frontend/cruise/

Gradle Integration

Add to build.gradle.kts (root):

tasks.register<Exec>("robotTests") {
    description = "Run Robot Framework frontend tests"
    group = "verification"

    workingDir = projectDir

    // Activate virtual environment and run tests
    commandLine(
        if (System.getProperty("os.name").lowercase().contains("windows"))
            listOf("cmd", "/c", ".venv\\Scripts\\robot")
        else
            listOf(".venv/bin/robot"),
        "--outputdir", "build/robot-results",
        "--variable", "HEADLESS:true",
        "tests/frontend/"
    )
}

tasks.register<Exec>("robotSmoke") {
    description = "Run Robot Framework smoke tests"
    group = "verification"

    workingDir = projectDir

    commandLine(
        if (System.getProperty("os.name").lowercase().contains("windows"))
            listOf("cmd", "/c", ".venv\\Scripts\\robot")
        else
            listOf(".venv/bin/robot"),
        "--include", "smoke",
        "--outputdir", "build/robot-results",
        "tests/frontend/"
    )
}

Run via Gradle:

./gradlew robotTests
./gradlew robotSmoke

Test Environment Setup

Option 1: Local Development Setup

  1. Start the API server:

    export TLINQ_HOME=/path/to/tqpro/config
    java -jar tqapi/build/libs/tqapi.jar
    

  2. Start a local web server for tqweb-adm:

    cd tqweb-adm
    python -m http.server 8080
    # or
    npx serve -l 8080
    

  3. Enable dev-mode in config/tlinqapi.properties:

    dev-mode=true
    dev-user-roles=admin,agent
    

  4. Run tests:

    robot --outputdir results tests/frontend/
    

Option 2: Docker-Based Setup

Create docker-compose.test.yml:

version: '3.8'

services:
  postgres:
    image: postgres:15
    environment:
      POSTGRES_DB: tlinq
      POSTGRES_USER: tlinq
      POSTGRES_PASSWORD: tlinq
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U tlinq"]
      interval: 5s
      timeout: 5s
      retries: 5

  tqapi:
    build:
      context: .
      dockerfile: Dockerfile.tqapi
    environment:
      TLINQ_HOME: /app/config
    ports:
      - "11080:11080"
    depends_on:
      postgres:
        condition: service_healthy

  tqweb:
    image: nginx:alpine
    volumes:
      - ./tqweb-adm:/usr/share/nginx/html:ro
    ports:
      - "8080:80"

  selenium-chrome:
    image: selenium/standalone-chrome:latest
    ports:
      - "4444:4444"
      - "7900:7900"  # noVNC for debugging
    shm_size: 2gb

Run tests against Docker environment:

docker-compose -f docker-compose.test.yml up -d

# Wait for services to be ready
sleep 10

# Run tests with remote WebDriver
robot \
    --variable BASE_URL:http://tqweb:80 \
    --variable SELENIUM_URL:http://localhost:4444/wd/hub \
    --outputdir results \
    tests/frontend/

Option 3: CI/CD Integration (TeamCity)

Add to TeamCity build configuration:

<build-runner type="simpleRunner">
    <parameters>
        <param name="command.executable" value="robot"/>
        <param name="command.parameters" value="
            --variable BASE_URL:%env.TEST_BASE_URL%
            --variable HEADLESS:true
            --include %env.TEST_TAGS%
            --outputdir build/robot-results
            tests/frontend/
        "/>
    </parameters>
</build-runner>

Best Practices

1. Keyword Design

# Good: Descriptive, action-oriented keywords
Create Company With Name And Code
    [Arguments]    ${name}    ${code}
    Click Add Company Button
    Fill Company Form    ${name}    ${code}
    Save Company

# Good: Verification keywords start with assertion words
Company Should Exist In Tree
    [Arguments]    ${company_name}
    Element Should Be Visible    xpath://span[text()='${company_name}']

# Avoid: Vague or implementation-focused names
Do Company Stuff    # Bad
Click Button 3      # Bad

2. Locator Strategy Priority

# Best: ID-based locators
${BTN_SAVE}    id:company_save

# Good: CSS selectors for structure
${SELECTED_COMPANY}    css:.company-header.selected

# Acceptable: XPath for complex relationships
${COMPANY_EDIT_BTN}    xpath://div[@class='company-header'][.//span[text()='${name}']]//button[@class='edit']

# Avoid: Fragile positional XPath
${BAD_LOCATOR}    xpath:/html/body/div[3]/div[2]/button[1]

3. Test Data Management

*** Variables ***
# Use prefixes for test data identification
${TEST_PREFIX}    ROBOT_TEST_

*** Test Cases ***
Create Test Data
    ${unique_name}=    Set Variable    ${TEST_PREFIX}${TEST_NAME}_${SUITE_NAME}
    # Use unique_name for test data

*** Keywords ***
Cleanup All Test Data
    [Documentation]    Remove all data created by Robot Framework tests
    # Find and delete all entities starting with ${TEST_PREFIX}

4. Wait Strategies

*** Keywords ***
# Prefer explicit waits
Wait For Company To Appear
    [Arguments]    ${company_name}
    Wait Until Element Is Visible
    ...    xpath://span[text()='${company_name}']
    ...    timeout=30s

# Wait for AJAX completion
Wait For Data Load
    Wait For Condition    return jQuery.active == 0    timeout=30s

# Wait for Foundation dialog animation
Wait For Dialog
    [Arguments]    ${dialog_id}
    Wait Until Element Is Visible    id:${dialog_id}
    Wait Until Element Has Attribute    id:${dialog_id}    aria-hidden    false
    Sleep    0.3s    # Allow animation to complete

5. Error Handling

*** Keywords ***
Safe Delete Company
    [Arguments]    ${company_name}
    ${exists}=    Run Keyword And Return Status
    ...    Company Should Be In Tree    ${company_name}
    Run Keyword If    ${exists}
    ...    Run Keyword And Ignore Error    Click Delete Company Button    ${company_name}

Reports and Debugging

Generated Reports

Robot Framework automatically generates:

  • output.xml - Machine-readable test results
  • log.html - Detailed execution log with screenshots
  • report.html - High-level test summary

Viewing Reports

# Open report in browser
open results/report.html

# Combine multiple output files
rebot --output combined.xml results/output*.xml

Debug Mode

# Run with debug logging
robot --loglevel DEBUG --outputdir results tests/frontend/

# Run single test interactively (stops on failure)
robot --exitonfailure --outputdir results tests/frontend/smoke/

Screenshots

*** Keywords ***
Capture State On Failure
    Run Keyword If Test Failed    Capture Page Screenshot
    Run Keyword If Test Failed    Log Source

# In test teardown
Test Teardown    Capture State On Failure

Troubleshooting

Common Issues

Issue Solution
ElementNotFound Increase wait time or verify locator
StaleElementReference Re-locate element after page update
ElementClickIntercepted Scroll to element or wait for overlay
WebDriverException Check browser/driver version compatibility
TimeoutException Verify page is loading, increase timeout

WebDriver Issues

# Update WebDriver
webdrivermanager chrome --linkpath /usr/local/bin

# Check Chrome version
google-chrome --version
chromedriver --version

Debugging Tips

  1. Use Pause Execution keyword to stop and inspect
  2. Set --loglevel DEBUG for verbose output
  3. View noVNC at http://localhost:7900 for Docker Selenium
  4. Check log.html for step-by-step execution details

Testing with OIDC Authentication (TQ-51)

Since TQ-51, the admin site uses native OIDC authentication via Keycloak. This affects frontend testing:

Set dev-mode=true and dev-user-roles=admin,agent in tlinqapi.properties. This bypasses OIDC authentication entirely, allowing tests to run without a Keycloak instance.

Option 2: OIDC-Aware Tests

For testing the actual OIDC flow, your Robot Framework tests must handle the Keycloak login redirect:

*** Keywords ***
Login Via Keycloak
    [Arguments]    ${username}    ${password}
    # Page will redirect to Keycloak login
    Wait Until Element Is Visible    id:username    timeout=15s
    Input Text    id:username    ${username}
    Input Text    id:password    ${password}
    Click Button    id:kc-login
    # Wait for redirect back to application
    Wait Until Location Contains    callback.html    timeout=15s
    # Wait for token exchange to complete and redirect to original page
    Wait For Page Load

Key Considerations

  • Token expiry: Long-running test suites may encounter token expiration. The silent renewal iframe (silent-renew.html) handles this automatically in the browser, but tests should verify that API calls continue to work after extended pauses.
  • Logout testing: Logout now redirects to Keycloak's end-session endpoint. Verify that logout.html correctly clears the OIDC session.
  • Multi-page navigation: Each page calls setGlobalHandlers({ requireAuth: true }), which checks for a valid token. Ensure the token persists across page navigation in test scenarios.

Additional Resources