Advanced events and pages

Copy Markdown View Source

The examples use these imports and aliases:

import Fluffy
import Fluffy.Locator
import Fluffy.Expect

alias Fluffy.{Dialog, Download, Event, Navigation, Page, Response}

Locator expectations remain imported from Fluffy.Expect; captured-result expectations live on the corresponding subject module, like page expectations.

wait_for(Event.*(...), action) installs the listener before running the action. Return the updated session from the callback. Captured results can be read repeatedly through their keys.

Downloads

session
|> wait_for(Event.download(:report), fn session ->
  click(session, by_role(:button, name: "Download potion ledger"))
end)
|> expect(Download.to_have_suggested_filename(:report, "potions.csv"))
|> expect(Download.to_have_content_type(:report, "text/csv"))

download(session, :report) returns %Fluffy.Download{} with filename, content_type, bytes, and url. Override the default retained-byte limit with max_bytes: on Event.download/2 or wait_for/4.

New pages and tabs (Playwright only)

session
|> wait_for(Event.popup(:secret_chamber), fn session ->
  click(session, by_role(:button, name: "Open chamber in new tab"))
end)
|> switch_page(:secret_chamber)
|> expect(Page.to_have_opener(:main))
|> expect(Page.to_have_url(path: "/chambers/secrets"))
|> expect(by_role(:heading, name: "Secret chamber") |> to_be_visible())
|> close_page()
|> expect(by_text("Creature index") |> to_be_visible())

Multiple pages and tabs require Playwright. Phoenix follows links and submits forms in its current page, ignoring target and formtarget, including _blank. Popup capture, page switching, and closing pages raise a capability error in Phoenix. Multiple isolated sessions remain supported by both backends. All pages inside one Playwright session share cookies/storage; independent sessions do not. page(session, :secret_chamber) returns the captured opaque Fluffy.Page; use Page.name/1, Page.url/1, Page.status/1, Page.opener/1, and Page.revision/1 to inspect its metadata without switching.

session
|> wait_for(Event.navigation(:forbidden_forest), fn session ->
  click(session, by_role(:link, name: "Follow the spiders"))
end)
|> expect(Navigation.to_have_url(:forbidden_forest, forbidden_forest_url))
|> expect(Navigation.to_have_status(:forbidden_forest, 200))

Captured URL assertions on Download, Navigation, Request, and Response accept exact strings, regexes, or structured path, query, and fragment matchers. This also applies to Navigation.to_have_from_url/3. Strings are compared as captured, without resolving relative URLs. Structured matching follows the same rules as page URL assertions: query matching is exact by default; use query_mode: :subset to allow additional parameter names.

session
|> expect(Navigation.to_have_url(:forbidden_forest,
  path: "/forest",
  query: %{"guide" => "spiders"},
  query_mode: :subset
))

These assertions inspect the retained event result; they do not wait for the active page to change. wait_for/3 handles waiting for the event.

Dialogs

Dialogs are atomic browser-only events. The decision is installed before the action so an unhandled modal cannot deadlock the click:

session
|> wait_for(Event.dialog(:release_basilisk, accept: true), fn session ->
  click(session, by_role(:button, name: "Release basilisk"))
end)
|> expect(Dialog.to_have_type(:release_basilisk, :confirm))
|> expect(Dialog.to_have_message(:release_basilisk, "Release the basilisk?"))
|> expect(Dialog.to_have_action(:release_basilisk, :accept))

Use :dismiss, {:accept, "prompt text"}, or a decision function receiving the unhandled %Fluffy.Dialog{}.

Requests and responses

session
|> wait_for(Event.response(:potions, ~r{/api/potions}), fn session ->
  click(session, by_role(:button, name: "Refresh potions"))
end)
|> expect(Response.to_have_status(:potions, 200))
|> expect(Response.to_have_resource_type(:potions, "fetch"))

Matchers can be an exact URL, regex, or normalized-event predicate. These are browser-wide subresource streams and therefore Playwright-only. The event contains method, URL, headers, resource type, post data, status fields, and the known page name.

File choosers

Directly locating the file input is preferred, even when it is hidden. Applications sometimes expose only a button whose JavaScript opens the input's native chooser. Capture that browser event before clicking, then pass its key to the same file-selection action:

session
|> wait_for(Event.file_chooser(:portrait), fn session ->
  click(session, by_role(:button, name: "Choose creature portrait"))
end)
|> set_input_files(:portrait, %Fluffy.FilePayload{
  name: "fluffy.png",
  bytes: png_bytes,
  content_type: "image/png"
})

file_chooser(session, :portrait) returns the opaque %Fluffy.FileChooser{}; Fluffy.FileChooser.multiple?/1 exposes whether it accepts multiple files. The chooser result is non-consuming and lives with the session, but it belongs to the page that opened it. Scripted chooser events are Playwright-only because Static and LiveView do not execute application JavaScript. This API sets files programmatically; it does not operate the OS picker UI.

See Browser diagnostics for traces, screenshots, and failure artifacts.