Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Testing Using Pilot - pilot.click() Doesn't Have Effect On Static Widget #2022

Closed
MalayAgr opened this issue Mar 11, 2023 · 7 comments · Fixed by #2034
Closed

Testing Using Pilot - pilot.click() Doesn't Have Effect On Static Widget #2022

MalayAgr opened this issue Mar 11, 2023 · 7 comments · Fixed by #2034
Assignees
Labels
bug Something isn't working Task

Comments

@MalayAgr
Copy link

MalayAgr commented Mar 11, 2023

Hi,

I am trying to write tests for the example calculator app present in the repo (at the same time, expanding it to add a few more operators). This is my CalculatorApp class:

class CalculatorApp(App):
    CSS_PATH = "calculator.css"

    viewport = var("0")
    show_ac = var(True)
    left = var(Decimal("0"))
    right = var(Decimal("0"))
    value = var("")
    operator = var("plus")

    def watch_viewport(self, value: str) -> None:
        self.query_one("#viewport", Static).update(value)

    def compute_show_ac(self) -> bool:
        return self.value in ("", "0") and self.viewport == "0"

    def watch_show_ac(self, show_ac: bool) -> None:
        self.query_one("#ac").display = show_ac
        self.query_one("#c").display = not show_ac

    def compose(self) -> ComposeResult:
        with Container(id="calculator"):
            yield Static(id="viewport")
            yield Button("AC", id="ac", variant="primary")
            yield Button("C", id="c", variant="primary")
            yield Button("+/-", id="negation", variant="primary")
            yield Button("%", id="percent", variant="primary")
            yield Button("sin(x)", id="sine", variant="warning")
            yield Button("cos(x)", id="cosine", variant="warning")
            yield Button("7", id="number-7", variant="primary")
            yield Button("8", id="number-8", variant="primary")
            yield Button("9", id="number-9", variant="primary")
            yield Button("+", id="plus", variant="warning")
            yield Button("x^y", id="exponent", variant="warning")
            yield Button("4", id="number-4", variant="primary")
            yield Button("5", id="number-5", variant="primary")
            yield Button("6", id="number-6", variant="primary")
            yield Button("-", id="minus", variant="warning")
            yield Button("ln(x)", id="logarithm", variant="warning")
            yield Button("1", id="number-1", variant="primary")
            yield Button("2", id="number-2", variant="primary")
            yield Button("3", id="number-3", variant="primary")
            yield Button("*", id="multiply", variant="warning")
            yield Button("x!", id="factorial", variant="warning")
            yield Button("0", id="number-0", variant="primary")
            yield Button(".", id="point", variant="primary")
            yield Button("÷", id="divide", variant="warning")
            yield Button("=", id="equals", variant="warning")

    def on_button_pressed(self, event: Button.Pressed) -> None:
        button_id = event.button.id

        assert button_id is not None

        if button_id.startswith("number-"):
            number = button_id.split("-")[-1]
            self.viewport = self.value = self.value.lstrip("0") + number

I wrote the following test to check that clicking the number buttons results in the calculator's display (Static(id="viewport")) accumulating digits to make a number:

async def test_number_buttons():
    async with CalculatorApp().run_test() as pilot:
        app = pilot.app

        await pilot.click("#number-1")

        display_content = app.query_one("#viewport").render()
        assert str(display_content) == "1"

        await pilot.click("#number-2")

        display_content = app.query_one("#viewport").render()
        assert str(display_content) == "12"

        await pilot.click("#number-3")

        display_content = app.query_one("#viewport").render()
        assert str(display_content) == "123"

While the GUI gets updated correctly on clicking the buttons, the test always fails since app.query_one("#viewport").render() always returns "0". I've also tried replacing app.query_one("#viewport").render() with app.query_one("#viewport", Static).render() but that hasn't helped either.

Is this supposed to happen?

Textual Diagnostics

Versions

Name Value
Textual 0.14.0
Rich 13.3.2

Python

Name Value
Version 3.10.9
Implementation CPython
Compiler GCC 11.2.0
Executable /home/malay_agr/anaconda3/envs/spe/bin/python

Operating System

Name Value
System Linux
Release 5.15.0-52-generic
Version #58-Ubuntu SMP Thu Oct 13 08:03:55 UTC 2022

Terminal

Name Value
Terminal Application vscode (1.76.1)
TERM xterm-256color
COLORTERM truecolor
FORCE_COLOR Not set
NO_COLOR Not set

Rich Console options

Name Value
size width=197, height=19
legacy_windows False
min_width 1
max_width 197
is_terminal True
encoding utf-8
max_height 19
justify None
overflow None
no_wrap False
highlight None
markup None
height None
@MalayAgr MalayAgr changed the title Testing the Static Widget Using PyTest Testing Using Pilot - pilot.click() Doesn't Have Effect On Static Widget Mar 11, 2023
@MalayAgr
Copy link
Author

Update: Since then, I've implemented keyboard support for the number buttons and the test passes if I use pilot.press() instead of pilot.click(). This is the testing code with pilot.press():

async def test_number_buttons():
    async with CalculatorApp().run_test() as pilot:
        await pilot.press("1")

        display_content = pilot.app.query_one("#viewport", Static).render()
        assert str(display_content) == "1"

        await pilot.press("2")

        display_content = pilot.app.query_one("#viewport", Static).render()
        assert str(display_content) == "12"

        await pilot.press("3")

        display_content = pilot.app.query_one("#viewport", Static).render()
        assert str(display_content) == "123"

@MalayAgr
Copy link
Author

MalayAgr commented Mar 12, 2023

Update: Since then, I've realized that it is possible to access class attributes like viewport, show_ac, left, right, etc. I've tried two variants of the above test with this in mind.

This one uses app.value.

async def test_number_buttons():
    async with CalculatorApp().run_test() as pilot:
        await pilot.click("#number-1")

        assert str(pilot.app.value) == "1"

        await pilot.click("#number-2")

        assert str(pilot.app.value) == "12"

        await pilot.click("#number-3")

        assert str(pilot.app.value) == "123"

This one uses app.viewport.

async def test_number_buttons():
    async with CalculatorApp().run_test() as pilot:
        await pilot.click("#number-1")

        assert str(pilot.app.viewport) == "1"

        await pilot.click("#number-2")

        assert str(pilot.app.viewport) == "12"

        await pilot.click("#number-3")

        assert str(pilot.app.viewport) == "123"

Both of them fail because app.value is always "" and app.viewport is always "0", even though the GUI gets updated correctly.

@Textualize Textualize deleted a comment from github-actions bot Mar 13, 2023
@davep
Copy link
Contributor

davep commented Mar 13, 2023

Both of them fail because app.value is always "" and app.viewport is always "0", even though the GUI gets updated correctly.

At first glance this sort of makes sense, when running with the pilot. What is happening is that the call to click is generating fake mouse messages (MouseDown, MouseUp and Click) and then returning after a pause. Those messages are going to then eventually have to result in a ButtonClicked message, which in turn will result in on_button_pressed being invoked, which in turn will result in value being changed, which in turn will eventually result in the Static that shows the numbers rendering the updated value.

Point being: by the time you get to the first assert most of that work won't have taken place yet.

However, it's reasonable to assume that the pilot.click should come back once the work has been done, so we'll dive a bit deeper into this and see if we can make that work.

@davep davep added bug Something isn't working Task labels Mar 13, 2023
@davep davep self-assigned this Mar 13, 2023
@davep
Copy link
Contributor

davep commented Mar 13, 2023

Right, found the issue and it's not what we were initially thinking. Fix is about to get under way.

Thanks for raising this; it's a good spot!

davep added a commit to davep/textual that referenced this issue Mar 13, 2023
@davep davep linked a pull request Mar 13, 2023 that will close this issue
willmcgugan added a commit that referenced this issue Mar 13, 2023
* Correct the x and y values for pilot-induced clicks

Fixes #2022.

* Update the ChangeLog

* Add modifier key support to `Pilot.click`

---------

Co-authored-by: Will McGugan <[email protected]>
@github-actions
Copy link

Don't forget to star the repository!

Follow @textualizeio for Textual updates.

@davep
Copy link
Contributor

davep commented Mar 13, 2023

Just to update, this fix is now available: https://github.com/Textualize/textual/releases/tag/v0.15.0

@MalayAgr
Copy link
Author

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working Task
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants