Usage¶
The Page class simulates a browser page for testing.
It runs a real V8 engine (via deno_core) with happy-dom providing the DOM,
and lets you drive it from Python — load pages, query and interact with DOM elements, fill forms,
click and trigger events.
If the page includes htmx, it runs normally and htmx requests are awaited automatically.
Creating a page¶
Create a page:
Page can also be used as a context manager (with Page() as page:) to close it on exit.
Page(...) accepts:
httpx_transport— anhttpx2.AsyncBaseTransport, useful to test an ASGI/WSGI app in-process without an HTTP server (see below).mounts— adict[str, Path]mapping a URL prefix to a local directory, so<script>tags can load local files (e.g. htmx itself or a bundle) without going through an HTTP request.
Async¶
An AsyncPage with the same constructor is available for async codebases:
Testing a WSGI/ASGI app in-process¶
You can test your WSGI/ASGI app directly (Django, Flask, FastAPI, etc)
with no HTTP server or network involved, by passing a miniclient.wsgi.WSGITransport to Page(...).
An example with nanodjango
(and htmx.min.js in the folder "path/to/htmx/dist"):
from pathlib import Path
from miniclient.page import Page
from miniclient.wsgi import WSGITransport
from nanodjango import Django
app = Django()
@app.route("/")
def index(request):
return """
<html>
<head>
<script src="http://localhost/static/htmx.min.js"></script>
</head>
<body>
<button hx-get="/hello" hx-target="#result">Say hi</button>
<div id="result"></div>
</body>
</html>
"""
@app.route("/hello")
def hello(request):
return "Hello from Django!"
with Page(
httpx_transport=WSGITransport(app=app.wsgi),
mounts={"http://localhost/static/": Path("path/to/htmx/dist")},
) as page:
page.goto("/")
page.find("button").click()
print(page.find("#result").text) # prints "Hello from Django!"
For an ASGI app instead, pass an httpx2.ASGITransport(app=app.asgi) — see
httpx's documentation.
Loading external scripts via mounts¶
Serve local files through mounts so a <script> tag can load them without a real server:
page = Page(mounts={"http://localhost/ext/": tmp_path})
page.eval(
'document.head.innerHTML = \'<script src="http://localhost/ext/external-script.js"></script>\''
)
Loading pages¶
Load content either via a real request, or directly as raw HTML:
# Fetch a URL, load the full document, and process htmx (real request via httpx_transport/network)
page.goto("http://localhost/page")
# Load raw HTML directly into the document body, no request involved
page.load("<p id='msg'>hello</p>")
Each call to load() replaces the previous body entirely.
Finding elements¶
Locate elements by CSS selector, returning Element wrappers:
el = page.find("#msg") # the first match, or None
items = page.find_all("li") # a list of all matches, possibly empty
Pass text to also filter by contained text (a substring match against textContent):
el = page.find("li", text="Buy milk") # first <li> containing this text, or None
items = page.find_all("li", text="urgent") # all <li>s containing this text
Element also exposes find()/find_all(), scoped to that element instead of the whole
document:
row = page.find("#results li")
label = row.find(".label") # searches only within `row`
badges = row.find_all(".badge", text="new")
Reading elements¶
Element exposes the usual ways to read content and attributes:
el.html # outerHTML — the element's tag plus its content
el.innerHTML # innerHTML — the element's content, without its own tag
el.text # textContent — all text inside, with tags stripped
el.attr("href") # value of the "href" attribute, or None if absent
el.parent # the parent Element, or None if there is no parent
Filling inputs¶
Set an input's value directly:
This works for <input>, <textarea> and <select> elements.
For <select>, this only takes effect if the value matches an existing <option>'s value,
just like in a real browser.
Clicking and triggering events¶
Simulate a click, or dispatch any DOM event:
page.find("button").click()
page.find("div").trigger("my-event") # any DOM event, e.g. for hx-trigger="my-event"
Both wait for the page to settle (any pending timers/fetches, e.g. from an htmx request, a
plain fetch(), or another framework's async work).
Submitting forms¶
page.find(...) returns a FormElement when the match is a <form>, which adds
requestSubmit():
If a script (e.g. htmx via hx-post, hx-get, ...) intercepts the submit, this waits for the
page to settle. If not, it performs the form's native GET/POST navigation and reloads the page.
Clicking a <button type="submit"> or <input type="submit"> inside the form works the same
way, through .click().
Executing JavaScript¶
For anything not covered by Page / Element, evaluate JavaScript directly.
With sync Page, use eval():
Element has its own eval(), with this bound to that element:
On an AsyncPage, elements also have eval_async():
async with AsyncPage() as page:
data = await page.find("#panel").eval_async("this.loadData()") # this.loadData() returns a promise
AsyncPage also has eval_async(), which awaits the result when the JavaScript returns a
promise: