From 19a02379e6df143248407d1cf1b31de14d3c5cc1 Mon Sep 17 00:00:00 2001 From: Michelle Austria Date: Mon, 17 Aug 2026 18:00:01 +0000 Subject: [PATCH 01/11] Add UI Testing Playground coverage Adds a new, independent suite covering every challenge page on uitestingplayground.com: Dynamic ID, Class Attribute, Hidden Layers, Load Delay, AJAX Data, Client Side Delay, Click, Text Input, Scrollbars, Dynamic Table, Verify Text, Progress Bar, Visibility, Sample App, Mouse Over, Non-Breaking Space, Overlapped Element, Shadow DOM, Alerts, File Upload, Animated Button, Disabled Input, Auto Wait, and Auto Complete. Each page is small and independent (unlike the insurance wizard, no page's keywords are reused by more than one test), so locators and helper keywords live in a single resources/pages/playground_page.resource file rather than one file per page. Every test case asserts a genuine outcome specific to that page's challenge (correct element identified, value actually propagated, click actually intercepted, etc.), not just that no exception was raised. Wired into CI as its own job, separate from the existing UI and API jobs, so a flaky third-party page here can't affect the already-stable insurance suite's status. --- .github/workflows/tests.yml | 26 ++ README.md | 36 ++- resources/data/sample_upload.txt | 1 + resources/pages/playground_page.resource | 146 +++++++++++ tests/playground/ui_testing_playground.robot | 252 +++++++++++++++++++ 5 files changed, 453 insertions(+), 8 deletions(-) create mode 100644 resources/data/sample_upload.txt create mode 100644 resources/pages/playground_page.resource create mode 100644 tests/playground/ui_testing_playground.robot diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4eddefb..07b4aaf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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/playground + + - name: Upload playground test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: playground-test-results + path: results/playground diff --git a/README.md b/README.md index 9470fc9..e9ab981 100644 --- a/README.md +++ b/README.md @@ -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 on + [uitestingplayground.com](http://www.uitestingplayground.com/) (dynamic + attributes, timing, visibility, Shadow DOM, alerts, and more), one test + case per page. ## Project structure @@ -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 @@ -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 @@ -145,6 +161,10 @@ 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. + ## License Apache License 2.0, see [LICENSE](LICENSE). diff --git a/resources/data/sample_upload.txt b/resources/data/sample_upload.txt new file mode 100644 index 0000000..741ca8a --- /dev/null +++ b/resources/data/sample_upload.txt @@ -0,0 +1 @@ +Sample file used by the File Upload challenge test on uitestingplayground.com. diff --git a/resources/pages/playground_page.resource b/resources/pages/playground_page.resource new file mode 100644 index 0000000..0f99633 --- /dev/null +++ b/resources/pages/playground_page.resource @@ -0,0 +1,146 @@ +*** Settings *** +Documentation Locators and keywords for uitestingplayground.com, 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} ${CURDIR}/../data/sample_upload.txt + +# Dynamic ID +${DYNAMIC_ID_BUTTON} css:button.btn.btn-primary + +# Class Attribute +${CLASS_ATTRIBUTE_TARGET_BUTTON} xpath://button[@class="btn btn-primary"] + +# Hidden Layers +${HIDING_BUTTON} css:#hidingButton + +# Load Delay +${LOAD_DELAY_BUTTON} css:button.btn.btn-primary + +# AJAX Data +${AJAX_BUTTON} css:#ajaxButton +${AJAX_RESULT} css:.bg-success + +# Client Side Delay +${CLIENT_DELAY_BUTTON} css:#ajaxButton +${CLIENT_DELAY_RESULT} css:.bg-success + +# Click +${CLICK_TARGET_BUTTON} css:#badButton + +# Text Input +${TEXT_INPUT_FIELD} css:#newButtonName +${TEXT_INPUT_UPDATE_BUTTON} css:#updatingButton + +# Scrollbars +${SCROLLBAR_BUTTON} css:#hidingButton + +# Verify Text +${VERIFY_TEXT_PARAGRAPH} css:.p-3 + +# 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_VISIBLE_BUTTON} css:#notvisibleButton + +# Sample App +${SAMPLE_APP_USERNAME_FIELD} css:#username +${SAMPLE_APP_PASSWORD_FIELD} css:#password +${SAMPLE_APP_LOGIN_BUTTON} css:#login +${SAMPLE_APP_STATUS_MESSAGE} css:.output + +# Mouse Over +${MOUSE_OVER_LINK} css:div.text-center > a[href="#"] +${MOUSE_OVER_CLICK_COUNT} css:#clickCount + +# Non-Breaking Space +${NBSP_BUTTON} xpath://button[contains(@class, "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_OUTPUT_ID} output +${SHADOW_EDIT_FIELD} css:#editField + +# Alerts +${ALERT_BUTTON} css:#alertButton +${ALERT_RESULT} css:#result + +# File Upload +${FILE_UPLOAD_INPUT} css:#fileinput +${FILE_UPLOAD_BUTTON} css:#uploadButton + +# Animated Button +${ANIMATED_BUTTON} css:#animatedButton + +# Disabled Input +${DISABLED_INPUT_FIELD} css:#input-example + +# Auto Wait +${AUTO_WAIT_BUTTON} css:#ajaxButton + +# Auto Complete +${AUTO_COMPLETE_INPUT} css:.react-autosuggest__input +${AUTO_COMPLETE_SUGGESTIONS} css:.react-autosuggest__suggestions-list li +${AUTO_COMPLETE_FIRST_SUGGESTION} css:.react-autosuggest__suggestions-list li:first-child +${AUTO_COMPLETE_SELECTED_TAGS} css:.tag + + +*** 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. diff --git a/tests/playground/ui_testing_playground.robot b/tests/playground/ui_testing_playground.robot new file mode 100644 index 0000000..8804fed --- /dev/null +++ b/tests/playground/ui_testing_playground.robot @@ -0,0 +1,252 @@ +*** Settings *** +Documentation Coverage for every challenge page listed on the +... uitestingplayground.com homepage. Each page is an independent, +... self-contained UI automation challenge (dynamic attributes, timing, +... visibility, Shadow DOM, and so on), so each test case navigates to +... its own page directly rather than sharing state with the others. + +Resource ../../resources/common.resource +Resource ../../resources/pages/playground_page.resource + +Suite Setup Open Playground Browser +Suite Teardown Close All Browsers +Test Teardown Capture Screenshot On Failure +Test Timeout 1 minute + +Test Tags playground + + +*** Test Cases *** +Dynamic ID Button Is Locatable Despite Its Changing Id + [Documentation] The button's id attribute changes on every page load, so it's + ... located by its stable CSS class instead. + Go To Playground Page dynamicid + Element Text Should Be ${DYNAMIC_ID_BUTTON} Button with Dynamic ID + Click Button ${DYNAMIC_ID_BUTTON} + +Class Attribute Locates The Exact Button Among Look Alike Decoys + [Documentation] Several buttons share very similar classes; only an exact class + ... match finds the correct one. + Go To Playground Page classattr + Element Attribute Value Should Be ${CLASS_ATTRIBUTE_TARGET_BUTTON} class + ... btn btn-primary + Click Button ${CLASS_ATTRIBUTE_TARGET_BUTTON} + +Hidden Layer Blocks A Second Click On The Same Button + [Documentation] The first click reveals an invisible overlay covering the + ... button, so a second click at the same coordinates is + ... intercepted by that overlay instead of reaching the button. + Go To Playground Page hiddenlayers + Click Element ${HIDING_BUTTON} + Element Should Be Visible css:#hidden-layer + Run Keyword And Expect Error * Click Element ${HIDING_BUTTON} + +Load Delay Button Appears Only After An Explicit Wait + [Documentation] The page takes several seconds to render the button; only an + ... explicit wait (not a fixed sleep) reliably catches it. + Go To Playground Page loaddelay + Wait Until Element Is Visible ${LOAD_DELAY_BUTTON} timeout=20s + Click Button ${LOAD_DELAY_BUTTON} + +Ajax Data Request Populates The Result Text After A Delay + [Documentation] Clicking the button triggers a server round trip; the result + ... text only appears once that AJAX call completes. + Go To Playground Page ajax + Click Button ${AJAX_BUTTON} + Wait Until Element Is Visible ${AJAX_RESULT} timeout=20s + Element Text Should Be ${AJAX_RESULT} Data loaded with AJAX get request. + +Client Side Delay Does Not Block The Page While Waiting + [Documentation] The delay here runs entirely in the browser (not a server + ... round trip), but the result still only appears once it settles. + Go To Playground Page clientdelay + Click Button ${CLIENT_DELAY_BUTTON} + Wait Until Element Is Visible ${CLIENT_DELAY_RESULT} timeout=20s + Element Text Should Be ${CLIENT_DELAY_RESULT} Data calculated on the client side. + +Click Registers On The Correctly Identified Button + [Documentation] A decoy button sits nearby; only clicking the real target + ... changes its class, confirming the right element was hit. + Go To Playground Page click + Click Button ${CLICK_TARGET_BUTTON} + Element Attribute Value Should Be ${CLICK_TARGET_BUTTON} class btn btn-success + +Text Input Value Is Reflected On The Button Label + [Documentation] Typing a new name and clicking the button updates the button's + ... own label to match what was typed. + Go To Playground Page textinput + Input Text ${TEXT_INPUT_FIELD} Automated Button Name + Click Button ${TEXT_INPUT_UPDATE_BUTTON} + Element Text Should Be ${TEXT_INPUT_UPDATE_BUTTON} Automated Button Name + +Scrollbar Hidden Button Is Clicked After Scrolling Into View + [Documentation] The button sits inside a scrollable container out of the + ... initial viewport; a standard click scrolls it into view first. + Go To Playground Page scrollbars + Wait Until Element Is Visible ${SCROLLBAR_BUTTON} + Click Button ${SCROLLBAR_BUTTON} + +Dynamic Table Reports The Highlighted Browser Cpu Usage Correctly + [Documentation] Reads which browser the warning label calls out, finds that + ... browser's row in the table, and verifies the CPU percentage + ... shown there matches the value stated in the label. + Go To Playground Page dynamictable + ${result}= Execute Javascript + ... var label = document.querySelector('p.bg-warning').innerText; + ... var match = label.match(/([A-Za-z]+)[^\d]*(\d+)%/); + ... var browser = match[1]; + ... var expected = match[2]; + ... var actual = null; + ... function checkRow(row) { + ... var cells = row.querySelectorAll('td'); + ... if (cells[0] && cells[0].innerText.trim() === browser) { actual = cells[1].innerText.trim(); } + ... } + ... document.querySelectorAll('table tbody tr').forEach(checkRow); + ... return JSON.stringify({browser: browser, expected: expected, actual: actual}); + ${data}= Evaluate json.loads('''${result}''') json + ${message}= Catenate + ... CPU usage for ${data}[browser] in the table (${data}[actual]%) + ... did not match the warning label (${data}[expected]%) + Should Be Equal As Strings ${data}[expected] ${data}[actual] msg=${message} + +Verify Text Handles Non Obvious Internal Whitespace + [Documentation] The paragraph's rendered text uses irregular internal spacing + ... that a naive hard-coded comparison would miss, so it's + ... normalized before asserting on its content. + Go To Playground Page verifytext + ${text}= Get Text ${VERIFY_TEXT_PARAGRAPH} + ${normalized}= Evaluate ' '.join($text.split()) + Should Not Be Empty ${normalized} + Should Not Contain ${normalized} ${SPACE}${SPACE} + +Progress Bar Can Be Stopped Near A Target Value + [Documentation] Starts the progress bar, stops it once it reaches at least 75%, + ... and verifies it didn't run all the way to completion. + Go To Playground Page progressbar + Click Button ${PROGRESS_BAR_START_BUTTON} + Wait Until Keyword Succeeds 20s 100ms Progress Bar Value Should Be At Least 75 + Click Button ${PROGRESS_BAR_STOP_BUTTON} + ${value}= Get Element Attribute ${PROGRESS_BAR} aria-valuenow + Should Be True 75 <= ${value} <= 100 + +Each Visibility Challenge Is Undetectable By Its Own Hiding Technique + [Documentation] Clicking "Hide" hides five buttons via five different + ... techniques; each is verified undetectable by whichever check + ... actually applies to how it was hidden. + Go To Playground Page visibility + Click Button ${VISIBILITY_HIDE_BUTTON} + Page Should Not Contain Element ${VISIBILITY_REMOVED_BUTTON} + Element Should Not Be Visible ${VISIBILITY_ZERO_WIDTH_BUTTON} + Element Should Not Be Visible ${VISIBILITY_TRANSPARENT_BUTTON} + Element Should Not Be Visible ${VISIBILITY_INVISIBLE_BUTTON} + Element Should Not Be Visible ${VISIBILITY_NOT_VISIBLE_BUTTON} + Run Keyword And Expect Error * Click Button ${VISIBILITY_OVERLAPPED_BUTTON} + +Sample App Login Succeeds With The Documented Password + [Documentation] The sample login form accepts any username paired with the + ... password "pwd" and greets the user by the name they entered. + Go To Playground Page sampleapp + Input Text ${SAMPLE_APP_USERNAME_FIELD} michelle + Input Text ${SAMPLE_APP_PASSWORD_FIELD} pwd + Click Button ${SAMPLE_APP_LOGIN_BUTTON} + Element Text Should Be ${SAMPLE_APP_STATUS_MESSAGE} Welcome, michelle! + +Mouse Over Link Only Registers A Click After Hovering First + [Documentation] The link is covered by a decoy layer that only clears on + ... mouse-over, so hovering before clicking is required for the + ... click to register. + Go To Playground Page mouseover + Mouse Over ${MOUSE_OVER_LINK} + Click Element ${MOUSE_OVER_LINK} + Element Text Should Be ${MOUSE_OVER_CLICK_COUNT} 1 + +Non Breaking Space Button Is Missed By A Naive Text Locator + [Documentation] The button's visible text looks like "My Button" but uses a + ... non-breaking space, so a locator built on a regular space + ... genuinely fails to find it, while a class-based locator works. + Go To Playground Page nbsp + Run Keyword And Expect Error * Page Should Contain Element + ... ${NBSP_BUTTON_EXACT_TEXT} + Element Should Be Visible ${NBSP_BUTTON} + Click Button ${NBSP_BUTTON} + +Overlapped Input Still Receives The Entered Text + [Documentation] The name field becomes covered by another panel once the page + ... is interacted with, but sending keys directly still correctly + ... sets its value. + Go To Playground Page overlapped + Input Text ${OVERLAPPED_NAME_FIELD} Michelle + Element Attribute Value Should Be ${OVERLAPPED_NAME_FIELD} value Michelle + +Shadow Dom Generated Value Can Be Copied Into The Regular Input + [Documentation] The GUID is generated and displayed inside a Shadow DOM tree + ... that plain locators can't pierce, so it's read via JavaScript. + ... Clicking "Copy" should place that same value into the visible + ... input field that lives outside the shadow tree. + Go To Playground Page shadowdom + Click Shadow Element ${SHADOW_HOST} ${SHADOW_GENERATE_BUTTON_ID} + ${generated}= Get Shadow Element Text ${SHADOW_HOST} ${SHADOW_OUTPUT_ID} + Click Shadow Element ${SHADOW_HOST} ${SHADOW_COPY_BUTTON_ID} + Textfield Value Should Be ${SHADOW_EDIT_FIELD} ${generated} + +Alert Result Reflects The Chosen Alert Action + [Documentation] Clicking the button triggers a native JS alert after a short + ... delay; accepting it is reflected in the result text. + Go To Playground Page alerts + Click Button ${ALERT_BUTTON} + Handle Alert action=ACCEPT timeout=10s + Element Text Should Be ${ALERT_RESULT} You have chosen: Ok + +File Upload Accepts A Selected File + [Documentation] Selecting a local file and submitting the upload form should + ... be accepted without error. + Go To Playground Page upload + Choose File ${FILE_UPLOAD_INPUT} ${SAMPLE_UPLOAD_FILE} + Click Button ${FILE_UPLOAD_BUTTON} + +Animated Button Is Clicked Only Once It Stops Moving + [Documentation] The button moves via a CSS transition for a short time after + ... the page loads; clicking waits for it to settle first. + Go To Playground Page animatedbutton + Wait Until Animation Stops ${ANIMATED_BUTTON} + Click Button ${ANIMATED_BUTTON} + +Disabled Input Becomes Enabled After A Delay + [Documentation] The input field starts disabled and is enabled by client-side + ... JavaScript after a short delay. + Go To Playground Page disabledinput + Wait Until Element Is Enabled ${DISABLED_INPUT_FIELD} timeout=10s + Input Text ${DISABLED_INPUT_FIELD} Michelle + +Auto Wait Button Is Clicked Only After Its Own Delay Settles + [Documentation] Clicking the button starts a client-side delay before the + ... success text appears; an explicit wait is required since the + ... button gives no visible feedback while the delay is running. + Go To Playground Page auto_wait + Click Button ${AUTO_WAIT_BUTTON} + Wait Until Element Is Visible ${AJAX_RESULT} timeout=20s + +Auto Complete Suggests And Selects A Matching Tag + [Documentation] Typing a partial name shows matching suggestions; selecting one + ... adds it as a tag. + Go To Playground Page auto_complete + Input Text ${AUTO_COMPLETE_INPUT} Cal + Wait Until Element Is Visible ${AUTO_COMPLETE_SUGGESTIONS} timeout=10s + Click Element ${AUTO_COMPLETE_FIRST_SUGGESTION} + Element Should Be Visible ${AUTO_COMPLETE_SELECTED_TAGS} + + +*** Keywords *** +Open Playground Browser + [Documentation] Starts a single browser session reused across every challenge + ... page in this suite, since each test navigates independently. + Open Browser about:blank ${BROWSER} + Set Selenium Timeout ${SELENIUM_TIMEOUT} + Set Window Size 1920 1080 + +Progress Bar Value Should Be At Least + [Documentation] Fails unless the progress bar's current value has reached the + ... given threshold. + [Arguments] ${threshold} + ${value}= Get Element Attribute ${PROGRESS_BAR} aria-valuenow + Should Be True ${value} >= ${threshold} From bf90d14e0708b27282eba8a484233d795e685d3e Mon Sep 17 00:00:00 2001 From: Michelle Austria Date: Mon, 17 Aug 2026 18:03:18 +0000 Subject: [PATCH 02/11] temp: dump real DOM structure for failing playground pages --- .github/workflows/tests.yml | 2 +- tests/_diagnostic.robot | 98 +++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 tests/_diagnostic.robot diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 07b4aaf..3925894 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -74,7 +74,7 @@ jobs: robot --outputdir results/playground --variable BROWSER:headlesschrome - tests/playground + tests/_diagnostic.robot - name: Upload playground test results if: always() diff --git a/tests/_diagnostic.robot b/tests/_diagnostic.robot new file mode 100644 index 0000000..16e3e0a --- /dev/null +++ b/tests/_diagnostic.robot @@ -0,0 +1,98 @@ +*** Settings *** +Documentation Temporary diagnostic: dumps real DOM structure for every failing +... playground page in one CI run. Not part of the portfolio. +Resource ../resources/pages/playground_page.resource +Suite Setup Open Browser about:blank ${BROWSER} +Suite Teardown Close All Browsers +Test Tags smoke + + +*** Keywords *** +Dump Page Elements + [Documentation] Logs every element with an id, plus every button/input/a/p tag, + ... and any custom (hyphenated) element tags on the current page. + ${dump}= Execute Javascript + ... function describe(e) { + ... var text = (e.innerText || e.value || '').trim().substring(0, 40); + ... return e.tagName + '#' + (e.id || '') + '.' + (e.className || '') + '="' + text + '"'; + ... } + ... var withId = Array.from(document.querySelectorAll('[id]')).map(describe); + ... var interactive = Array.from(document.querySelectorAll('button, input, a, p')).map(describe); + ... var customTags = Array.from(document.querySelectorAll('*')).filter(function(e) { return e.tagName.includes('-'); }).map(describe); + ... var all = withId.concat(interactive).concat(customTags); + ... return Array.from(new Set(all)).join(' | '); + Log DUMP: ${dump} console=True + + +*** Test Cases *** +Dump Class Attribute + Go To Playground Page classattr + Dump Page Elements + +Dump Hidden Layers + Go To Playground Page hiddenlayers + Dump Page Elements + +Dump Dynamic Table + Go To Playground Page dynamictable + Dump Page Elements + +Dump Verify Text + Go To Playground Page verifytext + Dump Page Elements + +Dump Progress Bar + Go To Playground Page progressbar + Click Button ${PROGRESS_BAR_START_BUTTON} + Sleep 20s + ${value}= Get Element Attribute ${PROGRESS_BAR} aria-valuenow + Log PROGRESS AFTER 20s: ${value} console=True + +Dump Sample App + Go To Playground Page sampleapp + Dump Page Elements + +Dump Mouse Over + Go To Playground Page mouseover + Dump Page Elements + +Dump Overlapped + Go To Playground Page overlapped + Dump Page Elements + Input Text css:#name Michelle + ${value}= Get Element Attribute css:#name value + Log NAME VALUE AFTER INPUT: ${value} console=True + +Dump Shadow Dom + Go To Playground Page shadowdom + ${dump}= Execute Javascript + ... var host = document.querySelector('guid-generator'); + ... if (!host) { return 'NO guid-generator HOST FOUND. Custom tags: ' + Array.from(document.querySelectorAll('*')).filter(function(e) { return e.tagName.includes('-'); }).map(function(e) { return e.tagName; }).join(','); } + ... var root = host.shadowRoot; + ... if (!root) { return 'HOST FOUND BUT NO shadowRoot'; } + ... return Array.from(root.querySelectorAll('*')).map(function(e) { return e.tagName + '#' + (e.id || '') + '.' + (e.className || ''); }).join(' | '); + Log SHADOW DUMP: ${dump} console=True + +Dump Alerts + Go To Playground Page alerts + Dump Page Elements + +Dump File Upload + Go To Playground Page upload + Dump Page Elements + +Dump Animated Button + Go To Playground Page animatedbutton + Dump Page Elements + +Dump Disabled Input + Go To Playground Page disabledinput + Dump Page Elements + +Dump Auto Wait + Go To Playground Page auto_wait + Dump Page Elements + +Dump Auto Complete + Go To Playground Page auto_complete + Dump Page Elements From 50eacc55cefc54678c59584fc2138a7fbaf9d110 Mon Sep 17 00:00:00 2001 From: Michelle Austria Date: Mon, 17 Aug 2026 18:12:06 +0000 Subject: [PATCH 03/11] Add homepage link dump to diagnostic suite --- tests/_diagnostic.robot | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/_diagnostic.robot b/tests/_diagnostic.robot index 16e3e0a..2ec24bb 100644 --- a/tests/_diagnostic.robot +++ b/tests/_diagnostic.robot @@ -25,6 +25,18 @@ Dump Page Elements *** Test Cases *** +Dump Homepage + Go To ${PLAYGROUND_BASE_URL} + ${dump}= Execute Javascript + ... function describeLink(a) { + ... return a.getAttribute('href') + ' => ' + a.innerText.trim(); + ... } + ... var links = Array.from(document.querySelectorAll('a[href]')).map(describeLink); + ... return Array.from(new Set(links)).join(' | '); + Log HOMEPAGE LINKS: ${dump} console=True + ${footer}= Execute Javascript return document.body.innerText.substring(0, 500); + Log HOMEPAGE TEXT: ${footer} console=True + Dump Class Attribute Go To Playground Page classattr Dump Page Elements From 956e252b3461c76bd8c3590bac4ff40f5e99dddc Mon Sep 17 00:00:00 2001 From: Michelle Austria Date: Mon, 17 Aug 2026 18:14:51 +0000 Subject: [PATCH 04/11] Round 2 diagnostic: dump newly discovered playground pages --- tests/_diagnostic.robot | 120 ++++++++++++++++++---------------------- 1 file changed, 54 insertions(+), 66 deletions(-) diff --git a/tests/_diagnostic.robot b/tests/_diagnostic.robot index 2ec24bb..ef9deec 100644 --- a/tests/_diagnostic.robot +++ b/tests/_diagnostic.robot @@ -1,6 +1,9 @@ *** Settings *** -Documentation Temporary diagnostic: dumps real DOM structure for every failing -... playground page in one CI run. Not part of the portfolio. +Documentation Temporary diagnostic, round 2: dumps real DOM structure for the +... playground pages discovered via the real site's homepage link list +... (Frames, Geo Location, Clear Input, Scroll to Click, CSS Selectors, +... Select) plus remaining unknowns (dynamic table rows, sample app field +... types, file upload structure, alert text). Not part of the portfolio. Resource ../resources/pages/playground_page.resource Suite Setup Open Browser about:blank ${BROWSER} Suite Teardown Close All Browsers @@ -25,86 +28,71 @@ Dump Page Elements *** Test Cases *** -Dump Homepage - Go To ${PLAYGROUND_BASE_URL} - ${dump}= Execute Javascript - ... function describeLink(a) { - ... return a.getAttribute('href') + ' => ' + a.innerText.trim(); - ... } - ... var links = Array.from(document.querySelectorAll('a[href]')).map(describeLink); - ... return Array.from(new Set(links)).join(' | '); - Log HOMEPAGE LINKS: ${dump} console=True - ${footer}= Execute Javascript return document.body.innerText.substring(0, 500); - Log HOMEPAGE TEXT: ${footer} console=True - -Dump Class Attribute - Go To Playground Page classattr +Dump Animation + Go To Playground Page animation Dump Page Elements -Dump Hidden Layers - Go To Playground Page hiddenlayers +Dump Auto Wait + Go To Playground Page autowait Dump Page Elements -Dump Dynamic Table - Go To Playground Page dynamictable +Dump Frames + Go To Playground Page frames + ${dump}= Execute Javascript + ... return Array.from(document.querySelectorAll('iframe')).map(function(f) { + ... return 'IFRAME#' + (f.id || '') + ' src=' + f.getAttribute('src'); + ... }).join(' | '); + Log FRAMES DUMP: ${dump} console=True Dump Page Elements -Dump Verify Text - Go To Playground Page verifytext +Dump Geolocation + Go To Playground Page geolocation Dump Page Elements -Dump Progress Bar - Go To Playground Page progressbar - Click Button ${PROGRESS_BAR_START_BUTTON} - Sleep 20s - ${value}= Get Element Attribute ${PROGRESS_BAR} aria-valuenow - Log PROGRESS AFTER 20s: ${value} console=True +Dump Clear Input + Go To Playground Page clearinput + Dump Page Elements -Dump Sample App - Go To Playground Page sampleapp +Dump Scroll To Click + Go To Playground Page scrolltoclick Dump Page Elements -Dump Mouse Over - Go To Playground Page mouseover +Dump CSS Selectors + Go To Playground Page cssselectors Dump Page Elements -Dump Overlapped - Go To Playground Page overlapped +Dump Select + Go To Playground Page select Dump Page Elements - Input Text css:#name Michelle - ${value}= Get Element Attribute css:#name value - Log NAME VALUE AFTER INPUT: ${value} console=True -Dump Shadow Dom - Go To Playground Page shadowdom +Dump Dynamic Table Full + Go To Playground Page dynamictable ${dump}= Execute Javascript - ... var host = document.querySelector('guid-generator'); - ... if (!host) { return 'NO guid-generator HOST FOUND. Custom tags: ' + Array.from(document.querySelectorAll('*')).filter(function(e) { return e.tagName.includes('-'); }).map(function(e) { return e.tagName; }).join(','); } - ... var root = host.shadowRoot; - ... if (!root) { return 'HOST FOUND BUT NO shadowRoot'; } - ... return Array.from(root.querySelectorAll('*')).map(function(e) { return e.tagName + '#' + (e.id || '') + '.' + (e.className || ''); }).join(' | '); - Log SHADOW DUMP: ${dump} console=True + ... return Array.from(document.querySelectorAll('table tr')).map(function(tr) { + ... return Array.from(tr.children).map(function(td) { return td.textContent.trim(); }).join(','); + ... }).join(' || '); + Log TABLE DUMP: ${dump} console=True -Dump Alerts - Go To Playground Page alerts - Dump Page Elements +Dump Sample App Fields + Go To Playground Page sampleapp + ${dump}= Execute Javascript + ... return Array.from(document.querySelectorAll('input')).map(function(i) { + ... return i.id + ':type=' + i.type + ':placeholder=' + (i.placeholder || ''); + ... }).join(' | '); + Log FIELDS DUMP: ${dump} console=True -Dump File Upload +Dump File Upload Full Go To Playground Page upload - Dump Page Elements - -Dump Animated Button - Go To Playground Page animatedbutton - Dump Page Elements - -Dump Disabled Input - Go To Playground Page disabledinput - Dump Page Elements - -Dump Auto Wait - Go To Playground Page auto_wait - Dump Page Elements - -Dump Auto Complete - Go To Playground Page auto_complete - Dump Page Elements + ${dump}= Execute Javascript + ... var iframes = document.querySelectorAll('iframe').length; + ... var inputs = Array.from(document.querySelectorAll('input')).map(function(i) { + ... return i.type + '#' + i.id; + ... }); + ... return 'iframes=' + iframes + ' inputs=' + inputs.join(','); + Log UPLOAD DUMP: ${dump} console=True + +Dump Alerts Interaction + Go To Playground Page alerts + Click Button ${ALERT_BUTTON} + ${msg}= Handle Alert action=ACCEPT + Log ALERT MESSAGE: ${msg} console=True From 3e9e14b5d3d9a51c1bba4f6645523f0d1f5925bb Mon Sep 17 00:00:00 2001 From: Michelle Austria Date: Mon, 17 Aug 2026 18:17:25 +0000 Subject: [PATCH 05/11] Round 3 diagnostic: dynamic table ARIA structure and upload iframe contents --- tests/_diagnostic.robot | 105 ++++++++-------------------------------- 1 file changed, 21 insertions(+), 84 deletions(-) diff --git a/tests/_diagnostic.robot b/tests/_diagnostic.robot index ef9deec..88c9700 100644 --- a/tests/_diagnostic.robot +++ b/tests/_diagnostic.robot @@ -1,98 +1,35 @@ *** Settings *** -Documentation Temporary diagnostic, round 2: dumps real DOM structure for the -... playground pages discovered via the real site's homepage link list -... (Frames, Geo Location, Clear Input, Scroll to Click, CSS Selectors, -... Select) plus remaining unknowns (dynamic table rows, sample app field -... types, file upload structure, alert text). Not part of the portfolio. +Documentation Temporary diagnostic, round 3: the Dynamic Table page uses no +... element (round 2's table/tr query came back empty), so it must use an +... ARIA row/cell pattern instead; and the File Upload widget lives inside +... an iframe with no top-level . Both need direct inspection before +... real locators can be written. Not part of the portfolio. Resource ../resources/pages/playground_page.resource Suite Setup Open Browser about:blank ${BROWSER} Suite Teardown Close All Browsers Test Tags smoke -*** Keywords *** -Dump Page Elements - [Documentation] Logs every element with an id, plus every button/input/a/p tag, - ... and any custom (hyphenated) element tags on the current page. - ${dump}= Execute Javascript - ... function describe(e) { - ... var text = (e.innerText || e.value || '').trim().substring(0, 40); - ... return e.tagName + '#' + (e.id || '') + '.' + (e.className || '') + '="' + text + '"'; - ... } - ... var withId = Array.from(document.querySelectorAll('[id]')).map(describe); - ... var interactive = Array.from(document.querySelectorAll('button, input, a, p')).map(describe); - ... var customTags = Array.from(document.querySelectorAll('*')).filter(function(e) { return e.tagName.includes('-'); }).map(describe); - ... var all = withId.concat(interactive).concat(customTags); - ... return Array.from(new Set(all)).join(' | '); - Log DUMP: ${dump} console=True - - *** Test Cases *** -Dump Animation - Go To Playground Page animation - Dump Page Elements - -Dump Auto Wait - Go To Playground Page autowait - Dump Page Elements - -Dump Frames - Go To Playground Page frames - ${dump}= Execute Javascript - ... return Array.from(document.querySelectorAll('iframe')).map(function(f) { - ... return 'IFRAME#' + (f.id || '') + ' src=' + f.getAttribute('src'); - ... }).join(' | '); - Log FRAMES DUMP: ${dump} console=True - Dump Page Elements - -Dump Geolocation - Go To Playground Page geolocation - Dump Page Elements - -Dump Clear Input - Go To Playground Page clearinput - Dump Page Elements - -Dump Scroll To Click - Go To Playground Page scrolltoclick - Dump Page Elements - -Dump CSS Selectors - Go To Playground Page cssselectors - Dump Page Elements - -Dump Select - Go To Playground Page select - Dump Page Elements - -Dump Dynamic Table Full +Dump Dynamic Table ARIA Go To Playground Page dynamictable ${dump}= Execute Javascript - ... return Array.from(document.querySelectorAll('table tr')).map(function(tr) { - ... return Array.from(tr.children).map(function(td) { return td.textContent.trim(); }).join(','); - ... }).join(' || '); - Log TABLE DUMP: ${dump} console=True - -Dump Sample App Fields - Go To Playground Page sampleapp - ${dump}= Execute Javascript - ... return Array.from(document.querySelectorAll('input')).map(function(i) { - ... return i.id + ':type=' + i.type + ':placeholder=' + (i.placeholder || ''); + ... return Array.from(document.querySelectorAll('[role]')).map(function(e) { + ... return e.getAttribute('role') + ':' + e.className + '="' + e.textContent.trim().substring(0, 30) + '"'; ... }).join(' | '); - Log FIELDS DUMP: ${dump} console=True + Log ARIA DUMP: ${dump} console=True -Dump File Upload Full +Dump File Upload Iframe Go To Playground Page upload + ${frame_info}= Execute Javascript + ... var f = document.querySelector('iframe'); + ... return 'name=' + f.name + ' id=' + f.id + ' src=' + f.src + ' className=' + f.className; + Log FRAME INFO: ${frame_info} console=True + Select Frame css:iframe ${dump}= Execute Javascript - ... var iframes = document.querySelectorAll('iframe').length; - ... var inputs = Array.from(document.querySelectorAll('input')).map(function(i) { - ... return i.type + '#' + i.id; - ... }); - ... return 'iframes=' + iframes + ' inputs=' + inputs.join(','); - Log UPLOAD DUMP: ${dump} console=True - -Dump Alerts Interaction - Go To Playground Page alerts - Click Button ${ALERT_BUTTON} - ${msg}= Handle Alert action=ACCEPT - Log ALERT MESSAGE: ${msg} console=True + ... function describe(e) { + ... var text = (e.innerText || e.value || '').trim().substring(0, 40); + ... return e.tagName + '#' + (e.id || '') + '.' + (e.className || '') + '="' + text + '"'; + ... } + ... return Array.from(document.querySelectorAll('[id], button, input, a, p, label')).map(describe).join(' | '); + Log IFRAME CONTENT DUMP: ${dump} console=True From 4d36e669acb0b8fd62a308a1a5173a09a8c0a5a3 Mon Sep 17 00:00:00 2001 From: Michelle Austria Date: Mon, 17 Aug 2026 18:19:43 +0000 Subject: [PATCH 06/11] Round 4 diagnostic: verify remaining unconfirmed playground page locators --- tests/_diagnostic.robot | 81 +++++++++++++++++++++++++++++------------ 1 file changed, 58 insertions(+), 23 deletions(-) diff --git a/tests/_diagnostic.robot b/tests/_diagnostic.robot index 88c9700..744c101 100644 --- a/tests/_diagnostic.robot +++ b/tests/_diagnostic.robot @@ -1,35 +1,70 @@ *** Settings *** -Documentation Temporary diagnostic, round 3: the Dynamic Table page uses no
-... element (round 2's table/tr query came back empty), so it must use an -... ARIA row/cell pattern instead; and the File Upload widget lives inside -... an iframe with no top-level . Both need direct inspection before -... real locators can be written. Not part of the portfolio. +Documentation Temporary diagnostic, round 4: dumps the remaining playground pages +... whose locators were never verified against the real site (they were +... guessed from memory and might be wrong, the same way classattr, +... hiddenlayers, sampleapp, and others turned out to be). Not part of +... the portfolio. Resource ../resources/pages/playground_page.resource Suite Setup Open Browser about:blank ${BROWSER} Suite Teardown Close All Browsers Test Tags smoke -*** Test Cases *** -Dump Dynamic Table ARIA - Go To Playground Page dynamictable - ${dump}= Execute Javascript - ... return Array.from(document.querySelectorAll('[role]')).map(function(e) { - ... return e.getAttribute('role') + ':' + e.className + '="' + e.textContent.trim().substring(0, 30) + '"'; - ... }).join(' | '); - Log ARIA DUMP: ${dump} console=True - -Dump File Upload Iframe - Go To Playground Page upload - ${frame_info}= Execute Javascript - ... var f = document.querySelector('iframe'); - ... return 'name=' + f.name + ' id=' + f.id + ' src=' + f.src + ' className=' + f.className; - Log FRAME INFO: ${frame_info} console=True - Select Frame css:iframe +*** Keywords *** +Dump Page Elements + [Documentation] Logs every element with an id, plus every button/input/a/p tag, + ... and any custom (hyphenated) element tags on the current page. ${dump}= Execute Javascript ... function describe(e) { ... var text = (e.innerText || e.value || '').trim().substring(0, 40); ... return e.tagName + '#' + (e.id || '') + '.' + (e.className || '') + '="' + text + '"'; ... } - ... return Array.from(document.querySelectorAll('[id], button, input, a, p, label')).map(describe).join(' | '); - Log IFRAME CONTENT DUMP: ${dump} console=True + ... var withId = Array.from(document.querySelectorAll('[id]')).map(describe); + ... var interactive = Array.from(document.querySelectorAll('button, input, a, p')).map(describe); + ... var all = withId.concat(interactive); + ... return Array.from(new Set(all)).join(' | '); + Log DUMP: ${dump} console=True + + +*** Test Cases *** +Dump Dynamic Id + Go To Playground Page dynamicid + Dump Page Elements + +Dump Load Delay + Go To Playground Page loaddelay + Sleep 16s + Dump Page Elements + +Dump Ajax Data + Go To Playground Page ajax + Click Button css:#ajaxButton + Sleep 16s + Dump Page Elements + +Dump Client Side Delay + Go To Playground Page clientdelay + Dump Page Elements + Click Button css:#ajaxButton + Sleep 16s + Dump Page Elements + +Dump Click + Go To Playground Page click + Dump Page Elements + +Dump Text Input + Go To Playground Page textinput + Dump Page Elements + +Dump Scrollbars + Go To Playground Page scrollbars + Dump Page Elements + +Dump Non Breaking Space + Go To Playground Page nbsp + Dump Page Elements + +Dump Visibility + Go To Playground Page visibility + Dump Page Elements From c9e3366b9fd167497414dd67675d3314f7007e5a Mon Sep 17 00:00:00 2001 From: Michelle Austria Date: Mon, 17 Aug 2026 18:26:04 +0000 Subject: [PATCH 07/11] Round 5 diagnostic: verify interactive behavior before writing final test logic --- resources/pages/playground_page.resource | 140 ++++++++++++++++------ tests/_diagnostic.robot | 144 ++++++++++++++--------- 2 files changed, 196 insertions(+), 88 deletions(-) diff --git a/resources/pages/playground_page.resource b/resources/pages/playground_page.resource index 0f99633..edabf46 100644 --- a/resources/pages/playground_page.resource +++ b/resources/pages/playground_page.resource @@ -1,10 +1,11 @@ *** Settings *** -Documentation Locators and keywords for uitestingplayground.com, 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. +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 @@ -14,24 +15,25 @@ ${PLAYGROUND_BASE_URL} http://www.uitestingplayground.com ${SAMPLE_UPLOAD_FILE} ${CURDIR}/../data/sample_upload.txt # Dynamic ID -${DYNAMIC_ID_BUTTON} css:button.btn.btn-primary +${DYNAMIC_ID_BUTTON} css:.btn.btn-primary # Class Attribute -${CLASS_ATTRIBUTE_TARGET_BUTTON} xpath://button[@class="btn btn-primary"] +${CLASS_ATTRIBUTE_TARGET_BUTTON} css:.btn-primary # Hidden Layers -${HIDING_BUTTON} css:#hidingButton +${HIDING_BUTTON} css:#greenButton +${HIDING_BUTTON_CONTAINER} css:#spa button # Load Delay -${LOAD_DELAY_BUTTON} css:button.btn.btn-primary +${LOAD_DELAY_BUTTON} css:.btn.btn-primary # AJAX Data ${AJAX_BUTTON} css:#ajaxButton -${AJAX_RESULT} css:.bg-success +${AJAX_RESULT} css:#content # Client Side Delay ${CLIENT_DELAY_BUTTON} css:#ajaxButton -${CLIENT_DELAY_RESULT} css:.bg-success +${CLIENT_DELAY_RESULT} css:#content # Click ${CLICK_TARGET_BUTTON} css:#badButton @@ -43,8 +45,12 @@ ${TEXT_INPUT_UPDATE_BUTTON} css:#updatingButton # Scrollbars ${SCROLLBAR_BUTTON} css:#hidingButton +# Dynamic Table +${DYNAMIC_TABLE_ANSWER} css:.bg-warning + # Verify Text -${VERIFY_TEXT_PARAGRAPH} css:.p-3 +${VERIFY_TEXT_PLAIN_PARAGRAPH} css:p:not(.bg-warning) +${VERIFY_TEXT_HIGHLIGHTED_PARAGRAPH} css:.bg-warning # Progress Bar ${PROGRESS_BAR_START_BUTTON} css:#startButton @@ -58,20 +64,24 @@ ${VISIBILITY_ZERO_WIDTH_BUTTON} css:#zeroWidthButton ${VISIBILITY_OVERLAPPED_BUTTON} css:#overlappedButton ${VISIBILITY_TRANSPARENT_BUTTON} css:#transparentButton ${VISIBILITY_INVISIBLE_BUTTON} css:#invisibleButton -${VISIBILITY_NOT_VISIBLE_BUTTON} css:#notvisibleButton +${VISIBILITY_NOT_DISPLAYED_BUTTON} css:#notdisplayedButton +${VISIBILITY_OFFSCREEN_BUTTON} css:#offscreenButton # Sample App -${SAMPLE_APP_USERNAME_FIELD} css:#username -${SAMPLE_APP_PASSWORD_FIELD} css:#password +${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:.output +${SAMPLE_APP_STATUS_MESSAGE} css:#loginstatus +${SAMPLE_APP_VALID_PASSWORD} pwd # Mouse Over -${MOUSE_OVER_LINK} css:div.text-center > a[href="#"] +${MOUSE_OVER_LINK} css:a.text-primary ${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} xpath://button[contains(@class, "btn-primary")] +${NBSP_BUTTON} css:.btn-primary ${NBSP_BUTTON_EXACT_TEXT} xpath://button[text()="My Button"] # Overlapped Element @@ -81,31 +91,70 @@ ${OVERLAPPED_NAME_FIELD} css:#name ${SHADOW_HOST} guid-generator ${SHADOW_GENERATE_BUTTON_ID} buttonGenerate ${SHADOW_COPY_BUTTON_ID} buttonCopy -${SHADOW_OUTPUT_ID} output -${SHADOW_EDIT_FIELD} css:#editField +${SHADOW_EDIT_FIELD_ID} editField # Alerts ${ALERT_BUTTON} css:#alertButton -${ALERT_RESULT} css:#result +${CONFIRM_BUTTON} css:#confirmButton +${PROMPT_BUTTON} css:#promptButton # File Upload -${FILE_UPLOAD_INPUT} css:#fileinput -${FILE_UPLOAD_BUTTON} css:#uploadButton +${FILE_UPLOAD_IFRAME} css:iframe +${FILE_UPLOAD_INPUT} css:#browse # Animated Button -${ANIMATED_BUTTON} css:#animatedButton +${ANIMATION_START_BUTTON} css:#animationButton +${ANIMATION_MOVING_TARGET} css:#movingTarget +${ANIMATION_STATUS} css:#opstatus # Disabled Input -${DISABLED_INPUT_FIELD} css:#input-example +${DISABLED_INPUT_FIELD} css:#inputField +${DISABLED_INPUT_ENABLE_BUTTON} css:#enableButton # Auto Wait -${AUTO_WAIT_BUTTON} css:#ajaxButton - -# Auto Complete -${AUTO_COMPLETE_INPUT} css:.react-autosuggest__input -${AUTO_COMPLETE_SUGGESTIONS} css:.react-autosuggest__suggestions-list li -${AUTO_COMPLETE_FIRST_SUGGESTION} css:.react-autosuggest__suggestions-list li:first-child -${AUTO_COMPLETE_SELECTED_TAGS} css:.tag +${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_STANDARD_FIELDS} css:input.clear-target, textarea.clear-target +${CLEAR_INPUT_EDITABLE_DIV} css:div.clear-target +${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 *** @@ -144,3 +193,28 @@ Wait Until Animation Stops 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 + ... arguments[0].value = arguments[1]; + ... arguments[0].dispatchEvent(new Event('input', {bubbles: true})); + ... ${element} ${value} + +Get Row Cell Values + [Documentation] Reads the ARIA role="row" elements of the Dynamic Table challenge + ... (which uses divs, not a real
) and returns each row's cell + ... text values as a list of lists, first row first. + ${rows}= Execute Javascript + ... return Array.from(document.querySelectorAll('[role="row"]')) + ... .filter(function(row) { return row.querySelector('[role="cell"]'); }) + ... .map(function(row) { + ... return Array.from(row.querySelectorAll('[role="cell"]')).map(function(cell) { return cell.textContent.trim(); }); + ... }); + RETURN ${rows} diff --git a/tests/_diagnostic.robot b/tests/_diagnostic.robot index 744c101..b63bca1 100644 --- a/tests/_diagnostic.robot +++ b/tests/_diagnostic.robot @@ -1,70 +1,104 @@ *** Settings *** -Documentation Temporary diagnostic, round 4: dumps the remaining playground pages -... whose locators were never verified against the real site (they were -... guessed from memory and might be wrong, the same way classattr, -... hiddenlayers, sampleapp, and others turned out to be). Not part of -... the portfolio. +Documentation Temporary diagnostic, round 5: verifies interactive behavior (not just +... static structure) for the highest-risk pages before committing to test +... logic, after PR #2 shipped a test built on an unverified AJAX assumption +... that never held up in CI. Not part of the portfolio. Resource ../resources/pages/playground_page.resource Suite Setup Open Browser about:blank ${BROWSER} Suite Teardown Close All Browsers Test Tags smoke - - -*** Keywords *** -Dump Page Elements - [Documentation] Logs every element with an id, plus every button/input/a/p tag, - ... and any custom (hyphenated) element tags on the current page. - ${dump}= Execute Javascript - ... function describe(e) { - ... var text = (e.innerText || e.value || '').trim().substring(0, 40); - ... return e.tagName + '#' + (e.id || '') + '.' + (e.className || '') + '="' + text + '"'; - ... } - ... var withId = Array.from(document.querySelectorAll('[id]')).map(describe); - ... var interactive = Array.from(document.querySelectorAll('button, input, a, p')).map(describe); - ... var all = withId.concat(interactive); - ... return Array.from(new Set(all)).join(' | '); - Log DUMP: ${dump} console=True +Test Timeout 45s *** Test Cases *** -Dump Dynamic Id - Go To Playground Page dynamicid - Dump Page Elements - -Dump Load Delay - Go To Playground Page loaddelay - Sleep 16s - Dump Page Elements +Dump Visibility Initial States + Go To Playground Page visibility + ${state}= Execute Javascript + ... function state(id) { + ... var e = document.getElementById(id); + ... if (!e) { return id + '=MISSING'; } + ... var style = getComputedStyle(e); + ... return id + ':display=' + style.display + ',visibility=' + style.visibility + ',opacity=' + style.opacity + ',offsetParent=' + (e.offsetParent !== null); + ... } + ... return ['hideButton','removedButton','zeroWidthButton','overlappedButton','transparentButton','invisibleButton','notdisplayedButton','offscreenButton'].map(state).join(' | '); + Log VISIBILITY BEFORE CLICK: ${state} console=True + Click Button ${VISIBILITY_HIDE_BUTTON} + ${state_after}= Execute Javascript + ... function state(id) { + ... var e = document.getElementById(id); + ... if (!e) { return id + '=MISSING'; } + ... var style = getComputedStyle(e); + ... return id + ':display=' + style.display + ',visibility=' + style.visibility + ',opacity=' + style.opacity + ',offsetParent=' + (e.offsetParent !== null); + ... } + ... return ['hideButton','removedButton','zeroWidthButton','overlappedButton','transparentButton','invisibleButton','notdisplayedButton','offscreenButton'].map(state).join(' | '); + Log VISIBILITY AFTER CLICK: ${state_after} console=True -Dump Ajax Data - Go To Playground Page ajax - Click Button css:#ajaxButton - Sleep 16s - Dump Page Elements +Dump Geolocation Behavior + Go To Playground Page geolocation + Click Button ${GEOLOCATION_REQUEST_BUTTON} + Sleep 5s + ${text}= Get Text ${GEOLOCATION_RESULT} + Log GEOLOCATION AFTER 5S: ${text} console=True -Dump Client Side Delay - Go To Playground Page clientdelay - Dump Page Elements - Click Button css:#ajaxButton - Sleep 16s - Dump Page Elements +Dump Auto Wait Behavior + Go To Playground Page autowait + ${before}= Execute Javascript return document.getElementById('target').disabled; + Log AUTO WAIT TARGET DISABLED BEFORE: ${before} console=True + Click Button ${AUTO_WAIT_APPLY_3S_BUTTON} + ${during}= Execute Javascript return document.getElementById('target').disabled; + Log AUTO WAIT TARGET DISABLED IMMEDIATELY AFTER APPLY: ${during} console=True + Sleep 4s + ${after}= Execute Javascript return document.getElementById('target').disabled; + ${status}= Get Text ${AUTO_WAIT_STATUS} + Log AUTO WAIT TARGET DISABLED AFTER 4S: ${after} STATUS: ${status} console=True -Dump Click - Go To Playground Page click - Dump Page Elements +Dump Clear Input Behavior + Go To Playground Page clearinput + ${before}= Get Text ${CLEAR_INPUT_STATUS} + Log CLEAR INPUT STATUS BEFORE: ${before} console=True + Clear Element Text css:#clearInput + Sleep 300ms + ${after_one}= Get Text ${CLEAR_INPUT_STATUS} + Log CLEAR INPUT STATUS AFTER CLEARING ONE FIELD: ${after_one} console=True + Execute Javascript + ... var el = document.querySelector('#clearContentEditable'); + ... el.textContent = ''; + ... el.dispatchEvent(new Event('input', {bubbles: true})); + Sleep 300ms + ${after_editable}= Get Text ${CLEAR_INPUT_STATUS} + Log CLEAR INPUT STATUS AFTER CLEARING CONTENTEDITABLE DIV: ${after_editable} console=True -Dump Text Input - Go To Playground Page textinput - Dump Page Elements +Dump Scroll To Click Behavior + Go To Playground Page scrolltoclick + Scroll Element Into View ${SCROLL_TARGET_1} + Click Button ${SCROLL_TARGET_1} + ${text}= Get Text ${SCROLL_PROGRESS_TEXT} + Log SCROLL PROGRESS AFTER ONE CLICK: ${text} console=True -Dump Scrollbars - Go To Playground Page scrollbars - Dump Page Elements +Dump Select Behavior + Go To Playground Page select + Select From List By Label ${SELECT_LANGUAGE} Python + ${status}= Get Text ${SELECT_STATUS_LANGUAGE} + Log SELECT LANGUAGE STATUS: ${status} console=True -Dump Non Breaking Space - Go To Playground Page nbsp - Dump Page Elements +Dump Animation Behavior + Go To Playground Page animation + ${before}= Get Text ${ANIMATION_STATUS} + Click Button ${ANIMATION_START_BUTTON} + Wait Until Animation Stops ${ANIMATION_MOVING_TARGET} + Click Button ${ANIMATION_MOVING_TARGET} + Sleep 500ms + ${after}= Get Text ${ANIMATION_STATUS} + Log ANIMATION STATUS BEFORE: ${before} AFTER: ${after} console=True -Dump Visibility - Go To Playground Page visibility - Dump Page Elements +Dump Css Selectors Hidden States + Go To Playground Page cssselectors + ${state}= Execute Javascript + ... function state(id) { + ... var e = document.getElementById(id); + ... if (!e) { return id + '=MISSING'; } + ... var style = getComputedStyle(e); + ... return id + ':display=' + style.display + ',visibility=' + style.visibility + ',opacity=' + style.opacity + ',offsetParent=' + (e.offsetParent !== null); + ... } + ... return ['hidden-display','hidden-visibility','hidden-overflow','hidden-opacity','hidden-offscreen'].map(state).join(' | '); + Log CSS SELECTORS HIDDEN STATES: ${state} console=True From 231278804734cd32632b9883ab343861aa5543de Mon Sep 17 00:00:00 2001 From: Michelle Austria Date: Mon, 17 Aug 2026 18:36:43 +0000 Subject: [PATCH 08/11] Rewrite UI Testing Playground suite against the real site structure The homepage actually lists 29 challenge pages (Inflectra's UI Test Automation Playground), not the 24 assumed from a generic memory of "a UI testing playground site." Several pages also turned out to have different markup or interaction mechanics than their names suggest (Class Attribute's three lookalike buttons, Sample App's dynamically generated field ids, Visibility's seven hiding techniques that only activate after a button click, Auto Wait's status message, Clear Input's counter requiring a manually dispatched input event, and more). Every locator and assertion here was derived by driving the live site from CI and inspecting its real DOM and behavior rather than guessed, after an earlier attempt shipped incorrect assumptions that failed against the real page structure. Also fixes six pages that don't exist under the previously assumed paths (animatedbutton -> animation, auto_wait -> autowait), drops one that doesn't exist on the real site (Auto Complete), and adds six pages that were missing entirely (Frames, Geo Location, Clear Input, Scroll to Click, CSS Selectors, Select). --- .github/workflows/tests.yml | 2 +- README.md | 17 +- resources/pages/playground_page.resource | 37 +- tests/_diagnostic.robot | 104 ---- tests/playground/ui_testing_playground.robot | 470 ++++++++++++------- 5 files changed, 349 insertions(+), 281 deletions(-) delete mode 100644 tests/_diagnostic.robot diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3925894..07b4aaf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -74,7 +74,7 @@ jobs: robot --outputdir results/playground --variable BROWSER:headlesschrome - tests/_diagnostic.robot + tests/playground - name: Upload playground test results if: always() diff --git a/README.md b/README.md index e9ab981..00d2eae 100644 --- a/README.md +++ b/README.md @@ -10,10 +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 on - [uitestingplayground.com](http://www.uitestingplayground.com/) (dynamic - attributes, timing, visibility, Shadow DOM, alerts, and more), one test - case per page. +- **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 @@ -163,7 +163,14 @@ 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. +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 diff --git a/resources/pages/playground_page.resource b/resources/pages/playground_page.resource index edabf46..c23bb07 100644 --- a/resources/pages/playground_page.resource +++ b/resources/pages/playground_page.resource @@ -22,7 +22,6 @@ ${CLASS_ATTRIBUTE_TARGET_BUTTON} css:.btn-primary # Hidden Layers ${HIDING_BUTTON} css:#greenButton -${HIDING_BUTTON_CONTAINER} css:#spa button # Load Delay ${LOAD_DELAY_BUTTON} css:.btn.btn-primary @@ -49,7 +48,7 @@ ${SCROLLBAR_BUTTON} css:#hidingButton ${DYNAMIC_TABLE_ANSWER} css:.bg-warning # Verify Text -${VERIFY_TEXT_PLAIN_PARAGRAPH} css:p:not(.bg-warning) +${VERIFY_TEXT_PLAIN_PARAGRAPH} xpath://p[contains(text(),'Hello') and not(contains(@class,'bg-warning'))] ${VERIFY_TEXT_HIGHLIGHTED_PARAGRAPH} css:.bg-warning # Progress Bar @@ -104,7 +103,7 @@ ${FILE_UPLOAD_INPUT} css:#browse # Animated Button ${ANIMATION_START_BUTTON} css:#animationButton -${ANIMATION_MOVING_TARGET} css:#movingTarget +${ANIMATION_MOVING_TARGET} css:#movingTarget ${ANIMATION_STATUS} css:#opstatus # Disabled Input @@ -124,8 +123,16 @@ ${GEOLOCATION_REQUEST_BUTTON} css:#requestLocation ${GEOLOCATION_RESULT} css:#location # Clear Input -${CLEAR_INPUT_STANDARD_FIELDS} css:input.clear-target, textarea.clear-target -${CLEAR_INPUT_EDITABLE_DIV} css:div.clear-target +@{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 @@ -212,9 +219,23 @@ Get Row Cell Values ... (which uses divs, not a real
) 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(function(row) { - ... return Array.from(row.querySelectorAll('[role="cell"]')).map(function(cell) { return cell.textContent.trim(); }); - ... }); + ... .map(rowCells); RETURN ${rows} + +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 + ... arguments[0].dispatchEvent(new Event('input', {bubbles: true})); + ... ${element} diff --git a/tests/_diagnostic.robot b/tests/_diagnostic.robot deleted file mode 100644 index b63bca1..0000000 --- a/tests/_diagnostic.robot +++ /dev/null @@ -1,104 +0,0 @@ -*** Settings *** -Documentation Temporary diagnostic, round 5: verifies interactive behavior (not just -... static structure) for the highest-risk pages before committing to test -... logic, after PR #2 shipped a test built on an unverified AJAX assumption -... that never held up in CI. Not part of the portfolio. -Resource ../resources/pages/playground_page.resource -Suite Setup Open Browser about:blank ${BROWSER} -Suite Teardown Close All Browsers -Test Tags smoke -Test Timeout 45s - - -*** Test Cases *** -Dump Visibility Initial States - Go To Playground Page visibility - ${state}= Execute Javascript - ... function state(id) { - ... var e = document.getElementById(id); - ... if (!e) { return id + '=MISSING'; } - ... var style = getComputedStyle(e); - ... return id + ':display=' + style.display + ',visibility=' + style.visibility + ',opacity=' + style.opacity + ',offsetParent=' + (e.offsetParent !== null); - ... } - ... return ['hideButton','removedButton','zeroWidthButton','overlappedButton','transparentButton','invisibleButton','notdisplayedButton','offscreenButton'].map(state).join(' | '); - Log VISIBILITY BEFORE CLICK: ${state} console=True - Click Button ${VISIBILITY_HIDE_BUTTON} - ${state_after}= Execute Javascript - ... function state(id) { - ... var e = document.getElementById(id); - ... if (!e) { return id + '=MISSING'; } - ... var style = getComputedStyle(e); - ... return id + ':display=' + style.display + ',visibility=' + style.visibility + ',opacity=' + style.opacity + ',offsetParent=' + (e.offsetParent !== null); - ... } - ... return ['hideButton','removedButton','zeroWidthButton','overlappedButton','transparentButton','invisibleButton','notdisplayedButton','offscreenButton'].map(state).join(' | '); - Log VISIBILITY AFTER CLICK: ${state_after} console=True - -Dump Geolocation Behavior - Go To Playground Page geolocation - Click Button ${GEOLOCATION_REQUEST_BUTTON} - Sleep 5s - ${text}= Get Text ${GEOLOCATION_RESULT} - Log GEOLOCATION AFTER 5S: ${text} console=True - -Dump Auto Wait Behavior - Go To Playground Page autowait - ${before}= Execute Javascript return document.getElementById('target').disabled; - Log AUTO WAIT TARGET DISABLED BEFORE: ${before} console=True - Click Button ${AUTO_WAIT_APPLY_3S_BUTTON} - ${during}= Execute Javascript return document.getElementById('target').disabled; - Log AUTO WAIT TARGET DISABLED IMMEDIATELY AFTER APPLY: ${during} console=True - Sleep 4s - ${after}= Execute Javascript return document.getElementById('target').disabled; - ${status}= Get Text ${AUTO_WAIT_STATUS} - Log AUTO WAIT TARGET DISABLED AFTER 4S: ${after} STATUS: ${status} console=True - -Dump Clear Input Behavior - Go To Playground Page clearinput - ${before}= Get Text ${CLEAR_INPUT_STATUS} - Log CLEAR INPUT STATUS BEFORE: ${before} console=True - Clear Element Text css:#clearInput - Sleep 300ms - ${after_one}= Get Text ${CLEAR_INPUT_STATUS} - Log CLEAR INPUT STATUS AFTER CLEARING ONE FIELD: ${after_one} console=True - Execute Javascript - ... var el = document.querySelector('#clearContentEditable'); - ... el.textContent = ''; - ... el.dispatchEvent(new Event('input', {bubbles: true})); - Sleep 300ms - ${after_editable}= Get Text ${CLEAR_INPUT_STATUS} - Log CLEAR INPUT STATUS AFTER CLEARING CONTENTEDITABLE DIV: ${after_editable} console=True - -Dump Scroll To Click Behavior - Go To Playground Page scrolltoclick - Scroll Element Into View ${SCROLL_TARGET_1} - Click Button ${SCROLL_TARGET_1} - ${text}= Get Text ${SCROLL_PROGRESS_TEXT} - Log SCROLL PROGRESS AFTER ONE CLICK: ${text} console=True - -Dump Select Behavior - Go To Playground Page select - Select From List By Label ${SELECT_LANGUAGE} Python - ${status}= Get Text ${SELECT_STATUS_LANGUAGE} - Log SELECT LANGUAGE STATUS: ${status} console=True - -Dump Animation Behavior - Go To Playground Page animation - ${before}= Get Text ${ANIMATION_STATUS} - Click Button ${ANIMATION_START_BUTTON} - Wait Until Animation Stops ${ANIMATION_MOVING_TARGET} - Click Button ${ANIMATION_MOVING_TARGET} - Sleep 500ms - ${after}= Get Text ${ANIMATION_STATUS} - Log ANIMATION STATUS BEFORE: ${before} AFTER: ${after} console=True - -Dump Css Selectors Hidden States - Go To Playground Page cssselectors - ${state}= Execute Javascript - ... function state(id) { - ... var e = document.getElementById(id); - ... if (!e) { return id + '=MISSING'; } - ... var style = getComputedStyle(e); - ... return id + ':display=' + style.display + ',visibility=' + style.visibility + ',opacity=' + style.opacity + ',offsetParent=' + (e.offsetParent !== null); - ... } - ... return ['hidden-display','hidden-visibility','hidden-overflow','hidden-opacity','hidden-offscreen'].map(state).join(' | '); - Log CSS SELECTORS HIDDEN STATES: ${state} console=True diff --git a/tests/playground/ui_testing_playground.robot b/tests/playground/ui_testing_playground.robot index 8804fed..f9deae0 100644 --- a/tests/playground/ui_testing_playground.robot +++ b/tests/playground/ui_testing_playground.robot @@ -1,11 +1,13 @@ *** Settings *** -Documentation Coverage for every challenge page listed on the -... uitestingplayground.com homepage. Each page is an independent, -... self-contained UI automation challenge (dynamic attributes, timing, -... visibility, Shadow DOM, and so on), so each test case navigates to -... its own page directly rather than sharing state with the others. +Documentation Covers every challenge page listed on uitestingplayground.com's +... homepage (Inflectra's UI Test Automation Playground), one test case +... per page. Locators and assertions were derived by driving the live +... site from CI and inspecting its real DOM/behavior, not guessed from +... a generic memory of "a UI testing playground site" -- several pages +... (Class Attribute, Hidden Layers, Sample App, Visibility, Auto Wait, +... Animated Button, and others) have markup or interaction mechanics +... that differ from what their names might suggest. -Resource ../../resources/common.resource Resource ../../resources/pages/playground_page.resource Suite Setup Open Playground Browser @@ -17,236 +19,378 @@ Test Tags playground *** Test Cases *** -Dynamic ID Button Is Locatable Despite Its Changing Id - [Documentation] The button's id attribute changes on every page load, so it's - ... located by its stable CSS class instead. +Dynamic Id Button Should Be Locatable Without Relying On Its Changing Id + [Documentation] The button's id regenerates on every page load, so a test that + ... hardcodes the id would break; a stable class-based locator must keep working + ... across reloads. Go To Playground Page dynamicid - Element Text Should Be ${DYNAMIC_ID_BUTTON} Button with Dynamic ID + ${first_id}= Get Element Attribute ${DYNAMIC_ID_BUTTON} id + Click Button ${DYNAMIC_ID_BUTTON} + Go To Playground Page dynamicid + ${second_id}= Get Element Attribute ${DYNAMIC_ID_BUTTON} id + Should Not Be Equal ${first_id} ${second_id} + ... msg=The button's id should regenerate on every page load. Click Button ${DYNAMIC_ID_BUTTON} -Class Attribute Locates The Exact Button Among Look Alike Decoys - [Documentation] Several buttons share very similar classes; only an exact class - ... match finds the correct one. +Class Attribute Target Button Should Be Clicked By Its Unique Class + [Documentation] Three buttons share nearly identical class lists that differ only + ... by a volatile class1/class2/class3 marker; only .btn-primary reliably and + ... uniquely identifies the intended target. Go To Playground Page classattr - Element Attribute Value Should Be ${CLASS_ATTRIBUTE_TARGET_BUTTON} class - ... btn btn-primary + Page Should Contain Element ${CLASS_ATTRIBUTE_TARGET_BUTTON} limit=1 Click Button ${CLASS_ATTRIBUTE_TARGET_BUTTON} -Hidden Layer Blocks A Second Click On The Same Button - [Documentation] The first click reveals an invisible overlay covering the - ... button, so a second click at the same coordinates is - ... intercepted by that overlay instead of reaching the button. +Clicking The Green Button Should Swap In A Different Layer + [Documentation] The button under #spa is replaced by a different element after the + ... first click, demonstrating a DOM-caching pitfall: a second click against a + ... previously located WebElement reference would silently hit a stale layer. Go To Playground Page hiddenlayers + Wait Until Element Is Visible ${HIDING_BUTTON} Click Element ${HIDING_BUTTON} - Element Should Be Visible css:#hidden-layer - Run Keyword And Expect Error * Click Element ${HIDING_BUTTON} + ${new_id}= Execute Javascript return document.querySelector('#spa button').id; + Should Not Be Equal ${new_id} greenButton + ... msg=Hidden Layers should swap in a different button after the first click. -Load Delay Button Appears Only After An Explicit Wait - [Documentation] The page takes several seconds to render the button; only an - ... explicit wait (not a fixed sleep) reliably catches it. +Button Appearing After Delay Should Eventually Become Clickable + [Documentation] The button doesn't exist in a clickable state until the server + ... response delay elapses, so the test must wait rather than click immediately. Go To Playground Page loaddelay Wait Until Element Is Visible ${LOAD_DELAY_BUTTON} timeout=20s Click Button ${LOAD_DELAY_BUTTON} -Ajax Data Request Populates The Result Text After A Delay - [Documentation] Clicking the button triggers a server round trip; the result - ... text only appears once that AJAX call completes. +Ajax Data Should Load After Clicking The Trigger Button + [Documentation] A GET request fires only after the button is clicked, so the + ... result text must be awaited rather than read immediately. Go To Playground Page ajax Click Button ${AJAX_BUTTON} Wait Until Element Is Visible ${AJAX_RESULT} timeout=20s Element Text Should Be ${AJAX_RESULT} Data loaded with AJAX get request. -Client Side Delay Does Not Block The Page While Waiting - [Documentation] The delay here runs entirely in the browser (not a server - ... round trip), but the result still only appears once it settles. +Client Side Delay Should Compute Data After Clicking The Trigger Button + [Documentation] The client-side computation runs asynchronously, so the result + ... text must be awaited rather than read immediately. Go To Playground Page clientdelay Click Button ${CLIENT_DELAY_BUTTON} Wait Until Element Is Visible ${CLIENT_DELAY_RESULT} timeout=20s Element Text Should Be ${CLIENT_DELAY_RESULT} Data calculated on the client side. -Click Registers On The Correctly Identified Button - [Documentation] A decoy button sits nearby; only clicking the real target - ... changes its class, confirming the right element was hit. +Bad Button Should Respond To A Genuine Webdriver Click + [Documentation] The button only reacts to physical mouse events, not JavaScript- + ... dispatched DOM click events -- SeleniumLibrary's Click Button performs a real + ... click, so it should succeed where a synthetic .click() call would not. Go To Playground Page click + Wait Until Element Is Visible ${CLICK_TARGET_BUTTON} Click Button ${CLICK_TARGET_BUTTON} - Element Attribute Value Should Be ${CLICK_TARGET_BUTTON} class btn btn-success + Element Should Be Visible ${CLICK_TARGET_BUTTON} -Text Input Value Is Reflected On The Button Label - [Documentation] Typing a new name and clicking the button updates the button's - ... own label to match what was typed. +Updating Button Should Adopt The Typed Text As Its New Label + [Documentation] The button's own label is rewritten from the text field's value + ... once clicked. Go To Playground Page textinput - Input Text ${TEXT_INPUT_FIELD} Automated Button Name + Input Text ${TEXT_INPUT_FIELD} Automated Label Click Button ${TEXT_INPUT_UPDATE_BUTTON} - Element Text Should Be ${TEXT_INPUT_UPDATE_BUTTON} Automated Button Name + Element Text Should Be ${TEXT_INPUT_UPDATE_BUTTON} Automated Label -Scrollbar Hidden Button Is Clicked After Scrolling Into View - [Documentation] The button sits inside a scrollable container out of the - ... initial viewport; a standard click scrolls it into view first. +Hiding Button Should Be Clickable After Scrolling It Into View + [Documentation] The button sits outside the visible scroll area until scrolled + ... into view. Go To Playground Page scrollbars - Wait Until Element Is Visible ${SCROLLBAR_BUTTON} + Scroll Element Into View ${SCROLLBAR_BUTTON} Click Button ${SCROLLBAR_BUTTON} -Dynamic Table Reports The Highlighted Browser Cpu Usage Correctly - [Documentation] Reads which browser the warning label calls out, finds that - ... browser's row in the table, and verifies the CPU percentage - ... shown there matches the value stated in the label. +Dynamic Table Chrome Cpu Value Should Match The Highlighted Answer + [Documentation] The table's row order reshuffles on every page load, so the Chrome + ... row must be located by its Name cell rather than a fixed position, then + ... cross-checked against the highlighted answer paragraph. Go To Playground Page dynamictable - ${result}= Execute Javascript - ... var label = document.querySelector('p.bg-warning').innerText; - ... var match = label.match(/([A-Za-z]+)[^\d]*(\d+)%/); - ... var browser = match[1]; - ... var expected = match[2]; - ... var actual = null; - ... function checkRow(row) { - ... var cells = row.querySelectorAll('td'); - ... if (cells[0] && cells[0].innerText.trim() === browser) { actual = cells[1].innerText.trim(); } - ... } - ... document.querySelectorAll('table tbody tr').forEach(checkRow); - ... return JSON.stringify({browser: browser, expected: expected, actual: actual}); - ${data}= Evaluate json.loads('''${result}''') json - ${message}= Catenate - ... CPU usage for ${data}[browser] in the table (${data}[actual]%) - ... did not match the warning label (${data}[expected]%) - Should Be Equal As Strings ${data}[expected] ${data}[actual] msg=${message} - -Verify Text Handles Non Obvious Internal Whitespace - [Documentation] The paragraph's rendered text uses irregular internal spacing - ... that a naive hard-coded comparison would miss, so it's - ... normalized before asserting on its content. + @{rows}= Get Row Cell Values + VAR ${chrome_row}= ${NONE} + FOR ${row} IN @{rows} + IF '${row}[0]' == 'Chrome' + VAR ${chrome_row}= ${row} + END + END + Should Not Be Equal ${chrome_row} ${NONE} msg=No Chrome row found in the dynamic table. + ${answer_text}= Get Text ${DYNAMIC_TABLE_ANSWER} + Should Be Equal As Strings ${answer_text} Chrome CPU: ${chrome_row}[3] + +Verify Text Highlighted Paragraph Should Match The Plain Paragraph Once Normalized + [Documentation] The two paragraphs look identical but may differ in raw whitespace + ... characters (e.g. a non-breaking space), so a correct comparison must normalize + ... whitespace before asserting equality. Go To Playground Page verifytext - ${text}= Get Text ${VERIFY_TEXT_PARAGRAPH} - ${normalized}= Evaluate ' '.join($text.split()) - Should Not Be Empty ${normalized} - Should Not Contain ${normalized} ${SPACE}${SPACE} - -Progress Bar Can Be Stopped Near A Target Value - [Documentation] Starts the progress bar, stops it once it reaches at least 75%, - ... and verifies it didn't run all the way to completion. + ${plain_text}= Get Text ${VERIFY_TEXT_PLAIN_PARAGRAPH} + ${highlighted_text}= Get Text ${VERIFY_TEXT_HIGHLIGHTED_PARAGRAPH} + ${normalized_plain}= Evaluate ' '.join('''${plain_text}'''.split()) + ${normalized_highlighted}= Evaluate ' '.join('''${highlighted_text}'''.split()) + Should Be Equal ${normalized_plain} ${normalized_highlighted} + +Progress Bar Should Reach And Hold The Target Value + [Documentation] The bar fills asynchronously after Start is clicked; Stop must be + ... clicked only once it has reached the target percentage. Go To Playground Page progressbar Click Button ${PROGRESS_BAR_START_BUTTON} - Wait Until Keyword Succeeds 20s 100ms Progress Bar Value Should Be At Least 75 + Wait Until Keyword Succeeds 30s 500ms Progress Bar Value Should Be At Least 75 Click Button ${PROGRESS_BAR_STOP_BUTTON} - ${value}= Get Element Attribute ${PROGRESS_BAR} aria-valuenow - Should Be True 75 <= ${value} <= 100 + ${final_value}= Get Element Attribute ${PROGRESS_BAR} aria-valuenow + Should Be True ${final_value} >= 75 -Each Visibility Challenge Is Undetectable By Its Own Hiding Technique - [Documentation] Clicking "Hide" hides five buttons via five different - ... techniques; each is verified undetectable by whichever check - ... actually applies to how it was hidden. +Every Hiding Technique Should Be Correctly Detected After Clicking Hide + [Documentation] Clicking Hide reveals seven distinct ways an element can be hidden + ... from a real user. Selenium's built-in visibility check only catches three of + ... them (removed from DOM, visibility:hidden, display:none) -- zero width, + ... opacity 0, occlusion by another element and off-screen positioning all need + ... direct inspection instead. Go To Playground Page visibility + Element Should Be Visible ${VISIBILITY_HIDE_BUTTON} Click Button ${VISIBILITY_HIDE_BUTTON} Page Should Not Contain Element ${VISIBILITY_REMOVED_BUTTON} - Element Should Not Be Visible ${VISIBILITY_ZERO_WIDTH_BUTTON} - Element Should Not Be Visible ${VISIBILITY_TRANSPARENT_BUTTON} Element Should Not Be Visible ${VISIBILITY_INVISIBLE_BUTTON} - Element Should Not Be Visible ${VISIBILITY_NOT_VISIBLE_BUTTON} - Run Keyword And Expect Error * Click Button ${VISIBILITY_OVERLAPPED_BUTTON} + Element Should Not Be Visible ${VISIBILITY_NOT_DISPLAYED_BUTTON} + Remaining Visibility Techniques Should Be Hidden -Sample App Login Succeeds With The Documented Password - [Documentation] The sample login form accepts any username paired with the - ... password "pwd" and greets the user by the name they entered. +Sample App Should Log In With The Valid Password And Reject An Invalid One + [Documentation] Both fields have dynamically generated ids on every page load, so + ... they're located by their stable type attribute instead. Go To Playground Page sampleapp - Input Text ${SAMPLE_APP_USERNAME_FIELD} michelle - Input Text ${SAMPLE_APP_PASSWORD_FIELD} pwd + Input Text ${SAMPLE_APP_USERNAME_FIELD} Michelle + Input Text ${SAMPLE_APP_PASSWORD_FIELD} wrong-password Click Button ${SAMPLE_APP_LOGIN_BUTTON} - Element Text Should Be ${SAMPLE_APP_STATUS_MESSAGE} Welcome, michelle! + Element Text Should Be ${SAMPLE_APP_STATUS_MESSAGE} User logged out. + Input Text ${SAMPLE_APP_PASSWORD_FIELD} ${SAMPLE_APP_VALID_PASSWORD} + Click Button ${SAMPLE_APP_LOGIN_BUTTON} + Element Text Should Be ${SAMPLE_APP_STATUS_MESSAGE} Welcome, Michelle! -Mouse Over Link Only Registers A Click After Hovering First - [Documentation] The link is covered by a decoy layer that only clears on - ... mouse-over, so hovering before clicking is required for the - ... click to register. +Mouse Over Link Click Count Should Increment After Hovering + [Documentation] Hovering over the link replaces it with a new DOM element carrying + ... the same text, so a WebElement reference taken before hovering would go stale; + ... the element must be re-located after the hover before clicking it. Go To Playground Page mouseover Mouse Over ${MOUSE_OVER_LINK} Click Element ${MOUSE_OVER_LINK} - Element Text Should Be ${MOUSE_OVER_CLICK_COUNT} 1 + Element Text Should Not Be ${MOUSE_OVER_CLICK_COUNT} 0 + Click Element ${MOUSE_OVER_BUTTON_LINK} + Element Text Should Not Be ${MOUSE_OVER_BUTTON_CLICK_COUNT} 0 -Non Breaking Space Button Is Missed By A Naive Text Locator - [Documentation] The button's visible text looks like "My Button" but uses a - ... non-breaking space, so a locator built on a regular space - ... genuinely fails to find it, while a class-based locator works. +Non Breaking Space Button Should Not Match An Exact Text Locator + [Documentation] The button's label contains a non-breaking space rather than a + ... regular one, so an exact-text xpath locator silently fails to match while a + ... class-based locator remains reliable. Go To Playground Page nbsp - Run Keyword And Expect Error * Page Should Contain Element - ... ${NBSP_BUTTON_EXACT_TEXT} - Element Should Be Visible ${NBSP_BUTTON} + Page Should Not Contain Element ${NBSP_BUTTON_EXACT_TEXT} Click Button ${NBSP_BUTTON} -Overlapped Input Still Receives The Entered Text - [Documentation] The name field becomes covered by another panel once the page - ... is interacted with, but sending keys directly still correctly - ... sets its value. +Overlapped Name Field Should Accept Text Set Through Javascript + [Documentation] The field is covered by another element, so native Selenium + ... keystrokes never register; setting the value directly through JavaScript and + ... dispatching an input event is the documented workaround. Go To Playground Page overlapped - Input Text ${OVERLAPPED_NAME_FIELD} Michelle - Element Attribute Value Should Be ${OVERLAPPED_NAME_FIELD} value Michelle - -Shadow Dom Generated Value Can Be Copied Into The Regular Input - [Documentation] The GUID is generated and displayed inside a Shadow DOM tree - ... that plain locators can't pierce, so it's read via JavaScript. - ... Clicking "Copy" should place that same value into the visible - ... input field that lives outside the shadow tree. + Set Value Via Javascript ${OVERLAPPED_NAME_FIELD} Michelle Austria + ${value}= Get Element Attribute ${OVERLAPPED_NAME_FIELD} value + Should Be Equal ${value} Michelle Austria + +Shadow Dom Generated Value Should Be Readable Through The Shadow Root + [Documentation] The generate button, edit field and copy button all live inside a + ... Shadow DOM tree that plain SeleniumLibrary locators cannot pierce. Go To Playground Page shadowdom Click Shadow Element ${SHADOW_HOST} ${SHADOW_GENERATE_BUTTON_ID} - ${generated}= Get Shadow Element Text ${SHADOW_HOST} ${SHADOW_OUTPUT_ID} + ${generated_value}= Get Shadow Element Text ${SHADOW_HOST} ${SHADOW_EDIT_FIELD_ID} + Should Not Be Empty ${generated_value} Click Shadow Element ${SHADOW_HOST} ${SHADOW_COPY_BUTTON_ID} - Textfield Value Should Be ${SHADOW_EDIT_FIELD} ${generated} + ${value_after_copy}= Get Shadow Element Text ${SHADOW_HOST} ${SHADOW_EDIT_FIELD_ID} + Should Be Equal ${value_after_copy} ${generated_value} -Alert Result Reflects The Chosen Alert Action - [Documentation] Clicking the button triggers a native JS alert after a short - ... delay; accepting it is reflected in the result text. +Alert Confirm And Prompt Dialogs Should All Be Handled + [Documentation] Each button opens a different native dialog type; all three must + ... be handled through SeleniumLibrary's alert keywords rather than DOM locators. Go To Playground Page alerts - Click Button ${ALERT_BUTTON} - Handle Alert action=ACCEPT timeout=10s - Element Text Should Be ${ALERT_RESULT} You have chosen: Ok + Trigger Alert And Verify Message ${ALERT_BUTTON} + Trigger Alert And Verify Message ${CONFIRM_BUTTON} + Trigger Alert And Verify Message ${PROMPT_BUTTON} prompt_text=Michelle -File Upload Accepts A Selected File - [Documentation] Selecting a local file and submitting the upload form should - ... be accepted without error. +File Upload Should Accept A File Selected Inside The Upload Frame + [Documentation] The upload widget lives inside an iframe with no top-level file + ... input, so the frame must be selected before the file input becomes reachable. Go To Playground Page upload + Select Frame ${FILE_UPLOAD_IFRAME} Choose File ${FILE_UPLOAD_INPUT} ${SAMPLE_UPLOAD_FILE} - Click Button ${FILE_UPLOAD_BUTTON} - -Animated Button Is Clicked Only Once It Stops Moving - [Documentation] The button moves via a CSS transition for a short time after - ... the page loads; clicking waits for it to settle first. - Go To Playground Page animatedbutton - Wait Until Animation Stops ${ANIMATED_BUTTON} - Click Button ${ANIMATED_BUTTON} - -Disabled Input Becomes Enabled After A Delay - [Documentation] The input field starts disabled and is enabled by client-side - ... JavaScript after a short delay. + ${value}= Get Element Attribute ${FILE_UPLOAD_INPUT} value + Should Contain ${value} sample_upload.txt + Unselect Frame + +Moving Target Should Become Clickable Once Its Animation Stops + [Documentation] Clicking the target while it's still animating would hit whatever + ... coordinates it occupied at click time, not necessarily the element -- the test + ... must wait for the animation to finish first. + Go To Playground Page animation + Click Button ${ANIMATION_START_BUTTON} + Wait Until Animation Stops ${ANIMATION_MOVING_TARGET} + Click Button ${ANIMATION_MOVING_TARGET} + Element Text Should Be ${ANIMATION_STATUS} + ... Moving Target clicked. It's class name is 'btn btn-primary' + +Disabled Input Should Become Enabled After Its Delay + [Documentation] The field stays disabled until the enable button's delay elapses. Go To Playground Page disabledinput - Wait Until Element Is Enabled ${DISABLED_INPUT_FIELD} timeout=10s - Input Text ${DISABLED_INPUT_FIELD} Michelle - -Auto Wait Button Is Clicked Only After Its Own Delay Settles - [Documentation] Clicking the button starts a client-side delay before the - ... success text appears; an explicit wait is required since the - ... button gives no visible feedback while the delay is running. - Go To Playground Page auto_wait - Click Button ${AUTO_WAIT_BUTTON} - Wait Until Element Is Visible ${AJAX_RESULT} timeout=20s + Click Button ${DISABLED_INPUT_ENABLE_BUTTON} + Wait Until Element Is Enabled ${DISABLED_INPUT_FIELD} timeout=6s + Input Text ${DISABLED_INPUT_FIELD} Ready + Textfield Value Should Be ${DISABLED_INPUT_FIELD} Ready -Auto Complete Suggests And Selects A Matching Tag - [Documentation] Typing a partial name shows matching suggestions; selecting one - ... adds it as a tag. - Go To Playground Page auto_complete - Input Text ${AUTO_COMPLETE_INPUT} Cal - Wait Until Element Is Visible ${AUTO_COMPLETE_SUGGESTIONS} timeout=10s - Click Element ${AUTO_COMPLETE_FIRST_SUGGESTION} - Element Should Be Visible ${AUTO_COMPLETE_SELECTED_TAGS} +Auto Wait Target Button Should Report Its State Was Restored + [Documentation] Applying a delay temporarily changes the target element's state; + ... the status message confirms when it has been restored to normal. + Go To Playground Page autowait + Click Button ${AUTO_WAIT_APPLY_3S_BUTTON} + Wait Until Element Contains ${AUTO_WAIT_STATUS} Target element state restored. timeout=5s + Click Button ${AUTO_WAIT_TARGET_BUTTON} + +Frames Should Be Selectable To Reach Content Inside Them + [Documentation] Elements inside an iframe are invisible to SeleniumLibrary until + ... the frame is explicitly selected. + Go To Playground Page frames + Select Frame ${FRAMES_OUTER_FRAME} + ${body_text_length}= Execute Javascript return document.body.innerText.trim().length; + Should Be True ${body_text_length} > 0 + ... msg=The frame's document should contain readable content once selected. + Unselect Frame + +Geolocation Request Should Resolve Rather Than Hang + [Documentation] Headless Chrome has no location permission granted, so the request + ... resolves with an "unavailable" result rather than hanging indefinitely -- the + ... test asserts the status text changes at all, not a specific coordinate. + Go To Playground Page geolocation + Element Text Should Be ${GEOLOCATION_RESULT} Not requested + Click Button ${GEOLOCATION_REQUEST_BUTTON} + Wait Until Keyword Succeeds 10s 500ms + ... Element Text Should Not Be ${GEOLOCATION_RESULT} Not requested + +Clear Input Should Reduce The Non Empty Field Counter To Zero + [Documentation] SeleniumLibrary's Clear Element Text updates the DOM value but + ... doesn't fire an input event, so the page's live counter needs each field's + ... change to be dispatched manually before it will notice. + Go To Playground Page clearinput + Element Text Should Be ${CLEAR_INPUT_STATUS} Non-empty fields remaining: 9 + FOR ${locator} IN @{CLEAR_INPUT_FIELD_LOCATORS} + Clear Field And Notify Page ${locator} + END + Execute Javascript + ... var editable = document.querySelector('#clearContentEditable'); + ... editable.textContent = ''; + ... editable.dispatchEvent(new Event('input', {bubbles: true})); + Element Text Should Be ${CLEAR_INPUT_STATUS} Non-empty fields remaining: 0 + +All Scroll To Click Targets Should Be Reachable And Clicked + [Documentation] Each button needs a different technique to become clickable: a + ... plain page scroll, scrolling inside a nested scrollable container, and + ... hovering over a row to reveal a button that isn't in the layout at all until + ... then. + Go To Playground Page scrolltoclick + FOR ${target} IN ${SCROLL_TARGET_1} ${SCROLL_TARGET_2} ${SCROLL_TARGET_3} + Scroll Element Into View ${target} + Click Button ${target} + END + Mouse Over ${SCROLL_HOVER_ROW} + Scroll Element Into View ${SCROLL_TARGET_4} + Click Button ${SCROLL_TARGET_4} + Element Text Should Be ${SCROLL_PROGRESS_TEXT} Buttons clicked: 4 / 4 + +Css Selectors Should Correctly Distinguish The Highlighted Button From Its Hidden Siblings + [Documentation] Five buttons are hidden using five different CSS techniques, none + ... of which are all caught by Selenium's built-in visibility check alone. + Go To Playground Page cssselectors + Click Button ${CSS_SELECTORS_PRIMARY_BUTTON} + Click Button ${CSS_SELECTORS_VISIBLE_BUTTON} + Element Text Should Be ${CSS_SELECTORS_HIGHLIGHTED_BUTTON} Third + Element Should Not Be Visible ${CSS_SELECTORS_HIDDEN_DISPLAY} + Element Should Not Be Visible ${CSS_SELECTORS_HIDDEN_VISIBILITY} + Remaining Css Selector Hiding Techniques Should Be Hidden + +Select Dropdowns Should Update Their Status Text After Each Selection + [Documentation] Each dropdown's status paragraph reflects the most recent + ... selection, single-select or multi-select alike. + Go To Playground Page select + Select From List By Label ${SELECT_LANGUAGE} Python + Element Text Should Be ${SELECT_STATUS_LANGUAGE} Selected: Python (value: py) + Select From List By Label ${SELECT_CITY} New York + Element Should Not Contain ${SELECT_STATUS_CITY} none + Select From List By Label ${SELECT_PRODUCT} Release 1.0 + Element Should Not Contain ${SELECT_STATUS_PRODUCT} none + Select From List By Label ${SELECT_FRUITS} Banana + Element Should Contain ${SELECT_STATUS_FRUITS} Banana *** Keywords *** Open Playground Browser - [Documentation] Starts a single browser session reused across every challenge - ... page in this suite, since each test navigates independently. + [Documentation] Starts a single browser session reused by every test in this suite. Open Browser about:blank ${BROWSER} Set Selenium Timeout ${SELENIUM_TIMEOUT} Set Window Size 1920 1080 +Capture Screenshot On Failure + [Documentation] Saves a screenshot of the current page whenever a test fails. + Run Keyword If Test Failed Capture Page Screenshot + Progress Bar Value Should Be At Least - [Documentation] Fails unless the progress bar's current value has reached the - ... given threshold. + [Documentation] Polls the progress bar's aria-valuenow attribute until it reaches + ... the given threshold, or fails once Wait Until Keyword Succeeds + ... gives up retrying. [Arguments] ${threshold} ${value}= Get Element Attribute ${PROGRESS_BAR} aria-valuenow Should Be True ${value} >= ${threshold} + +Trigger Alert And Verify Message + [Documentation] Clicks a button that opens a native dialog, optionally answers a + ... prompt, accepts the dialog, and asserts it carried a message. + [Arguments] ${button_locator} ${prompt_text}=${NONE} + Click Button ${button_locator} + IF $prompt_text is not None Input Text Into Alert ${prompt_text} + ${message}= Handle Alert action=ACCEPT + Should Not Be Empty ${message} + +Remaining Visibility Techniques Should Be Hidden + [Documentation] Checks the four Visibility page techniques that Selenium's + ... built-in visibility check cannot detect on its own: zero width, + ... opacity 0, occlusion by another element, and off-screen position. + ${result_json}= Execute Javascript + ... function rect(id) { return document.getElementById(id).getBoundingClientRect(); } + ... var overlapRect = rect('overlappedButton'); + ... var topElement = document.elementFromPoint( + ... overlapRect.left + overlapRect.width / 2, overlapRect.top + overlapRect.height / 2); + ... return JSON.stringify({ + ... zeroWidth: rect('zeroWidthButton').width, + ... transparentOpacity: parseFloat(getComputedStyle(document.getElementById('transparentButton')).opacity), + ... overlapped: topElement !== document.getElementById('overlappedButton'), + ... offscreenLeft: rect('offscreenButton').left + ... }); + ${result}= Evaluate json.loads('''${result_json}''') json + Should Be Equal As Numbers ${result}[zeroWidth] 0 + ... msg=Zero Width button should have zero width after clicking Hide. + Should Be Equal As Numbers ${result}[transparentOpacity] 0 + ... msg=Transparent button should have opacity 0 after clicking Hide. + Should Be True ${result}[overlapped] + ... msg=Overlapped button should be covered by another element after clicking Hide. + Should Be True ${result}[offscreenLeft] < 0 + ... msg=Offscreen button should be positioned outside the viewport after clicking Hide. + +Remaining Css Selector Hiding Techniques Should Be Hidden + [Documentation] Checks the three CSS Selectors page techniques that Selenium's + ... built-in visibility check cannot detect on its own: clipping by an + ... overflow:hidden ancestor, opacity 0, and off-screen position. + ${result_json}= Execute Javascript + ... function rect(id) { return document.getElementById(id).getBoundingClientRect(); } + ... var overflowRect = rect('hidden-overflow'); + ... var topElement = document.elementFromPoint( + ... overflowRect.left + overflowRect.width / 2, overflowRect.top + overflowRect.height / 2); + ... return JSON.stringify({ + ... overflowClipped: topElement !== document.getElementById('hidden-overflow'), + ... opacityValue: parseFloat(getComputedStyle(document.getElementById('hidden-opacity')).opacity), + ... offscreenLeft: rect('hidden-offscreen').left + ... }); + ${result}= Evaluate json.loads('''${result_json}''') json + Should Be True ${result}[overflowClipped] + ... msg=The overflow-clipped button should not be the topmost element at its own coordinates. + Should Be Equal As Numbers ${result}[opacityValue] 0 + ... msg=The opacity-hidden button should have opacity 0. + Should Be True ${result}[offscreenLeft] < 0 + ... msg=The offscreen button should be positioned outside the viewport. From 16967c1acb35a317a2f8a78415fcad1465632316 Mon Sep 17 00:00:00 2001 From: Michelle Austria Date: Mon, 17 Aug 2026 18:37:48 +0000 Subject: [PATCH 09/11] Fix missing variables.resource import in playground suite --- tests/playground/ui_testing_playground.robot | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/playground/ui_testing_playground.robot b/tests/playground/ui_testing_playground.robot index f9deae0..41e7d7a 100644 --- a/tests/playground/ui_testing_playground.robot +++ b/tests/playground/ui_testing_playground.robot @@ -8,6 +8,7 @@ Documentation Covers every challenge page listed on uitestingplayground.co ... Animated Button, and others) have markup or interaction mechanics ... that differ from what their names might suggest. +Resource ../../resources/variables.resource Resource ../../resources/pages/playground_page.resource Suite Setup Open Playground Browser From 7c0fe7ed232f01a859c7761f6144cc93b299d2b9 Mon Sep 17 00:00:00 2001 From: Michelle Austria Date: Mon, 17 Aug 2026 18:43:36 +0000 Subject: [PATCH 10/11] Fix nine playground test failures found in the first real CI run - Execute Javascript's ARGUMENTS marker must follow the JS code, not precede it; fixes Overlapped Element and Clear Input. - Class Attribute's target button raises a confirmation alert that was left unhandled, breaking the next test's page load. - Dynamic Table shuffles column order as well as row order, so the CPU value's position must be read from the header, not hardcoded. - Verify Text's xpath used text() (direct text children only) instead of . (full descendant text), so it never matched. - Sample App's actual failed-login message is "Invalid username/password", not the idle "User logged out." text. - Mouse Over's link locator matched two elements sharing the same class; narrowed to the specific link by its text. - The Prompt dialog raises a second, unhandled alert echoing the entered value; now accepted before the next test runs. - Scroll To Click's actual completion text is "All buttons clicked!" --- resources/pages/playground_page.resource | 21 +++++++++++---- robocop.toml | 4 +++ tests/playground/ui_testing_playground.robot | 27 ++++++++++++++------ 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/resources/pages/playground_page.resource b/resources/pages/playground_page.resource index c23bb07..33518a3 100644 --- a/resources/pages/playground_page.resource +++ b/resources/pages/playground_page.resource @@ -48,7 +48,7 @@ ${SCROLLBAR_BUTTON} css:#hidingButton ${DYNAMIC_TABLE_ANSWER} css:.bg-warning # Verify Text -${VERIFY_TEXT_PLAIN_PARAGRAPH} xpath://p[contains(text(),'Hello') and not(contains(@class,'bg-warning'))] +${VERIFY_TEXT_PLAIN_PARAGRAPH} xpath://p[contains(.,'Hello') and not(contains(@class,'bg-warning'))] ${VERIFY_TEXT_HIGHLIGHTED_PARAGRAPH} css:.bg-warning # Progress Bar @@ -74,7 +74,7 @@ ${SAMPLE_APP_STATUS_MESSAGE} css:#loginstatus ${SAMPLE_APP_VALID_PASSWORD} pwd # Mouse Over -${MOUSE_OVER_LINK} css:a.text-primary +${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 @@ -209,10 +209,11 @@ Set Value Via Javascript [Arguments] ${locator} ${value} ${element}= Get WebElement ${locator} Execute Javascript - ... ARGUMENTS ... arguments[0].value = arguments[1]; ... arguments[0].dispatchEvent(new Event('input', {bubbles: true})); - ... ${element} ${value} + ... ARGUMENTS + ... ${element} + ... ${value} Get Row Cell Values [Documentation] Reads the ARIA role="row" elements of the Dynamic Table challenge @@ -226,6 +227,16 @@ Get Row Cell Values ... .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 @@ -236,6 +247,6 @@ Clear Field And Notify Page Clear Element Text ${locator} ${element}= Get WebElement ${locator} Execute Javascript - ... ARGUMENTS ... arguments[0].dispatchEvent(new Event('input', {bubbles: true})); + ... ARGUMENTS ... ${element} diff --git a/robocop.toml b/robocop.toml index c7db7d9..d4b8b91 100644 --- a/robocop.toml +++ b/robocop.toml @@ -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", ] diff --git a/tests/playground/ui_testing_playground.robot b/tests/playground/ui_testing_playground.robot index 41e7d7a..32ebfac 100644 --- a/tests/playground/ui_testing_playground.robot +++ b/tests/playground/ui_testing_playground.robot @@ -8,6 +8,7 @@ Documentation Covers every challenge page listed on uitestingplayground.co ... Animated Button, and others) have markup or interaction mechanics ... that differ from what their names might suggest. +Library Collections Resource ../../resources/variables.resource Resource ../../resources/pages/playground_page.resource @@ -36,10 +37,12 @@ Dynamic Id Button Should Be Locatable Without Relying On Its Changing Id Class Attribute Target Button Should Be Clicked By Its Unique Class [Documentation] Three buttons share nearly identical class lists that differ only ... by a volatile class1/class2/class3 marker; only .btn-primary reliably and - ... uniquely identifies the intended target. + ... uniquely identifies the intended target, confirmed by the alert it raises. Go To Playground Page classattr Page Should Contain Element ${CLASS_ATTRIBUTE_TARGET_BUTTON} limit=1 Click Button ${CLASS_ATTRIBUTE_TARGET_BUTTON} + ${alert_message}= Handle Alert action=ACCEPT + Should Be Equal ${alert_message} Primary button pressed Clicking The Green Button Should Swap In A Different Layer [Documentation] The button under #spa is replaced by a different element after the @@ -100,11 +103,12 @@ Hiding Button Should Be Clickable After Scrolling It Into View Click Button ${SCROLLBAR_BUTTON} Dynamic Table Chrome Cpu Value Should Match The Highlighted Answer - [Documentation] The table's row order reshuffles on every page load, so the Chrome - ... row must be located by its Name cell rather than a fixed position, then - ... cross-checked against the highlighted answer paragraph. + [Documentation] Both row order and column order reshuffle on every page load, so + ... the Chrome row must be located by its Name cell and the CPU value by its + ... header label, then cross-checked against the highlighted answer paragraph. Go To Playground Page dynamictable @{rows}= Get Row Cell Values + ${cpu_index}= Get Cpu Column Index VAR ${chrome_row}= ${NONE} FOR ${row} IN @{rows} IF '${row}[0]' == 'Chrome' @@ -112,8 +116,9 @@ Dynamic Table Chrome Cpu Value Should Match The Highlighted Answer END END Should Not Be Equal ${chrome_row} ${NONE} msg=No Chrome row found in the dynamic table. + ${cpu_value}= Get From List ${chrome_row} ${cpu_index} ${answer_text}= Get Text ${DYNAMIC_TABLE_ANSWER} - Should Be Equal As Strings ${answer_text} Chrome CPU: ${chrome_row}[3] + Should Be Equal As Strings ${answer_text} Chrome CPU: ${cpu_value} Verify Text Highlighted Paragraph Should Match The Plain Paragraph Once Normalized [Documentation] The two paragraphs look identical but may differ in raw whitespace @@ -157,7 +162,7 @@ Sample App Should Log In With The Valid Password And Reject An Invalid One Input Text ${SAMPLE_APP_USERNAME_FIELD} Michelle Input Text ${SAMPLE_APP_PASSWORD_FIELD} wrong-password Click Button ${SAMPLE_APP_LOGIN_BUTTON} - Element Text Should Be ${SAMPLE_APP_STATUS_MESSAGE} User logged out. + Element Text Should Be ${SAMPLE_APP_STATUS_MESSAGE} Invalid username/password Input Text ${SAMPLE_APP_PASSWORD_FIELD} ${SAMPLE_APP_VALID_PASSWORD} Click Button ${SAMPLE_APP_LOGIN_BUTTON} Element Text Should Be ${SAMPLE_APP_STATUS_MESSAGE} Welcome, Michelle! @@ -294,7 +299,7 @@ All Scroll To Click Targets Should Be Reachable And Clicked Mouse Over ${SCROLL_HOVER_ROW} Scroll Element Into View ${SCROLL_TARGET_4} Click Button ${SCROLL_TARGET_4} - Element Text Should Be ${SCROLL_PROGRESS_TEXT} Buttons clicked: 4 / 4 + Element Text Should Be ${SCROLL_PROGRESS_TEXT} All buttons clicked! Css Selectors Should Correctly Distinguish The Highlighted Button From Its Hidden Siblings [Documentation] Five buttons are hidden using five different CSS techniques, none @@ -342,12 +347,18 @@ Progress Bar Value Should Be At Least Trigger Alert And Verify Message [Documentation] Clicks a button that opens a native dialog, optionally answers a - ... prompt, accepts the dialog, and asserts it carried a message. + ... prompt, accepts the dialog, and asserts it carried a message. The + ... Prompt dialog raises a second follow-up alert echoing the entered + ... value, which must also be accepted before the next test runs. [Arguments] ${button_locator} ${prompt_text}=${NONE} Click Button ${button_locator} IF $prompt_text is not None Input Text Into Alert ${prompt_text} ${message}= Handle Alert action=ACCEPT Should Not Be Empty ${message} + IF $prompt_text is not None + ${followup_message}= Handle Alert action=ACCEPT + Should Contain ${followup_message} ${prompt_text} + END Remaining Visibility Techniques Should Be Hidden [Documentation] Checks the four Visibility page techniques that Selenium's From 15fd47b19d711d90fa508bf1b4ce77ed23c639aa Mon Sep 17 00:00:00 2001 From: Michelle Austria Date: Mon, 17 Aug 2026 18:49:14 +0000 Subject: [PATCH 11/11] Fix confirmed regressions; diagnose four repeatedly-guessed assertions File Upload's fixture path used a relative ".." segment that Chrome's webdriver rejects as non-canonical; now resolved to an absolute path at variable-definition time. Clear Input's actual completion message is "All fields are cleared!", not the "remaining: 0" text previously assumed. Hidden Layers' id-swap, Sample App's valid password, Mouse Over's link-replacement text, and Overlapped's JS value-setting have each now failed twice on guessed fixes. Rather than guess a third time, this adds a diagnostic suite that inspects the real DOM state around each interaction directly, and temporarily points CI at it. --- .github/workflows/tests.yml | 2 +- resources/pages/playground_page.resource | 3 +- tests/_diagnostic.robot | 62 ++++++++++++++++++++ tests/playground/ui_testing_playground.robot | 2 +- 4 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 tests/_diagnostic.robot diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 07b4aaf..3925894 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -74,7 +74,7 @@ jobs: robot --outputdir results/playground --variable BROWSER:headlesschrome - tests/playground + tests/_diagnostic.robot - name: Upload playground test results if: always() diff --git a/resources/pages/playground_page.resource b/resources/pages/playground_page.resource index 33518a3..7c36121 100644 --- a/resources/pages/playground_page.resource +++ b/resources/pages/playground_page.resource @@ -12,7 +12,8 @@ Library SeleniumLibrary *** Variables *** ${PLAYGROUND_BASE_URL} http://www.uitestingplayground.com -${SAMPLE_UPLOAD_FILE} ${CURDIR}/../data/sample_upload.txt +${SAMPLE_UPLOAD_FILE} +... ${{os.path.normpath(r'${CURDIR}' + '/../data/sample_upload.txt')}} # Dynamic ID ${DYNAMIC_ID_BUTTON} css:.btn.btn-primary diff --git a/tests/_diagnostic.robot b/tests/_diagnostic.robot new file mode 100644 index 0000000..c36f8f0 --- /dev/null +++ b/tests/_diagnostic.robot @@ -0,0 +1,62 @@ +*** Settings *** +Documentation Temporary diagnostic, round 6: four assertions have now failed twice on +... guessed fixes (Hidden Layers' id-swap, Sample App's valid password, +... Mouse Over's link replacement, Overlapped's JS value-setting) -- this +... gets real evidence instead of guessing a third time. Not part of the +... portfolio. +Resource ../resources/pages/playground_page.resource +Suite Setup Open Browser about:blank ${BROWSER} +Suite Teardown Close All Browsers +Test Tags smoke + + +*** Test Cases *** +Dump Hidden Layers After Click + Go To Playground Page hiddenlayers + ${before}= Execute Javascript return document.querySelector('#spa').outerHTML; + Log SPA BEFORE CLICK: ${before} console=True + Click Element ${HIDING_BUTTON} + ${after}= Execute Javascript return document.querySelector('#spa').outerHTML; + Log SPA AFTER CLICK: ${after} console=True + +Dump Sample App Login Attempt + Go To Playground Page sampleapp + Input Text ${SAMPLE_APP_USERNAME_FIELD} Michelle + Input Text ${SAMPLE_APP_PASSWORD_FIELD} pwd + ${username_value}= Get Element Attribute ${SAMPLE_APP_USERNAME_FIELD} value + ${password_value}= Get Element Attribute ${SAMPLE_APP_PASSWORD_FIELD} value + Log FIELD VALUES BEFORE SUBMIT: username=${username_value} password=${password_value} console=True + Click Button ${SAMPLE_APP_LOGIN_BUTTON} + ${status}= Get Text ${SAMPLE_APP_STATUS_MESSAGE} + Log STATUS AFTER SUBMIT: ${status} console=True + +Dump Mouse Over Link Structure + Go To Playground Page mouseover + ${before}= Execute Javascript + ... return Array.from(document.querySelectorAll('a')).map(function(a) { + ... return 'outerHTML=' + a.outerHTML; + ... }).join(' || '); + Log LINKS BEFORE HOVER: ${before} console=True + Mouse Over css:a.text-primary + Sleep 300ms + ${after}= Execute Javascript + ... return Array.from(document.querySelectorAll('a')).map(function(a) { + ... return 'outerHTML=' + a.outerHTML; + ... }).join(' || '); + Log LINKS AFTER HOVER: ${after} console=True + +Dump Overlapped Field Framework And Direct Js Set + Go To Playground Page overlapped + ${framework}= Execute Javascript + ... var root = document.getElementById('root') || document.getElementById('app') || document.body.firstElementChild; + ... return 'reactroot=' + (root && !!Object.keys(root).find(function(k) { return k.startsWith('__reactContainer') || k.startsWith('_reactRootContainer'); })) + ' outerHTML=' + document.querySelector('#name').outerHTML; + Log OVERLAPPED FIELD INFO: ${framework} console=True + ${direct_result}= Execute Javascript + ... var el = document.querySelector('#name'); + ... el.value = 'Michelle Austria'; + ... el.dispatchEvent(new Event('input', {bubbles: true})); + ... return 'immediately after set: ' + el.value; + Log DIRECT SET RESULT: ${direct_result} console=True + Sleep 300ms + ${value_after_wait}= Get Element Attribute css:#name value + Log VALUE AFTER 300MS WAIT: ${value_after_wait} console=True diff --git a/tests/playground/ui_testing_playground.robot b/tests/playground/ui_testing_playground.robot index 32ebfac..82f5665 100644 --- a/tests/playground/ui_testing_playground.robot +++ b/tests/playground/ui_testing_playground.robot @@ -284,7 +284,7 @@ Clear Input Should Reduce The Non Empty Field Counter To Zero ... var editable = document.querySelector('#clearContentEditable'); ... editable.textContent = ''; ... editable.dispatchEvent(new Event('input', {bubbles: true})); - Element Text Should Be ${CLEAR_INPUT_STATUS} Non-empty fields remaining: 0 + Element Text Should Be ${CLEAR_INPUT_STATUS} All fields are cleared! All Scroll To Click Targets Should Be Reachable And Clicked [Documentation] Each button needs a different technique to become clickable: a