Различить исходы страницы#

После действия страница может подтвердить успех, показать отказ или не изменить наблюдаемое состояние. outcomes(...) связывает признаки страницы с типизированными результатами:

# examples/mail/browser/send.py
class ComposerPage:
    recipient: Annotated[Element, css('input[name="recipient"]')]
    subject: Annotated[Element, css('input[name="subject"]')]
    body: Annotated[Element, css('textarea[name="body"]')]
    submit: Annotated[Element, css('button[type="submit"]')]
    rejected: Annotated[Element, css('[data-outcome="rejected"]')]


@dataclass(frozen=True, slots=True)
class Sent:
    pass


@dataclass(frozen=True, slots=True)
class Rejected:
    pass


@dataclass(frozen=True, slots=True)
class Silent:
    pass


async def _sent(content: ComposerPage) -> Sent:
    _ = content
    return Sent()


async def _rejected(content: ComposerPage) -> Rejected:
    _ = content
    return Rejected()


async def _silent(content: ComposerPage) -> Silent:
    _ = content
    return Silent()


@dataclass(frozen=True, slots=True, kw_only=True)
class OpenComposer(BrowserOperation[None, None]):
    __browser__ = Browser.goto("/compose/", at=css("[data-composer]"))


@dataclass(frozen=True, slots=True, kw_only=True)
class SubmitMessage(BrowserOperation[ComposerPage, Sent | Rejected | Silent]):
    __browser__ = Browser.act(
        ComposerPage,
        at=css("[data-composer]"),
        outcomes=outcomes(
            when(visible(css('[role="status"]')), then=_sent),
            when(visible(css('[data-outcome="rejected"]')), then=_rejected),
            otherwise=_silent,
        ),
    )

    recipient: str
    subject: str
    body: str

    async def act(self, content: ComposerPage) -> None:
        if await content.rejected.visible():
            await content.rejected.press("Escape")
        await content.recipient.fill(self.recipient)
        await content.subject.fill(self.subject)
        await content.body.fill(self.body)
        await content.submit.click()


class ComposerApi(AsyncBrowserApi):
    open = op(OpenComposer)
    send = op(SubmitMessage)

Один запуск проходит все три состояния настоящей страницы:

    # examples/docs/integration_browser_outcomes.py
    sent, rejected, silent = await send_three_messages()
    outcomes = type(sent).__name__, type(rejected).__name__, type(silent).__name__
Файл целиком
"""Run one declared browser operation through all composer outcomes."""

from __future__ import annotations

import asyncio

from examples.mail.browser.send import send_three_messages


async def main() -> None:
    # docs:integration-browser-outcomes:start
    # examples/docs/integration_browser_outcomes.py
    sent, rejected, silent = await send_three_messages()
    outcomes = type(sent).__name__, type(rejected).__name__, type(silent).__name__
    # docs:integration-browser-outcomes:end
    print(f"outcomes: {', '.join(outcomes)}")


if __name__ == "__main__":
    asyncio.run(main())
outcomes: Sent, Rejected, Silent

Что наблюдает клиент

Что срабатывает

Что получает вызывающий

Видимая строка состояния

первый when(...)

Sent

Видимый диалог отказа

второй when(...)

Rejected

Ни один признак до дедлайна

otherwise

Silent

Правила Failure(...) проверяются перед успешными исходами и выбрасывают PageError. Помехи Handle(...) выполняются ещё раньше, чтобы баннер или диалог не скрывал настоящий результат. Первое совпавшее правило завершает ожидание.

Примечание

otherwise должен отражать реальный контракт сайта. Верните отдельный тип для допустимого молчания или выбросьте исключение, если отсутствие признака означает сбой.

Дальше#