Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,29 @@ jobs:
with:
name: api-test-results
path: results/api

playground-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 Testing Playground suite (headless)
run: >
robot
--outputdir results/playground
--variable BROWSER:headlesschrome
tests/_diagnostic.robot

- name: Upload playground test results
if: always()
uses: actions/upload-artifact@v4
with:
name: playground-test-results
path: results/playground
43 changes: 35 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ CI execution.
scope).
- **API suite** — exercises the [JSONPlaceholder](https://jsonplaceholder.typicode.com)
REST API with `RequestsLibrary`.
- **UI Testing Playground suite** — covers every challenge page listed on
[uitestingplayground.com](http://www.uitestingplayground.com/)'s homepage
(Inflectra's UI Test Automation Playground: dynamic attributes, timing,
visibility, Shadow DOM, alerts, frames, and more), one test case per page.

## Project structure

Expand All @@ -21,17 +25,21 @@ resources/
│ ├── home_page.resource
│ ├── vehicle_data_page.resource
│ ├── insurant_data_page.resource
│ └── product_data_page.resource
│ ├── product_data_page.resource
│ └── playground_page.resource # Locators & keywords for every Playground page
├── data/
│ └── car_insurance_test_data.resource # Named vehicle/insurant test profiles
│ ├── car_insurance_test_data.resource # Named vehicle/insurant test profiles
│ └── sample_upload.txt # Fixture for the File Upload challenge
└── 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
├── api/
│ └── posts_api.robot # API regression suite
└── playground/
└── ui_testing_playground.robot # One test case per playground challenge page
```

Each page resource owns its own locators and keywords, so a markup change on
Expand Down Expand Up @@ -75,22 +83,30 @@ Run only the API suite:
robot --outputdir results tests/api
```

Run only the UI Testing Playground suite, headless:

```bash
robot --outputdir results --variable BROWSER:headlesschrome tests/playground
```

Run by tag (e.g. the fast smoke subset):

```bash
robot --outputdir results --include smoke tests
```

Available tags: `smoke`, `regression`, `navigation`, `api`, `car-insurance`.
Available tags: `smoke`, `regression`, `navigation`, `api`, `car-insurance`,
`playground`.

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.
`.github/workflows/tests.yml` runs three independent jobs on every push and
pull request — the UI smoke suite, the API suite, and the UI Testing
Playground suite (all headless Chrome where applicable) — and publishes
each job's Robot Framework logs as workflow artifacts.

## Development tooling

Expand Down Expand Up @@ -145,6 +161,17 @@ 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.

The UI Testing Playground suite also targets a third-party public site,
under `resources/pages/playground_page.resource`; the same
markup-drift caveat applies there. Its locators and assertions were derived
by driving the live site from CI and inspecting its real DOM and behavior,
rather than assumed from the site's name — several pages turned out to have
markup or interaction mechanics quite different from what a generic "UI
testing playground" might suggest (e.g. Sample App's fields use dynamically
generated ids with no stable attribute but their `type`, and the Visibility
page's seven hiding techniques only take effect after a button click, not
on page load).

## License

Apache License 2.0, see [LICENSE](LICENSE).
1 change: 1 addition & 0 deletions resources/data/sample_upload.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Sample file used by the File Upload challenge test on uitestingplayground.com.
253 changes: 253 additions & 0 deletions resources/pages/playground_page.resource
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
*** Settings ***
Documentation Locators and keywords for uitestingplayground.com (Inflectra's "UI Test
... Automation Playground"), a collection of small, independent pages that
... each demonstrate one UI automation challenge (dynamic attributes,
... timing, visibility, Shadow DOM, and so on). Unlike the car insurance
... wizard, these pages aren't part of a shared flow and each locator/keyword
... is used by exactly one test, so they're kept in a single file rather
... than one per page.

Library SeleniumLibrary


*** Variables ***
${PLAYGROUND_BASE_URL} http://www.uitestingplayground.com
${SAMPLE_UPLOAD_FILE}
... ${{os.path.normpath(r'${CURDIR}' + '/../data/sample_upload.txt')}}

# Dynamic ID
${DYNAMIC_ID_BUTTON} css:.btn.btn-primary

# Class Attribute
${CLASS_ATTRIBUTE_TARGET_BUTTON} css:.btn-primary

# Hidden Layers
${HIDING_BUTTON} css:#greenButton

# Load Delay
${LOAD_DELAY_BUTTON} css:.btn.btn-primary

# AJAX Data
${AJAX_BUTTON} css:#ajaxButton
${AJAX_RESULT} css:#content

# Client Side Delay
${CLIENT_DELAY_BUTTON} css:#ajaxButton
${CLIENT_DELAY_RESULT} css:#content

# Click
${CLICK_TARGET_BUTTON} css:#badButton

# Text Input
${TEXT_INPUT_FIELD} css:#newButtonName
${TEXT_INPUT_UPDATE_BUTTON} css:#updatingButton

# Scrollbars
${SCROLLBAR_BUTTON} css:#hidingButton

# Dynamic Table
${DYNAMIC_TABLE_ANSWER} css:.bg-warning

# Verify Text
${VERIFY_TEXT_PLAIN_PARAGRAPH} xpath://p[contains(.,'Hello') and not(contains(@class,'bg-warning'))]
${VERIFY_TEXT_HIGHLIGHTED_PARAGRAPH} css:.bg-warning

# Progress Bar
${PROGRESS_BAR_START_BUTTON} css:#startButton
${PROGRESS_BAR_STOP_BUTTON} css:#stopButton
${PROGRESS_BAR} css:#progressBar

# Visibility
${VISIBILITY_HIDE_BUTTON} css:#hideButton
${VISIBILITY_REMOVED_BUTTON} css:#removedButton
${VISIBILITY_ZERO_WIDTH_BUTTON} css:#zeroWidthButton
${VISIBILITY_OVERLAPPED_BUTTON} css:#overlappedButton
${VISIBILITY_TRANSPARENT_BUTTON} css:#transparentButton
${VISIBILITY_INVISIBLE_BUTTON} css:#invisibleButton
${VISIBILITY_NOT_DISPLAYED_BUTTON} css:#notdisplayedButton
${VISIBILITY_OFFSCREEN_BUTTON} css:#offscreenButton

# Sample App
${SAMPLE_APP_USERNAME_FIELD} css:input[type="text"]
${SAMPLE_APP_PASSWORD_FIELD} css:input[type="password"]
${SAMPLE_APP_LOGIN_BUTTON} css:#login
${SAMPLE_APP_STATUS_MESSAGE} css:#loginstatus
${SAMPLE_APP_VALID_PASSWORD} pwd

# Mouse Over
${MOUSE_OVER_LINK} xpath://a[@class="text-primary" and text()="Click me"]
${MOUSE_OVER_CLICK_COUNT} css:#clickCount
${MOUSE_OVER_BUTTON_LINK} xpath://a[@class="text-primary" and text()="Link Button"]
${MOUSE_OVER_BUTTON_CLICK_COUNT} css:#clickButtonCount

# Non-Breaking Space
${NBSP_BUTTON} css:.btn-primary
${NBSP_BUTTON_EXACT_TEXT} xpath://button[text()="My Button"]

# Overlapped Element
${OVERLAPPED_NAME_FIELD} css:#name

# Shadow DOM
${SHADOW_HOST} guid-generator
${SHADOW_GENERATE_BUTTON_ID} buttonGenerate
${SHADOW_COPY_BUTTON_ID} buttonCopy
${SHADOW_EDIT_FIELD_ID} editField

# Alerts
${ALERT_BUTTON} css:#alertButton
${CONFIRM_BUTTON} css:#confirmButton
${PROMPT_BUTTON} css:#promptButton

# File Upload
${FILE_UPLOAD_IFRAME} css:iframe
${FILE_UPLOAD_INPUT} css:#browse

# Animated Button
${ANIMATION_START_BUTTON} css:#animationButton
${ANIMATION_MOVING_TARGET} css:#movingTarget
${ANIMATION_STATUS} css:#opstatus

# Disabled Input
${DISABLED_INPUT_FIELD} css:#inputField
${DISABLED_INPUT_ENABLE_BUTTON} css:#enableButton

# Auto Wait
${AUTO_WAIT_APPLY_3S_BUTTON} css:#applyButton3
${AUTO_WAIT_TARGET_BUTTON} css:#target
${AUTO_WAIT_STATUS} css:#opstatus

# Frames
${FRAMES_OUTER_FRAME} css:iframe

# Geolocation
${GEOLOCATION_REQUEST_BUTTON} css:#requestLocation
${GEOLOCATION_RESULT} css:#location

# Clear Input
@{CLEAR_INPUT_FIELD_LOCATORS}
... css:#clearInput
... css:#clearTextarea
... css:#clearPassword
... css:#clearEmail
... css:#clearNumber
... css:#clearSearch
... css:#clearUrl
... css:#clearTel
${CLEAR_INPUT_EDITABLE_DIV} css:#clearContentEditable
${CLEAR_INPUT_STATUS} css:#opstatus

# Scroll To Click
${SCROLL_TARGET_1} css:#scrollTarget1
${SCROLL_TARGET_2} css:#scrollTarget2
${SCROLL_TARGET_3} css:#scrollTarget3
${SCROLL_TARGET_4} css:#scrollTarget4
${SCROLL_HOVER_ROW} css:#targetRow4
${SCROLL_PROGRESS_TEXT} css:#progressText

# CSS Selectors
${CSS_SELECTORS_PRIMARY_BUTTON} css:#primary-btn
${CSS_SELECTORS_VISIBLE_BUTTON} css:#visible-btn
${CSS_SELECTORS_HIGHLIGHTED_BUTTON} css:.css-btn.highlight
${CSS_SELECTORS_HIDDEN_DISPLAY} css:#hidden-display
${CSS_SELECTORS_HIDDEN_VISIBILITY} css:#hidden-visibility
${CSS_SELECTORS_HIDDEN_OVERFLOW} css:#hidden-overflow
${CSS_SELECTORS_HIDDEN_OPACITY} css:#hidden-opacity
${CSS_SELECTORS_HIDDEN_OFFSCREEN} css:#hidden-offscreen

# Select
${SELECT_LANGUAGE} css:#selectLanguage
${SELECT_CITY} css:#selectCity
${SELECT_PRODUCT} css:#selectProduct
${SELECT_FRUITS} css:#selectFruits
${SELECT_STATUS_LANGUAGE} css:#statusLanguage
${SELECT_STATUS_CITY} css:#statusCity
${SELECT_STATUS_PRODUCT} css:#statusProduct
${SELECT_STATUS_FRUITS} css:#statusFruits


*** Keywords ***
Go To Playground Page
[Documentation] Navigates to the given uitestingplayground.com challenge page.
[Arguments] ${path}
Go To ${PLAYGROUND_BASE_URL}/${path}

Get Shadow Element Text
[Documentation] Reads the text content of an element nested inside a Shadow DOM
... tree, which plain SeleniumLibrary locators cannot pierce. Works
... for both form controls (via .value) and plain text elements.
[Arguments] ${host_tag} ${element_id}
${text}= Execute Javascript
... var el = document.querySelector('${host_tag}').shadowRoot.getElementById('${element_id}');
... return ('value' in el) ? el.value : el.textContent.trim();
RETURN ${text}

Click Shadow Element
[Documentation] Clicks an element nested inside a Shadow DOM tree by dispatching
... a native click through JavaScript.
[Arguments] ${host_tag} ${element_id}
Execute Javascript
... document.querySelector('${host_tag}').shadowRoot.getElementById('${element_id}').click();

Wait Until Animation Stops
[Documentation] Waits until the given element's horizontal position stops
... changing between polls, confirming a CSS animation has finished
... before interacting with it.
[Arguments] ${locator} ${attempts}=50
${previous}= Get Horizontal Position ${locator}
FOR ${_attempt} IN RANGE ${attempts}
Sleep 200ms
${current}= Get Horizontal Position ${locator}
IF ${current} == ${previous} RETURN
VAR ${previous}= ${current}
END
Fail Element '${locator}' never stopped moving.

Set Value Via Javascript
[Documentation] Sets an input's value directly through JavaScript and dispatches an
... input event so the page's own listeners see the change. Used for the
... Overlapped Element challenge, where the field is covered by another
... element and native Selenium keystrokes never register.
[Arguments] ${locator} ${value}
${element}= Get WebElement ${locator}
Execute Javascript
... arguments[0].value = arguments[1];
... arguments[0].dispatchEvent(new Event('input', {bubbles: true}));
... ARGUMENTS
... ${element}
... ${value}

Get Row Cell Values
[Documentation] Reads the ARIA role="row" elements of the Dynamic Table challenge
... (which uses divs, not a real <table>) and returns each row's cell
... text values as a list of lists, first row first.
${rows}= Execute Javascript
... function cellText(cell) { return cell.textContent.trim(); }
... function rowCells(row) { return Array.from(row.querySelectorAll('[role="cell"]')).map(cellText); }
... return Array.from(document.querySelectorAll('[role="row"]'))
... .filter(function(row) { return row.querySelector('[role="cell"]'); })
... .map(rowCells);
RETURN ${rows}

Get Cpu Column Index
[Documentation] Returns the position of the "CPU" column among the Dynamic Table
... challenge's headers. Column order (not just row order) reshuffles
... on every page load, so this can't be hardcoded.
${index}= Execute Javascript
... var headers = Array.from(document.querySelectorAll('[role="columnheader"]'))
... .map(function(h) { return h.textContent.trim(); });
... return headers.indexOf('CPU');
RETURN ${index}

Clear Field And Notify Page
[Documentation] Clears an input/textarea and manually dispatches an input event.
... SeleniumLibrary's Clear Element Text updates the DOM value but does
... not fire an input event, so pages that recompute state from that
... event (like Clear Input's live counter) never notice the change
... without this extra nudge.
[Arguments] ${locator}
Clear Element Text ${locator}
${element}= Get WebElement ${locator}
Execute Javascript
... arguments[0].dispatchEvent(new Event('input', {bubbles: true}));
... ARGUMENTS
... ${element}
4 changes: 4 additions & 0 deletions robocop.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@ configure = [
# A form-filling keyword with one call per field naturally exceeds the
# default ceiling; still bounded to keep keywords readable.
"too-many-calls-in-keyword.max_calls=12",
# The playground suite covers 29 independent single-page challenges, one
# test case each; splitting that cohesive suite into multiple files to
# dodge a line count would hurt readability more than it helps.
"file-too-long.max_lines=420",
]
Loading
Loading