diff --git a/README.md b/README.md index 45b6bde..4e5225c 100644 --- a/README.md +++ b/README.md @@ -56,28 +56,28 @@ Embedded from [`docs/scenario-matrix.md`](docs/scenario-matrix.md), generated by | UI-LOGIN-001 | P0 | Login succeeds with valid credentials | ✅ | ✅ | ✅ | | UI-LOGIN-002 | P0 | Login rejects an invalid username | ✅ | ✅ | — | | UI-LOGIN-003 | P0 | Login rejects an invalid password | ✅ | ✅ | — | -| UI-ADDREMOVE-001 | P0 | Add and remove buttons update the dynamic DOM | — | ✅ | — | -| UI-CHECKBOX-001 | P0 | Checkboxes toggle on and off | ✅ | ✅ | — | +| UI-ADDREMOVE-001 | P0 | Add and remove buttons update the dynamic DOM | — | ✅ | ✅ | +| UI-CHECKBOX-001 | P0 | Checkboxes toggle on and off | ✅ | ✅ | ✅ | | UI-CONTEXT-001 | P1 | Context menu opens the expected alert | ✅ | — | — | | UI-DRAGDROP-001 | P1 | Drag box A over box B | ✅ | — | — | | UI-DRAGDROP-002 | P1 | Drag box B over box A | ✅ | — | — | | UI-DRAGDROP-003 | P1 | Drag boxes in both directions | ✅ | — | — | -| UI-DROPDOWN-001 | P0 | Dropdown options can be selected | ✅ | ✅ | — | +| UI-DROPDOWN-001 | P0 | Dropdown options can be selected | ✅ | ✅ | ✅ | | UI-DYNCONTENT-001 | P1 | Dynamic content changes on reload | ✅ | — | — | | UI-DYNCONTENT-002 | P1 | Dynamic content partially changes on reload | ✅ | — | — | -| UI-DYNCTRL-001 | P0 | Dynamic controls remove and add a checkbox | ✅ | ✅ | — | -| UI-DYNCTRL-002 | P0 | Dynamic controls checkbox toggles | ✅ | ✅ | — | -| UI-DYNCTRL-003 | P0 | Dynamic controls enable and disable a textbox | ✅ | ✅ | — | -| UI-DYNLOAD-001 | P0 | Dynamic loading reveals the success message | ✅ | ✅ | — | +| UI-DYNCTRL-001 | P0 | Dynamic controls remove and add a checkbox | ✅ | ✅ | ✅ | +| UI-DYNCTRL-002 | P0 | Dynamic controls checkbox toggles | ✅ | ✅ | ✅ | +| UI-DYNCTRL-003 | P0 | Dynamic controls enable and disable a textbox | ✅ | ✅ | ✅ | +| UI-DYNLOAD-001 | P0 | Dynamic loading reveals the success message | ✅ | ✅ | ✅ | | UI-UPLOAD-001 | P0 | File upload shows the uploaded file | ✅ | ✅ | — | | UI-DOWNLOAD-001 | P1 | File download retrieves the expected content | ✅ | ✅ | — | | UI-FLOATING-001 | P1 | Floating menu remains visible while scrolling | ✅ | — | — | | UI-IFRAME-001 | P0 | iFrame editor supports context switching | ✅ | ✅ | — | -| UI-INPUT-001 | P0 | Numeric input accepts keyboard data | — | ✅ | — | +| UI-INPUT-001 | P0 | Numeric input accepts keyboard data | — | ✅ | ✅ | | UI-HOVERS-001 | P1 | Hovering reveals user details | ✅ | — | — | -| UI-JSALERT-001 | P0 | JavaScript alert buttons open popups | ✅ | ✅ | — | -| UI-JSALERT-002 | P0 | JavaScript alert messages match expectations | ✅ | ✅ | — | -| UI-JSALERT-003 | P0 | JavaScript prompt accepts input | ✅ | ✅ | — | +| UI-JSALERT-001 | P0 | JavaScript alert buttons open popups | ✅ | ✅ | ✅ | +| UI-JSALERT-002 | P0 | JavaScript alert messages match expectations | ✅ | ✅ | ✅ | +| UI-JSALERT-003 | P0 | JavaScript prompt accepts input | ✅ | ✅ | ✅ | | UI-JSERROR-001 | P1 | JavaScript error page exposes the expected error | ✅ | — | — | | UI-WINDOWS-001 | P0 | Link opens expected content in a new tab | ✅ | ✅ | — | | HTTP-STATUS-001 | P0 | Status code endpoints return expected responses | — | ✅ | — | diff --git a/docs/scenario-matrix.md b/docs/scenario-matrix.md index f279da7..cc54acd 100644 --- a/docs/scenario-matrix.md +++ b/docs/scenario-matrix.md @@ -7,28 +7,28 @@ Generated by `tools/check-scenarios.py --write-matrix`. | UI-LOGIN-001 | P0 | Login succeeds with valid credentials | ✅ | ✅ | ✅ | | UI-LOGIN-002 | P0 | Login rejects an invalid username | ✅ | ✅ | — | | UI-LOGIN-003 | P0 | Login rejects an invalid password | ✅ | ✅ | — | -| UI-ADDREMOVE-001 | P0 | Add and remove buttons update the dynamic DOM | — | ✅ | — | -| UI-CHECKBOX-001 | P0 | Checkboxes toggle on and off | ✅ | ✅ | — | +| UI-ADDREMOVE-001 | P0 | Add and remove buttons update the dynamic DOM | — | ✅ | ✅ | +| UI-CHECKBOX-001 | P0 | Checkboxes toggle on and off | ✅ | ✅ | ✅ | | UI-CONTEXT-001 | P1 | Context menu opens the expected alert | ✅ | — | — | | UI-DRAGDROP-001 | P1 | Drag box A over box B | ✅ | — | — | | UI-DRAGDROP-002 | P1 | Drag box B over box A | ✅ | — | — | | UI-DRAGDROP-003 | P1 | Drag boxes in both directions | ✅ | — | — | -| UI-DROPDOWN-001 | P0 | Dropdown options can be selected | ✅ | ✅ | — | +| UI-DROPDOWN-001 | P0 | Dropdown options can be selected | ✅ | ✅ | ✅ | | UI-DYNCONTENT-001 | P1 | Dynamic content changes on reload | ✅ | — | — | | UI-DYNCONTENT-002 | P1 | Dynamic content partially changes on reload | ✅ | — | — | -| UI-DYNCTRL-001 | P0 | Dynamic controls remove and add a checkbox | ✅ | ✅ | — | -| UI-DYNCTRL-002 | P0 | Dynamic controls checkbox toggles | ✅ | ✅ | — | -| UI-DYNCTRL-003 | P0 | Dynamic controls enable and disable a textbox | ✅ | ✅ | — | -| UI-DYNLOAD-001 | P0 | Dynamic loading reveals the success message | ✅ | ✅ | — | +| UI-DYNCTRL-001 | P0 | Dynamic controls remove and add a checkbox | ✅ | ✅ | ✅ | +| UI-DYNCTRL-002 | P0 | Dynamic controls checkbox toggles | ✅ | ✅ | ✅ | +| UI-DYNCTRL-003 | P0 | Dynamic controls enable and disable a textbox | ✅ | ✅ | ✅ | +| UI-DYNLOAD-001 | P0 | Dynamic loading reveals the success message | ✅ | ✅ | ✅ | | UI-UPLOAD-001 | P0 | File upload shows the uploaded file | ✅ | ✅ | — | | UI-DOWNLOAD-001 | P1 | File download retrieves the expected content | ✅ | ✅ | — | | UI-FLOATING-001 | P1 | Floating menu remains visible while scrolling | ✅ | — | — | | UI-IFRAME-001 | P0 | iFrame editor supports context switching | ✅ | ✅ | — | -| UI-INPUT-001 | P0 | Numeric input accepts keyboard data | — | ✅ | — | +| UI-INPUT-001 | P0 | Numeric input accepts keyboard data | — | ✅ | ✅ | | UI-HOVERS-001 | P1 | Hovering reveals user details | ✅ | — | — | -| UI-JSALERT-001 | P0 | JavaScript alert buttons open popups | ✅ | ✅ | — | -| UI-JSALERT-002 | P0 | JavaScript alert messages match expectations | ✅ | ✅ | — | -| UI-JSALERT-003 | P0 | JavaScript prompt accepts input | ✅ | ✅ | — | +| UI-JSALERT-001 | P0 | JavaScript alert buttons open popups | ✅ | ✅ | ✅ | +| UI-JSALERT-002 | P0 | JavaScript alert messages match expectations | ✅ | ✅ | ✅ | +| UI-JSALERT-003 | P0 | JavaScript prompt accepts input | ✅ | ✅ | ✅ | | UI-JSERROR-001 | P1 | JavaScript error page exposes the expected error | ✅ | — | — | | UI-WINDOWS-001 | P0 | Link opens expected content in a new tab | ✅ | ✅ | — | | HTTP-STATUS-001 | P0 | Status code endpoints return expected responses | — | ✅ | — | diff --git a/scenarios/catalog.yml b/scenarios/catalog.yml index d80f284..f9f99fb 100644 --- a/scenarios/catalog.yml +++ b/scenarios/catalog.yml @@ -18,12 +18,12 @@ scenarios: title: Add and remove buttons update the dynamic DOM priority: P0 tags: [] - coverage: {java-selenium-testng: false, ts-playwright: true, python-playwright: false} + coverage: {java-selenium-testng: false, ts-playwright: true, python-playwright: true} - id: UI-CHECKBOX-001 title: Checkboxes toggle on and off priority: P0 tags: [smoke] - coverage: {java-selenium-testng: true, ts-playwright: true, python-playwright: false} + coverage: {java-selenium-testng: true, ts-playwright: true, python-playwright: true} - id: UI-CONTEXT-001 title: Context menu opens the expected alert priority: P1 @@ -48,7 +48,7 @@ scenarios: title: Dropdown options can be selected priority: P0 tags: [smoke] - coverage: {java-selenium-testng: true, ts-playwright: true, python-playwright: false} + coverage: {java-selenium-testng: true, ts-playwright: true, python-playwright: true} - id: UI-DYNCONTENT-001 title: Dynamic content changes on reload priority: P1 @@ -63,22 +63,22 @@ scenarios: title: Dynamic controls remove and add a checkbox priority: P0 tags: [] - coverage: {java-selenium-testng: true, ts-playwright: true, python-playwright: false} + coverage: {java-selenium-testng: true, ts-playwright: true, python-playwright: true} - id: UI-DYNCTRL-002 title: Dynamic controls checkbox toggles priority: P0 tags: [] - coverage: {java-selenium-testng: true, ts-playwright: true, python-playwright: false} + coverage: {java-selenium-testng: true, ts-playwright: true, python-playwright: true} - id: UI-DYNCTRL-003 title: Dynamic controls enable and disable a textbox priority: P0 tags: [smoke] - coverage: {java-selenium-testng: true, ts-playwright: true, python-playwright: false} + coverage: {java-selenium-testng: true, ts-playwright: true, python-playwright: true} - id: UI-DYNLOAD-001 title: Dynamic loading reveals the success message priority: P0 tags: [smoke] - coverage: {java-selenium-testng: true, ts-playwright: true, python-playwright: false} + coverage: {java-selenium-testng: true, ts-playwright: true, python-playwright: true} - id: UI-UPLOAD-001 title: File upload shows the uploaded file priority: P0 @@ -103,7 +103,7 @@ scenarios: title: Numeric input accepts keyboard data priority: P0 tags: [] - coverage: {java-selenium-testng: false, ts-playwright: true, python-playwright: false} + coverage: {java-selenium-testng: false, ts-playwright: true, python-playwright: true} - id: UI-HOVERS-001 title: Hovering reveals user details priority: P1 @@ -113,17 +113,17 @@ scenarios: title: JavaScript alert buttons open popups priority: P0 tags: [] - coverage: {java-selenium-testng: true, ts-playwright: true, python-playwright: false} + coverage: {java-selenium-testng: true, ts-playwright: true, python-playwright: true} - id: UI-JSALERT-002 title: JavaScript alert messages match expectations priority: P0 tags: [] - coverage: {java-selenium-testng: true, ts-playwright: true, python-playwright: false} + coverage: {java-selenium-testng: true, ts-playwright: true, python-playwright: true} - id: UI-JSALERT-003 title: JavaScript prompt accepts input priority: P0 tags: [smoke] - coverage: {java-selenium-testng: true, ts-playwright: true, python-playwright: false} + coverage: {java-selenium-testng: true, ts-playwright: true, python-playwright: true} - id: UI-JSERROR-001 title: JavaScript error page exposes the expected error priority: P1 diff --git a/stacks/python-playwright/README.md b/stacks/python-playwright/README.md index 38cd4b5..47e762e 100644 --- a/stacks/python-playwright/README.md +++ b/stacks/python-playwright/README.md @@ -1,8 +1,8 @@ # Python Playwright stack -Pytest + Playwright coverage for the shared scenario catalog. This stack starts -with the Python smoke scenario and grows through the P0 suite in the Phase 7 -follow-up PRs. +Pytest + Playwright coverage for the shared scenario catalog. This stack covers +the first Python P0 UI batch and grows through the remaining P0 suite in the +Phase 7 follow-up PR. ## Prerequisites diff --git a/stacks/python-playwright/tests/test_add_remove.py b/stacks/python-playwright/tests/test_add_remove.py new file mode 100644 index 0000000..76f1215 --- /dev/null +++ b/stacks/python-playwright/tests/test_add_remove.py @@ -0,0 +1,22 @@ +"""Add/remove element scenarios for the Python Playwright stack.""" + +from __future__ import annotations + +from playwright.sync_api import Page, expect + + +def test_ui_addremove_001_adds_and_removes_delete_buttons(page: Page) -> None: + """UI-ADDREMOVE-001 adds and removes delete buttons.""" + page.goto("/add_remove_elements/") + + add_button = page.get_by_role("button", name="Add Element") + delete_buttons = page.get_by_role("button", name="Delete") + + expect(delete_buttons).to_have_count(0) + add_button.click() + add_button.click() + add_button.click() + expect(delete_buttons).to_have_count(3) + + delete_buttons.nth(1).click() + expect(delete_buttons).to_have_count(2) diff --git a/stacks/python-playwright/tests/test_checkboxes.py b/stacks/python-playwright/tests/test_checkboxes.py new file mode 100644 index 0000000..d499cb4 --- /dev/null +++ b/stacks/python-playwright/tests/test_checkboxes.py @@ -0,0 +1,26 @@ +"""Checkbox scenarios for the Python Playwright stack.""" + +from __future__ import annotations + +import pytest +from playwright.sync_api import Page, expect + + +@pytest.mark.smoke +def test_ui_checkbox_001_toggles_checkboxes_on_and_off(page: Page) -> None: + """UI-CHECKBOX-001 @smoke toggles checkboxes on and off.""" + page.goto("/checkboxes") + + checkboxes = page.locator('#checkboxes input[type="checkbox"]') + first = checkboxes.nth(0) + second = checkboxes.nth(1) + + expect(checkboxes).to_have_count(2) + expect(first).not_to_be_checked() + expect(second).to_be_checked() + + first.check() + second.uncheck() + + expect(first).to_be_checked() + expect(second).not_to_be_checked() diff --git a/stacks/python-playwright/tests/test_dropdown.py b/stacks/python-playwright/tests/test_dropdown.py new file mode 100644 index 0000000..cbe40e2 --- /dev/null +++ b/stacks/python-playwright/tests/test_dropdown.py @@ -0,0 +1,22 @@ +"""Dropdown scenarios for the Python Playwright stack.""" + +from __future__ import annotations + +import pytest +from playwright.sync_api import Page, expect + + +@pytest.mark.smoke +def test_ui_dropdown_001_selects_dropdown_options(page: Page) -> None: + """UI-DROPDOWN-001 @smoke selects dropdown options.""" + page.goto("/dropdown") + + dropdown = page.locator("#dropdown") + + dropdown.select_option("1") + expect(dropdown).to_have_value("1") + expect(dropdown.locator("option:checked")).to_have_text("Option 1") + + dropdown.select_option("2") + expect(dropdown).to_have_value("2") + expect(dropdown.locator("option:checked")).to_have_text("Option 2") diff --git a/stacks/python-playwright/tests/test_dynamic_controls.py b/stacks/python-playwright/tests/test_dynamic_controls.py new file mode 100644 index 0000000..5bbecaf --- /dev/null +++ b/stacks/python-playwright/tests/test_dynamic_controls.py @@ -0,0 +1,54 @@ +"""Dynamic controls scenarios for the Python Playwright stack.""" + +from __future__ import annotations + +import pytest +from playwright.sync_api import Page, expect + + +def test_ui_dynctrl_001_removes_and_adds_the_checkbox(page: Page) -> None: + """UI-DYNCTRL-001 removes and adds the checkbox.""" + page.goto("/dynamic_controls") + + checkbox = page.get_by_role("checkbox") + message = page.locator("#message") + + expect(checkbox).to_have_count(1) + page.get_by_role("button", name="Remove").click() + expect(checkbox).to_have_count(0) + expect(message).to_have_text("It's gone!") + + page.get_by_role("button", name="Add").click() + expect(checkbox).to_have_count(1) + expect(message).to_have_text("It's back!") + + +def test_ui_dynctrl_002_toggles_the_checkbox_state(page: Page) -> None: + """UI-DYNCTRL-002 toggles the checkbox state.""" + page.goto("/dynamic_controls") + + checkbox = page.get_by_role("checkbox") + + expect(checkbox).not_to_be_checked() + checkbox.check() + expect(checkbox).to_be_checked() + checkbox.uncheck() + expect(checkbox).not_to_be_checked() + + +@pytest.mark.smoke +def test_ui_dynctrl_003_enables_and_disables_the_textbox(page: Page) -> None: + """UI-DYNCTRL-003 @smoke enables and disables the textbox.""" + page.goto("/dynamic_controls") + + textbox = page.get_by_role("textbox") + message = page.locator("#message") + + expect(textbox).to_be_disabled() + page.get_by_role("button", name="Enable").click() + expect(textbox).to_be_enabled() + expect(message).to_have_text("It's enabled!") + + page.get_by_role("button", name="Disable").click() + expect(textbox).to_be_disabled() + expect(message).to_have_text("It's disabled!") diff --git a/stacks/python-playwright/tests/test_dynamic_loading.py b/stacks/python-playwright/tests/test_dynamic_loading.py new file mode 100644 index 0000000..397f4a8 --- /dev/null +++ b/stacks/python-playwright/tests/test_dynamic_loading.py @@ -0,0 +1,17 @@ +"""Dynamic loading scenarios for the Python Playwright stack.""" + +from __future__ import annotations + +import pytest +from playwright.sync_api import Page, expect + + +@pytest.mark.smoke +def test_ui_dynload_001_reveals_the_loaded_message(page: Page) -> None: + """UI-DYNLOAD-001 @smoke reveals the loaded message.""" + page.goto("/dynamic_loading/2") + + expect(page.get_by_role("heading", name="Dynamically Loaded Page Elements")).to_be_visible() + page.get_by_role("button", name="Start").click() + + expect(page.locator("#finish")).to_have_text("Hello World!", timeout=15_000) diff --git a/stacks/python-playwright/tests/test_inputs.py b/stacks/python-playwright/tests/test_inputs.py new file mode 100644 index 0000000..41c61bd --- /dev/null +++ b/stacks/python-playwright/tests/test_inputs.py @@ -0,0 +1,21 @@ +"""Input scenarios for the Python Playwright stack.""" + +from __future__ import annotations + +from playwright.sync_api import Page, expect + + +def test_ui_input_001_accepts_typed_numeric_input(page: Page) -> None: + """UI-INPUT-001 accepts typed numeric input.""" + page.goto("/inputs") + + number_input = page.locator('input[type="number"]') + + number_input.fill("12345") + expect(number_input).to_have_value("12345") + + number_input.press("ArrowDown") + expect(number_input).to_have_value("12344") + + number_input.fill("-7") + expect(number_input).to_have_value("-7") diff --git a/stacks/python-playwright/tests/test_javascript_alerts.py b/stacks/python-playwright/tests/test_javascript_alerts.py new file mode 100644 index 0000000..f1be43b --- /dev/null +++ b/stacks/python-playwright/tests/test_javascript_alerts.py @@ -0,0 +1,81 @@ +"""JavaScript alert scenarios for the Python Playwright stack.""" + +from __future__ import annotations + +from dataclasses import dataclass + +import pytest +from playwright.sync_api import Dialog, Page, expect + + +@dataclass(frozen=True, slots=True) +class DialogExpectation: + """Expected dialog metadata for a JavaScript alert control.""" + + button_name: str + message: str + dialog_type: str + + +@dataclass(frozen=True, slots=True) +class DialogSnapshot: + """Observed dialog metadata captured before handling the dialog.""" + + message: str + dialog_type: str + + +DIALOG_EXPECTATIONS = ( + DialogExpectation("Click for JS Alert", "I am a JS Alert", "alert"), + DialogExpectation("Click for JS Confirm", "I am a JS Confirm", "confirm"), + DialogExpectation("Click for JS Prompt", "I am a JS prompt", "prompt"), +) + + +def trigger_dialog(page: Page, button_name: str, prompt_text: str | None = None) -> DialogSnapshot: + """Click a control and return the dialog snapshot it opens.""" + snapshots: list[DialogSnapshot] = [] + + def handle_dialog(dialog: Dialog) -> None: + snapshots.append(DialogSnapshot(message=dialog.message, dialog_type=dialog.type)) + if prompt_text is None: + dialog.dismiss() + else: + dialog.accept(prompt_text) + + page.once("dialog", handle_dialog) + page.get_by_role("button", name=button_name).click() + if not snapshots: + msg = f"button did not open a dialog: {button_name}" + raise AssertionError(msg) + return snapshots[0] + + +def test_ui_jsalert_001_opens_each_javascript_dialog_type(page: Page) -> None: + """UI-JSALERT-001 opens each JavaScript dialog type.""" + page.goto("/javascript_alerts") + + for expected in DIALOG_EXPECTATIONS: + dialog = trigger_dialog(page, expected.button_name) + assert dialog.dialog_type == expected.dialog_type + + +def test_ui_jsalert_002_validates_javascript_dialog_messages(page: Page) -> None: + """UI-JSALERT-002 validates JavaScript dialog messages.""" + page.goto("/javascript_alerts") + + for expected in DIALOG_EXPECTATIONS: + dialog = trigger_dialog(page, expected.button_name) + assert dialog.message == expected.message + + +@pytest.mark.smoke +def test_ui_jsalert_003_accepts_prompt_input(page: Page) -> None: + """UI-JSALERT-003 @smoke accepts prompt input.""" + page.goto("/javascript_alerts") + + prompt_text = "This is a test message" + dialog = trigger_dialog(page, "Click for JS Prompt", prompt_text) + assert dialog.message == "I am a JS prompt" + + expect(page.locator("#result")).to_have_text(f"You entered: {prompt_text}")