# NiceGUI

[Button: icon:menu]

[](/)

[Installation](/#installation)

[Features](/#features)

[Demos](/#demos)

[Documentation](/documentation)

[Examples](/examples)

[Why?](/#why)

[Button]

[Button]

[](https://github.com/zauberzeug/nicegui/)

[Button: icon:more_vert]

# User Fixture

## User Fixture


        We recommend utilizing the `user` fixture instead of the [`screen` fixture](/documentation/screen) wherever possible
        because execution is as fast as unit tests and it does not need Selenium as a dependency.
        The `user` fixture cuts away the browser and replaces it by a lightweight simulation.

        You can assert to "see" specific elements or content, click buttons, type into inputs and trigger events.
        We aimed for a nice API to write acceptance tests which read like a story and are easy to understand.
        Due to the fast execution, the classical [test pyramid](https://martinfowler.com/bliki/TestPyramid.html),
        where UI tests are considered to be slow, error prone and expensive, does not apply anymore 🚀.
    

example

[Button]

````python
await user.open('/')
user.find('Username').type('user1')
user.find('Password').type('pass1').trigger('keydown.enter')
await user.should_see('Hello user1!')
user.find('logout').click()
await user.should_see('Log in')

````


        **Note:** The `user` fixture might still miss some features.
        Please let us know in separate feature requests
        [over on GitHub](https://github.com/zauberzeug/nicegui/discussions/new?category=ideas-feature-requests).
    

## Async execution


        The user simulation runs in the same async context as your app
        to make querying and interaction as easy as possible.
        But that also means that your tests must be `async`.
        We suggest to activate the [pytest-asyncio auto-mode](https://pytest-asyncio.readthedocs.io/en/latest/concepts.html#auto-mode)
        by either creating a `pytest.ini` file in your project root
        or adding the activation directly to your `pyproject.toml`.

        **Note:** Do not set `asyncio_default_fixture_loop_scope` to anything other than `function` (the default).
        Using `module`, `session`, or other scopes can interfere with NiceGUI's background tasks
        and cause issues like binding updates not being reflected in tests.
    

pytest.ini

[Button]

````ini
[pytest]
asyncio_mode = auto

````

or

pyproject.toml

[Button]

````toml
[tool.pytest.ini_options]
asyncio_mode = "auto"

````

## Querying


    The querying capabilities of the `User` are built upon the [ElementFilter](/documentation/element_filter).
    The `user.should_see(...)` method and `user.find(...)` method
    provide parameters to filter for content, [markers](/documentation/element_filter#markers), types, etc.
    If you do not provide a named property, the string will match against the text content and markers.


some UI code

[Button]

````python
with ui.row():
    ui.label('Hello World!').mark('greeting')
    ui.icon('star')
with ui.row():
    ui.label('Hello Universe!')
    ui.input(placeholder='Type here')

````

user assertions

[Button]

````python
await user.should_see('greeting')
await user.should_see('star')
await user.should_see('Hello Universe!')
await user.should_see('Type here')
await user.should_see('Hello')
await user.should_see(marker='greeting')
await user.should_see(kind=ui.icon)

````

## User Interaction


    `user.find(...)` returns a `UserInteraction` object which provides methods to type text,
    clear inputs, click buttons and trigger events on the found elements.
    This demo shows how to trigger a "keydown.tab" event to autocomplete an input field after typing the first letter.

    *Added in version 2.7.0: triggering events*


some UI code

[Button]

````python
fruits = ['apple', 'banana', 'cherry']
ui.input(label='fruit', autocomplete=fruits)

````

user assertions

[Button]

````python
await user.open('/')
user.find('fruit').type('a').trigger('keydown.tab')
await user.should_see('apple')

````

## Selecting options


    To choose items in a `ui.select` simply

    - locate the `ui.select` element using `user.find()`,
    - use `click()` to open the dropdown,
    - locate the specific _option_ you want to select, again using `user.find()`, and
    - use `click()` a second time to select the desired option.

    For a multi-select element, repeat the click-and-choose steps for each item.


UI code

[Button]

````python
ui.select(
    ['Apple', 'Banana', 'Cherry'],
    label='Fruits',
    multiple=True,
    on_change=lambda e: ui.notify(', '.join(e.value)),
)

````

user assertions

[Button]

````python
user.find('Fruits').click()
user.find('Apple').click()
user.find('Banana').click()
await user.should_see('Apple, Banana')

````

## Using an ElementFilter


    It may be desirable to use an [`ElementFilter`](/documentation/element_filter) to

    - preserve the order of elements to check their order on the page, and
    - more granular filtering options, such as `ElementFilter(...).within(...)`.

    By entering the `user` context and iterating over `ElementFilter`,
    you can preserve the natural document order of matching elements:


UI code

[Button]

````python
ui.label('1').mark('number')
ui.label('2').mark('number')
ui.label('3').mark('number')

````

user assertions

[Button]

````python
with user:
    elements = list(ElementFilter(marker='number'))
    assert len(elements) == 3
    assert elements[0].text == '1'
    assert elements[1].text == '2'
    assert elements[2].text == '3'

````

## Complex elements


    There are some elements with complex visualization and interaction behaviors (`ui.upload`, `ui.table`, ...).
    Not every aspect of these elements can be tested with `should_see` and `UserInteraction`.
    Still, you can grab them with `user.find(...)` and do the testing on the elements themselves.


some UI code

[Button]

````python
async def receive_file(e: events.UploadEventArguments):
    content = await e.file.text()
    reader = csv.DictReader(content.splitlines())
    ui.table(
        columns=[{
            'name': h,
            'label': h.capitalize(),
            'field': h,
        } for h in reader.fieldnames or []],
        rows=list(reader),
    )

ui.upload(on_upload=receive_file)

````

user assertions

[Button]

````python
from nicegui import ui

upload = user.find(ui.upload).elements.pop()
await upload.handle_uploads([
    ui.upload.SmallFileUpload('data.csv', 'text/csv', b'name,age\nAlice,30\nBob,28')
])
await user.should_see(ui.table)
table = user.find(ui.table).elements.pop()
assert table.columns == [
    {'name': 'name', 'label': 'Name', 'field': 'name'},
    {'name': 'age', 'label': 'Age', 'field': 'age'},
]
assert table.rows == [
    {'name': 'Alice', 'age': '30'},
    {'name': 'Bob', 'age': '28'},
]

````

## Test Downloads


    You can verify that a download was triggered by checking `user.download.http_responses`.
    By awaiting `user.download.next()` you can get the next download response.

    *Added in version 2.1.0*


some UI code

[Button]

````python
@ui.page('/')
def page():
    def download():
        ui.download(b'Hello', filename='hello.txt')

    ui.button('Download', on_click=download)

````

user assertions

[Button]

````python
await user.open('/')
assert len(user.download.http_responses) == 0
user.find('Download').click()
response = await user.download.next()
assert response.text == 'Hello'

````

## Multiple Users


    Sometimes it is not enough to just interact with the UI as a single user.
    Besides the `user` fixture, we also provide the `create_user` fixture which is a factory function to create users.
    The `User` instances are independent from each other and can interact with the UI in parallel.
    See our [Chat App example](https://github.com/zauberzeug/nicegui/blob/main/examples/chat_app/test_chat_app.py)
    for a full demonstration.


example

[Button]

````python
async def test_chat(create_user: Callable[[], User]) -> None:
    user1 = create_user()
    await user1.open('/')
    user2 = create_user()
    await user2.open('/')

    user1.find(ui.input).type('from A').trigger('keydown.enter')
    await user2.should_see('from A')
    user2.find(ui.input).type('from B').trigger('keydown.enter')
    await user1.should_see('from A')
    await user1.should_see('from B')

````

## Simulate JavasScript


    The `User` class has a `javascript_rules` dictionary to simulate JavaScript execution.
    The key is a compiled regular expression and the value is a function that returns the JavaScript response.
    The function will be called with the match object of the regular expression on the JavaScript command.

    *Added in version 2.14.0*


some UI code

[Button]

````python
@ui.page('/')
async def page():
    await context.client.connected()
    date = await ui.run_javascript('Math.sqrt(1764)')
    ui.label(date)

````

user assertions

[Button]

````python
user.javascript_rules[re.compile(r'Math.sqrt\((\d+)\)')] = \
    lambda match: int(match.group(1))**0.5
await user.open('/')
await user.should_see('42')

````

## Comparison with the screen fixture


    By cutting out the browser, test execution becomes much faster than the [`screen` fixture](/documentation/screen).
    See our [pytests example](https://github.com/zauberzeug/nicegui/tree/main/examples/pytests)
    which implements the same tests with both fixtures.
    Of course, some features like screenshots or browser-specific behavior are not available,
    but in most cases the speed of the `user` fixture makes it the first choice.


## User Simulation Context


    The [`user_simulation`](https://github.com/zauberzeug/nicegui/blob/main/nicegui/testing/user_simulation.py) context
    is the low-level building block behind the `user` fixture.
    It spins up a NiceGUI app inside the same event loop, providing deterministic test control without Selenium.
    Unlike the `user` fixture, it does not rely on pytest-specific infrastructure
    and can be used with `unittest` or within plain async code.

    The context supports three testing approaches:

    - Test a `root` callable directly.
    - Test a specific main file by passing its path.
    - Define `ui.page` definitions inline within the context.

    More usage examples can be found in
    [`tests/test_user_simulation_context.py`](https://github.com/zauberzeug/nicegui/blob/main/tests/test_user_simulation_context.py).


script mode with root

[Button]

````python
from nicegui.testing import user_simulation

async def test_click_via_root():
    def root():
        ui.button('Click me', on_click=lambda: ui.notify('Hello World!'))

    async with user_simulation(root) as user:
        await user.open('/')
        await user.should_see('Click me')
        user.find(ui.button).click()
        await user.should_see('Hello World!')

````

main file via path

[Button]

````python
from nicegui.testing import user_simulation

async def test_click_via_main_file():
    async with user_simulation(main_file='app.py') as user:
        await user.open('/')
        await user.should_see('Main file content')

````

inline UI definitions

[Button]

````python
from nicegui.testing import user_simulation

async def test_inline_pages():
    async with user_simulation() as user:

        @ui.page('/')
        def main_page():
            ui.label('Main page')

        await user.open('/')
        await user.should_see('Main page')

````

## User Reference

## Properties

**`current_layout`**`: 'ui.element'`

Return the root layout element of the current page.

## Methods

**`find`**`(target: str | type[T] | None = None, kind: type[T] | None = None, marker: str | list[str] | None = None, content: str | list[str] | None = None) -> UserInteraction[T]`

Select elements for interaction.

**`open`**`(path: str, clear_forward_history: bool = True) -> Client`

Open the given path.

**`should_not_see`**`(target: str | type[T] | None = None, kind: type[T] | None = None, marker: str | list[str] | None = None, content: str | list[str] | None = None, retries: int = 3) -> None`

Assert that the page does not contain an input with the given value.

**`should_see`**`(target: str | type[T] | None = None, kind: type[T] | None = None, marker: str | list[str] | None = None, content: str | list[str] | None = None, retries: int = 3) -> None`

Assert that the page contains an element fulfilling certain filter rules.

Note that there is no scrolling in the user simulation -- the entire page is always *visible*.
Due to asynchronous execution, sometimes the expected elements only appear after a short delay.

By default `should_see` makes three attempts to find the element before failing.
This can be adjusted with the `retries` parameter.

## UserInteraction Reference

## Methods

**`clear`**`() -> Self`

Clear the selected elements.

Note: All elements must have a ``value`` attribute.

**`click`**`() -> Self`

Click the selected elements.

**`trigger`**`(event: str, args: Any = None) -> Self`

Trigger the given event on the elements selected by the simulated user.

:param event: the event type to trigger (e.g. "keydown.enter", "click", ...)
:param args: optional event arguments to pass to the handler (default: ``None``)

**`type`**`(text: str) -> Self`

Type the given text into the selected elements.

Note: All elements must have a ``value`` attribute.

**Nice**GUI

The Python UI framework that shows up in your browser.

[](https://github.com/zauberzeug/nicegui/)

[](https://discord.gg/TEpFeAaF4f)

[](https://www.reddit.com/r/nicegui/)

Resources

[Documentation](/documentation)

[Examples](/examples)

[LLM reference](/llms.txt)

[GitHub](https://github.com/zauberzeug/nicegui/)

[PyPI](https://pypi.org/project/nicegui/)

Community

[Discussions](https://github.com/zauberzeug/nicegui/discussions)

[Discord](https://discord.gg/TEpFeAaF4f)

[Reddit](https://www.reddit.com/r/nicegui/)

[Contributing](https://github.com/zauberzeug/nicegui/blob/main/CONTRIBUTING.md)

[Sponsors](https://github.com/sponsors/zauberzeug)

Legal

[Imprint](/imprint_privacy#imprint)

[Privacy](/imprint_privacy#privacy)

Made with NiceGUI by [Zauberzeug](https://zauberzeug.com)

© 2026 [Zauberzeug GmbH](https://zauberzeug.com)